aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/dynamic_matchers.rb
blob: b41546064b8bed4d0ed2ee836ffb662986df85ed (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
123
require "active_support/core_ext/regexp"

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

    private

    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! { |n| @model.attribute_aliases[n] || n }
      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