attribute_methods.rb 10.6 KB
Newer Older
J
Jeremy Kemper 已提交
1 2
require 'active_support/core_ext/enumerable'

3 4
module ActiveRecord
  module AttributeMethods #:nodoc:
5
    extend ActiveSupport::Concern
6

7 8
    class AttributeMethodMatcher
      attr_reader :prefix, :suffix
J
Joshua Peek 已提交
9

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
      AttributeMethodMatch = Struct.new(:prefix, :base, :suffix)

      def initialize(options = {})
        options.symbolize_keys!
        @prefix, @suffix = options[:prefix] || '', options[:suffix] || ''
        @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/
      end

      def match(method_name)
        if matchdata = @regex.match(method_name)
          AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3])
        else
          nil
        end
      end
    end

27 28
    # Declare and check for suffixed attribute methods.
    module ClassMethods
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
      # Declares a method available for all attributes with the given prefix.
      # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
      #
      #   #{prefix}#{attr}(*args, &block)
      #
      # to
      #
      #   #{prefix}attribute(#{attr}, *args, &block)
      #
      # An <tt>#{prefix}attribute</tt> instance method must exist and accept at least
      # the +attr+ argument.
      #
      # For example:
      #
      #   class Person < ActiveRecord::Base
      #     attribute_method_prefix 'clear_'
      #
      #     private
      #       def clear_attribute(attr)
      #         ...
      #       end
      #   end
      #
      #   person = Person.find(1)
      #   person.name          # => 'Gem'
      #   person.clear_name
      #   person.name          # => ''
      def attribute_method_prefix(*prefixes)
        attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix })
        undefine_attribute_methods
      end

61
      # Declares a method available for all attributes with the given suffix.
62
      # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
63
      #
64
      #   #{attr}#{suffix}(*args, &block)
65
      #
66
      # to
67
      #
68 69
      #   attribute#{suffix}(#{attr}, *args, &block)
      #
70 71
      # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
      # the +attr+ argument.
72 73
      #
      # For example:
74
      #
75
      #   class Person < ActiveRecord::Base
76
      #     attribute_method_suffix '_short?'
77 78
      #
      #     private
79
      #       def attribute_short?(attr)
80 81 82 83 84
      #         ...
      #       end
      #   end
      #
      #   person = Person.find(1)
85 86
      #   person.name           # => 'Gem'
      #   person.name_short?    # => true
87
      def attribute_method_suffix(*suffixes)
88
        attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix })
89
        undefine_attribute_methods
90 91
      end

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
      # Declares a method available for all attributes with the given prefix
      # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
      # the method.
      #
      #   #{prefix}#{attr}#{suffix}(*args, &block)
      #
      # to
      #
      #   #{prefix}attribute#{suffix}(#{attr}, *args, &block)
      #
      # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
      # accept at least the +attr+ argument.
      #
      # For example:
      #
      #   class Person < ActiveRecord::Base
      #     attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!'
      #
      #     private
      #       def reset_attribute_to_default!(attr)
      #         ...
      #       end
      #   end
      #
      #   person = Person.find(1)
      #   person.name                         # => 'Gem'
      #   person.reset_name_to_default!
      #   person.name                         # => 'Gemma'
      def attribute_method_affix(*affixes)
        attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] })
        undefine_attribute_methods
      end
J
Joshua Peek 已提交
124

125 126 127
      def matching_attribute_methods(method_name)
        attribute_method_matchers.collect { |method| method.match(method_name) }.compact
      end
J
Joshua Peek 已提交
128

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
      # Defines an "attribute" method (like +inheritance_column+ or
      # +table_name+). A new (class) method will be created with the
      # given name. If a value is specified, the new method will
      # return that value (as a string). Otherwise, the given block
      # will be used to compute the value of the method.
      #
      # The original method will be aliased, with the new name being
      # prefixed with "original_". This allows the new method to
      # access the original value.
      #
      # Example:
      #
      #   class A < ActiveRecord::Base
      #     define_attr_method :primary_key, "sysid"
      #     define_attr_method( :inheritance_column ) do
      #       original_inheritance_column + "_id"
      #     end
      #   end
      def define_attr_method(name, value=nil, &block)
        sing = metaclass
        sing.send :alias_method, "original_#{name}", name
        if block_given?
          sing.send :define_method, name, &block
        else
          # use eval instead of a block to work around a memory leak in dev
          # mode in fcgi
          sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
        end
      end

159
      def generated_methods #:nodoc:
160 161 162 163 164
        @generated_methods ||= begin
          mod = Module.new
          include mod
          mod
        end
165
      end
J
Joshua Peek 已提交
166

167 168
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
169
      def define_attribute_methods
170
        return unless generated_methods.instance_methods.empty?
J
Joshua Peek 已提交
171
        columns_hash.keys.each do |name|
172 173
          attribute_method_matchers.each do |method|
            method_name = "#{method.prefix}#{name}#{method.suffix}"
J
Joshua Peek 已提交
174
            unless instance_method_already_implemented?(method_name)
175
              generate_method = "define_method_#{method.prefix}attribute#{method.suffix}"
J
Joshua Peek 已提交
176

J
Joshua Peek 已提交
177 178 179
              if respond_to?(generate_method)
                send(generate_method, name)
              else
180
                generated_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
J
Joshua Peek 已提交
181
              end
182
            end
183 184 185
          end
        end
      end
186

187
      def undefine_attribute_methods
188 189 190
        generated_methods.module_eval do
          instance_methods.each { |m| undef_method(m) }
        end
191 192
      end

193
      # Checks whether the method is defined in the model or any of its subclasses
J
Joshua Peek 已提交
194 195
      # that also derive from Active Record. Raises DangerousAttributeError if the
      # method is defined by Active Record though.
196
      def instance_method_already_implemented?(method_name)
J
Jeremy Kemper 已提交
197
        method_name = method_name.to_s
J
Joshua Peek 已提交
198 199 200
        @_defined_class_methods         ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set
        @@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map{|m| m.to_s }.to_set
        raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
201
        @_defined_class_methods.include?(method_name)
202
      end
203

204
      private
205 206 207
        # Default to *=, *? and *_before_type_cast
        def attribute_method_matchers
          @@attribute_method_matchers ||= []
208
        end
J
Joshua Peek 已提交
209
    end
210

211 212 213 214 215 216 217
    # Returns a struct representing the matching attribute method.
    # The struct's attributes are prefix, base and suffix.
    def match_attribute_method?(method_name)
      self.class.matching_attribute_methods(method_name).find do |match|
        match.base == 'id' || @attributes.include?(match.base)
      end
    end
J
Joshua Peek 已提交
218

219
    # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
220
    # were first-class methods. So a Person class with a name attribute can use Person#name and
221 222
    # Person#name= and never directly use the attributes hash -- except for multiple assigns with
    # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
223
    # the completed attribute is not +nil+ or 0.
224 225
    #
    # It's also possible to instantiate related objects, so a Client class belonging to the clients
226
    # table with a +master_id+ foreign key can instantiate master through Client#master.
227 228 229 230 231
    def method_missing(method_id, *args, &block)
      method_name = method_id.to_s

      # If we haven't generated any methods yet, generate them, then
      # see if we've created the method we're looking for.
232
      if self.class.generated_methods.instance_methods.empty?
233
        self.class.define_attribute_methods
234
        guard_private_attribute_method!(method_name, args)
235
        if self.class.generated_methods.instance_methods.include?(method_name)
236 237 238
          return self.send(method_id, *args, &block)
        end
      end
J
Joshua Peek 已提交
239

240 241 242
      if match = match_attribute_method?(method_name)
        guard_private_attribute_method!(method_name, args)
        return __send__("#{match.prefix}attribute#{match.suffix}", match.base, *args, &block)
243
      end
244
      super
245 246
    end

247 248
    # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
    # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
249
    # which will all return +true+.
250
    alias :respond_to_without_attributes? :respond_to?
251
    def respond_to?(method, include_private_methods = false)
252 253 254
      method_name = method.to_s
      if super
        return true
255
      elsif !include_private_methods && super(method, true)
256
        # If we're here then we haven't found among non-private methods
257
        # but found among all methods. Which means that given method is private.
258
        return false
259
      elsif self.class.generated_methods.instance_methods.empty?
260
        self.class.define_attribute_methods
261
        if self.class.generated_methods.instance_methods.include?(method_name)
262 263
          return true
        end
264 265
      elsif match_attribute_method?(method_name)
        return true
266 267 268
      end
      super
    end
269 270

    private
271 272 273 274 275 276
      # prevent method_missing from calling private methods with #send
      def guard_private_attribute_method!(method_name, args)
        if self.class.private_method_defined?(method_name)
          raise NoMethodError.new("Attempt to call private method", method_name, args)
        end
      end
J
Joshua Peek 已提交
277

278 279 280
      def missing_attribute(attr_name, stack)
        raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
      end
281 282
  end
end