aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/dynamic_matchers.rb
blob: 2c941b0008e91a8bae10bd3560ac0a081e906daf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# frozen_string_literal: true

module ActiveRecord
  module DynamicMatchers #:nodoc:
    private
      def respond_to_missing?(name, _)
        if self == Base
          super
        else
          match = Method.match(self, name)
          match && match.valid? || super
        end
      end

      def method_missing(name, *arguments, &block)
        match = Method.match(self, name)

        if match && match.valid?
          match.define
          send(name, *arguments, &block)
        else
          super
        end
      end

      class Method
        @matchers = []

        class << self
          attr_reader :matchers

          def match(model, name)
            klass = matchers.find { |k| k.pattern.match?(name) }
            klass.new(model, name) if klass
          end

          def pattern
            @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
          end

          def prefix
            raise NotImplementedError
          end

          def suffix
            ""
          end
        end

        attr_reader :model, :name, :attribute_names

        def initialize(model, name)
          @model           = model
          @name            = name.to_s
          @attribute_names = @name.match(self.class.pattern)[1].split("_and_")
          @attribute_names.map! { |name| @model.attribute_aliases[name] || name }
        end

        def valid?
          attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
        end

        def define
          model.class_eval <<-CODE, __FILE__, __LINE__ + 1
            def self.#{name}(#{signature})
              #{body}
            end
          CODE
        end

        private

          def body
            "#{finder}(#{attributes_hash})"
          end

          # The parameters in the signature may have reserved Ruby words, in order
          # to prevent errors, we start each param name with `_`.
          def signature
            attribute_names.map { |name| "_#{name}" }.join(", ")
          end

          # Given that the parameters starts with `_`, the finder needs to use the
          # same parameter name.
          def attributes_hash
            "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
          end

          def finder
            raise NotImplementedError
          end
      end

      class FindBy < Method
        Method.matchers << self

        def self.prefix
          "find_by"
        end

        def finder
          "find_by"
        end
      end

      class FindByBang < Method
        Method.matchers << self

        def self.prefix
          "find_by"
        end

        def self.suffix
          "!"
        end

        def finder
          "find_by!"
        end
      end
  end
end