diff options
Diffstat (limited to 'activemodel/lib/active_model/attribute_methods.rb')
-rw-r--r-- | activemodel/lib/active_model/attribute_methods.rb | 192 |
1 files changed, 93 insertions, 99 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index a43436e008..be55581c66 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' module ActiveModel class MissingAttributeError < NoMethodError @@ -9,46 +9,46 @@ module ActiveModel # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and suffixes # to your methods as well as handling the creation of Active Record like class methods # such as +table_name+. - # + # # The requirements to implement ActiveModel::AttributeMethods are to: # # * <tt>include ActiveModel::AttributeMethods</tt> in your object - # * Call each Attribute Method module method you want to add, such as + # * Call each Attribute Method module method you want to add, such as # attribute_method_suffix or attribute_method_prefix # * Call <tt>define_attribute_methods</tt> after the other methods are # called. # * Define the various generic +_attribute+ methods that you have declared - # + # # A minimal implementation could be: - # + # # class Person # include ActiveModel::AttributeMethods - # + # # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # attribute_method_suffix '_contrived?' # attribute_method_prefix 'clear_' # define_attribute_methods ['name'] - # + # # attr_accessor :name - # + # # private - # + # # def attribute_contrived?(attr) # true # end - # + # # def clear_attribute(attr) # send("#{attr}=", nil) # end - # + # # def reset_attribute_to_default!(attr) # send("#{attr}=", "Default Name") # end # end # - # Notice that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement a <tt>attributes</tt> methods which returns a hash - # with each attribute name in your model as hash key and the attribute value as + # Note that whenever you include ActiveModel::AttributeMethods in your class, + # it requires you to implement an <tt>attributes</tt> method which returns a hash + # with each attribute name in your model as hash key and the attribute value as # hash value. # # Hash keys must be strings. @@ -56,35 +56,40 @@ module ActiveModel module AttributeMethods extend ActiveSupport::Concern + included do + class_attribute :attribute_method_matchers, :instance_writer => false + self.attribute_method_matchers = [] + end + module ClassMethods - # 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 + # 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 + # with "original_". This allows the new method to access the original # value. # # Example: # # class Person - # + # # include ActiveModel::AttributeMethods - # + # # cattr_accessor :primary_key # cattr_accessor :inheritance_column - # + # # define_attr_method :primary_key, "sysid" # define_attr_method( :inheritance_column ) do # original_inheritance_column + "_id" # end - # + # # end - # + # # Provides you with: - # + # # AttributePerson.primary_key # # => "sysid" # AttributePerson.inheritance_column = 'address' @@ -93,19 +98,22 @@ module ActiveModel def define_attr_method(name, value=nil, &block) sing = singleton_class sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 - if method_defined?(:original_#{name}) - undef :original_#{name} + if method_defined?('original_#{name}') + undef :'original_#{name}' end - alias_method :original_#{name}, :#{name} + alias_method :'original_#{name}', :'#{name}' eorb 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 <<-eorb, __FILE__, __LINE__ + 1 - def #{name}; #{value.to_s.inspect}; end - eorb + if name =~ /^[a-zA-Z_]\w*[!?=]?$/ + sing.class_eval <<-eorb, __FILE__, __LINE__ + 1 + def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end + eorb + else + value = value.to_s if value + sing.send(:define_method, name) { value } + end end end @@ -118,20 +126,20 @@ module ActiveModel # # #{prefix}attribute(#{attr}, *args, &block) # - # An instance method <tt>#{prefix}attribute</tt> must exist and accept + # An instance method <tt>#{prefix}attribute</tt> must exist and accept # at least the +attr+ argument. # # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_prefix 'clear_' # define_attribute_methods [:name] # # private - # + # # def clear_attribute(attr) # send("#{attr}=", nil) # end @@ -143,7 +151,7 @@ module ActiveModel # person.clear_name # person.name # => nil def attribute_method_prefix(*prefixes) - attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix }) + self.attribute_method_matchers += prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix } undefine_attribute_methods end @@ -162,14 +170,14 @@ module ActiveModel # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_suffix '_short?' # define_attribute_methods [:name] # # private - # + # # def attribute_short?(attr) # send(attr).length < 5 # end @@ -180,7 +188,7 @@ module ActiveModel # person.name # => "Bob" # person.name_short? # => true def attribute_method_suffix(*suffixes) - attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix }) + self.attribute_method_matchers += suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix } undefine_attribute_methods end @@ -200,14 +208,14 @@ module ActiveModel # For example: # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' # define_attribute_methods [:name] # # private - # + # # def reset_attribute_to_default!(attr) # ... # end @@ -218,29 +226,27 @@ module ActiveModel # 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] }) + self.attribute_method_matchers += affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] } undefine_attribute_methods end def alias_attribute(new_name, old_name) attribute_method_matchers.each do |matcher| - module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{matcher.method_name(new_name)}(*args) - send(:#{matcher.method_name(old_name)}, *args) - end - STR + define_method(matcher.method_name(new_name)) do |*args| + send(matcher.method_name(old_name), *args) + end end end - # Declares a the attributes that should be prefixed and suffixed by + # Declares the attributes that should be prefixed and suffixed by # ActiveModel::AttributeMethods. - # + # # To use, pass in an array of attribute names (as strings or symbols), # be sure to declare +define_attribute_methods+ after you define any # prefix, suffix or affix methods, or they will not hook in. - # + # # class Person - # + # # include ActiveModel::AttributeMethods # attr_accessor :name, :age, :address # attribute_method_prefix 'clear_' @@ -251,36 +257,36 @@ module ActiveModel # define_attribute_methods [:name, :age, :address] # # private - # + # # def clear_attribute(attr) # ... # end # end def define_attribute_methods(attr_names) - return if attribute_methods_generated? - attr_names.each do |attr_name| - attribute_method_matchers.each do |matcher| - unless instance_method_already_implemented?(matcher.method_name(attr_name)) - generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + attr_names.each { |attr_name| define_attribute_method(attr_name) } + end - if respond_to?(generate_method) - send(generate_method, attr_name) - else - method_name = matcher.method_name(attr_name) + def define_attribute_method(attr_name) + attribute_method_matchers.each do |matcher| + unless instance_method_already_implemented?(matcher.method_name(attr_name)) + generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + + if respond_to?(generate_method) + send(generate_method, attr_name) + else + method_name = matcher.method_name(attr_name) - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - if method_defined?(:#{method_name}) - undef :#{method_name} - end - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - STR - end + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + if method_defined?('#{method_name}') + undef :'#{method_name}' + end + define_method('#{method_name}') do |*args| + send('#{matcher.method_missing_target}', '#{attr_name}', *args) + end + STR end end end - @attribute_methods_generated = true end # Removes all the previously dynamically defined methods from the class @@ -288,7 +294,6 @@ module ActiveModel generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } end - @attribute_methods_generated = nil end # Returns true if the attribute methods defined have been generated. @@ -300,11 +305,6 @@ module ActiveModel end end - # Returns true if the attribute methods defined have been generated. - def attribute_methods_generated? - @attribute_methods_generated ||= nil - end - protected def instance_method_already_implemented?(method_name) method_defined?(method_name) @@ -312,7 +312,7 @@ module ActiveModel private class AttributeMethodMatcher - attr_reader :prefix, :suffix + attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name) @@ -320,40 +320,34 @@ module ActiveModel options.symbolize_keys! @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ + @method_missing_target = "#{@prefix}attribute#{@suffix}" + @method_name = "#{prefix}%s#{suffix}" end def match(method_name) - if matchdata = @regex.match(method_name) - AttributeMethodMatch.new(method_missing_target, matchdata[2]) + if @regex =~ method_name + AttributeMethodMatch.new(method_missing_target, $2) else nil end end def method_name(attr_name) - "#{prefix}#{attr_name}#{suffix}" - end - - def method_missing_target - :"#{prefix}attribute#{suffix}" + @method_name % attr_name end end - - def attribute_method_matchers #:nodoc: - read_inheritable_attribute(:attribute_method_matchers) || write_inheritable_attribute(:attribute_method_matchers, []) - end end - # Allows access to the object attributes, which are held in the - # <tt>@attributes</tt> hash, as though they were first-class methods. So a - # Person class with a name attribute can use Person#name and Person#name= + # Allows access to the object attributes, which are held in the + # <tt>@attributes</tt> hash, as though they were first-class methods. So a + # Person class with a name attribute can use Person#name and 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 the completed attribute is not +nil+ + # with ActiveRecord#attributes=. A Milestone class can also ask + # Milestone#completed? to test that the completed attribute is not +nil+ # or 0. # - # It's also possible to instantiate related objects, so a Client class - # belonging to the clients table with a +master_id+ foreign key can + # It's also possible to instantiate related objects, so a Client class + # belonging to the clients table with a +master_id+ foreign key can # instantiate master through Client#master. def method_missing(method_id, *args, &block) method_name = method_id.to_s @@ -390,7 +384,7 @@ module ActiveModel # 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.send(:attribute_method_matchers).each do |method| + self.class.attribute_method_matchers.each do |method| if (match = method.match(method_name)) && attribute_method?(match.attr_name) return match end @@ -401,7 +395,7 @@ module ActiveModel # 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) + raise NoMethodError.new("Attempt to call private method `#{method_name}'", method_name, args) end end |