dynamic_matchers.rb 2.7 KB
Newer Older
1
require "active_support/core_ext/regexp"
X
Xavier Noria 已提交
2

3
module ActiveRecord
4
  module DynamicMatchers #:nodoc:
5
    def respond_to_missing?(name, include_private = false)
6 7 8 9 10 11
      if self == Base
        super
      else
        match = Method.match(self, name)
        match && match.valid? || super
      end
12 13 14 15
    end

    private

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

19 20 21 22 23 24
        if match && match.valid?
          match.define
          send(name, *arguments, &block)
        else
          super
        end
25 26
      end

27 28
      class Method
        @matchers = []
29

30 31
        class << self
          attr_reader :matchers
32

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

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

42 43 44
          def prefix
            raise NotImplementedError
          end
45

46 47 48
          def suffix
            ""
          end
49
        end
50

51
        attr_reader :model, :name, :attribute_names
52

53 54 55 56 57 58
        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
59

60 61 62
        def valid?
          attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
        end
J
Jon Leighton 已提交
63

64 65
        def define
          model.class_eval <<-CODE, __FILE__, __LINE__ + 1
66 67 68 69
            def self.#{name}(#{signature})
              #{body}
            end
          CODE
70
        end
71

72
        private
73

74 75 76
          def body
            "#{finder}(#{attributes_hash})"
          end
77

78 79 80 81 82
        # 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
83

84 85 86 87 88
        # 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
89

90 91 92
          def finder
            raise NotImplementedError
          end
93 94
      end

95 96
      class FindBy < Method
        Method.matchers << self
97

98 99 100
        def self.prefix
          "find_by"
        end
101

102 103 104
        def finder
          "find_by"
        end
105 106
      end

107 108
      class FindByBang < Method
        Method.matchers << self
109

110 111 112
        def self.prefix
          "find_by"
        end
113

114 115 116
        def self.suffix
          "!"
        end
117

118 119 120
        def finder
          "find_by!"
        end
121
      end
122 123
  end
end