attribute_methods.rb 7.9 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
    ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
8

9 10 11
    included do
      cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
      self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
12 13 14 15
    end

    # Declare and check for suffixed attribute methods.
    module ClassMethods
16 17 18
      # Declares a method available for all attributes with the given suffix.
      # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
      #
19
      #   #{attr}#{suffix}(*args, &block)
20
      #
21
      # to
22
      #
23 24
      #   attribute#{suffix}(#{attr}, *args, &block)
      #
25 26
      # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
      # the +attr+ argument.
27 28
      #
      # For example:
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
      #   class Person < ActiveRecord::Base
      #     attribute_method_suffix '_changed?'
      #
      #     private
      #       def attribute_changed?(attr)
      #         ...
      #       end
      #   end
      #
      #   person = Person.find(1)
      #   person.name_changed?    # => false
      #   person.name = 'Hubert'
      #   person.name_changed?    # => true
      def attribute_method_suffix(*suffixes)
        attribute_method_suffixes.concat suffixes
        rebuild_attribute_method_regexp
      end

      # Returns MatchData if method_name is an attribute method.
      def match_attribute_method?(method_name)
        rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
        @@attribute_method_regexp.match(method_name)
      end

54 55 56 57
      # Contains the names of the generated attribute methods.
      def generated_methods #:nodoc:
        @generated_methods ||= Set.new
      end
J
Joshua Peek 已提交
58

59 60 61
      def generated_methods?
        !generated_methods.empty?
      end
J
Joshua Peek 已提交
62

63 64
      # Generates all the attribute related methods for columns in the database
      # accessors, mutators and query methods.
65 66
      def define_attribute_methods
        return if generated_methods?
J
Joshua Peek 已提交
67
        columns_hash.keys.each do |name|
J
Joshua Peek 已提交
68
          attribute_method_suffixes.each do |suffix|
J
Joshua Peek 已提交
69 70 71 72 73 74 75 76
            method_name = "#{name}#{suffix}"
            unless instance_method_already_implemented?(method_name)
              generate_method = "define_attribute_method#{suffix}"
              if respond_to?(generate_method)
                send(generate_method, name)
              else
                evaluate_attribute_method(name, "def #{method_name}(*args); attribute#{suffix}('#{name}', *args); end", method_name)
              end
77
            end
78 79 80
          end
        end
      end
81

82 83 84 85 86
      def undefine_attribute_methods
        generated_methods.each { |name| undef_method(name) }
        @generated_methods = nil
      end

87
      # Checks whether the method is defined in the model or any of its subclasses
J
Joshua Peek 已提交
88 89
      # that also derive from Active Record. Raises DangerousAttributeError if the
      # method is defined by Active Record though.
90
      def instance_method_already_implemented?(method_name)
J
Jeremy Kemper 已提交
91
        method_name = method_name.to_s
J
Joshua Peek 已提交
92 93 94 95
        return true if method_name =~ /^id(=$|\?$|_before_type_cast$|$)/
        @_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)
96
        @_defined_class_methods.include?(method_name)
97
      end
98

99 100 101 102 103 104 105 106 107 108
      private
        # Suffixes a, ?, c become regexp /(a|\?|c)$/
        def rebuild_attribute_method_regexp
          suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
          @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
        end

        def attribute_method_suffixes
          @@attribute_method_suffixes ||= []
        end
109 110

        # Evaluate the definition for an attribute related method
J
Joshua Peek 已提交
111
        def evaluate_attribute_method(attr_name, method_definition, method_name)
J
Joshua Peek 已提交
112 113 114
          unless method_name.to_s == primary_key.to_s
            generated_methods << method_name
          end
115 116

          begin
J
Jeremy Kemper 已提交
117
            class_eval(method_definition, __FILE__, __LINE__)
118 119 120 121 122
          rescue SyntaxError => err
            generated_methods.delete(attr_name)
            if logger
              logger.warn "Exception occurred during reader method compilation."
              logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
123
              logger.warn err.message
124 125 126
            end
          end
        end
J
Joshua Peek 已提交
127
    end
128

129
    # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
130
    # were first-class methods. So a Person class with a name attribute can use Person#name and
131 132
    # 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
133
    # the completed attribute is not +nil+ or 0.
134 135
    #
    # It's also possible to instantiate related objects, so a Client class belonging to the clients
136
    # table with a +master_id+ foreign key can instantiate master through Client#master.
137 138 139 140 141 142 143
    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.
      if !self.class.generated_methods?
        self.class.define_attribute_methods
144
        guard_private_attribute_method!(method_name, args)
145 146 147 148
        if self.class.generated_methods.include?(method_name)
          return self.send(method_id, *args, &block)
        end
      end
J
Joshua Peek 已提交
149

150
      guard_private_attribute_method!(method_name, args)
151 152 153 154 155 156 157 158 159 160 161 162 163 164
      if self.class.primary_key.to_s == method_name
        id
      elsif md = self.class.match_attribute_method?(method_name)
        attribute_name, method_type = md.pre_match, md.to_s
        if @attributes.include?(attribute_name)
          __send__("attribute#{method_type}", attribute_name, *args, &block)
        else
          super
        end
      else
        super
      end
    end

165 166
    # 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>
167
    # which will all return +true+.
168
    alias :respond_to_without_attributes? :respond_to?
169
    def respond_to?(method, include_private_methods = false)
170 171 172
      method_name = method.to_s
      if super
        return true
173 174 175
      elsif !include_private_methods && super(method, true)
        # If we're here than we haven't found among non-private methods
        # but found among all methods. Which means that given method is private.
176
        return false
177 178 179 180 181 182
      elsif !self.class.generated_methods?
        self.class.define_attribute_methods
        if self.class.generated_methods.include?(method_name)
          return true
        end
      end
J
Joshua Peek 已提交
183

184
      if md = self.class.match_attribute_method?(method_name)
185 186 187 188
        return true if @attributes.include?(md.pre_match)
      end
      super
    end
189 190

    private
191 192 193 194 195 196
      # 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 已提交
197

198 199 200
      def missing_attribute(attr_name, stack)
        raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
      end
201 202
  end
end