aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorJoshua Peek <josh@joshpeek.com>2009-08-04 23:36:05 -0500
committerJoshua Peek <josh@joshpeek.com>2009-08-04 23:36:05 -0500
commitf8d3c72c39ad209abca7f3613f91fb3a03805261 (patch)
tree2d6f5b67e3212b59be1f00391eeb64ebf5b40548 /activerecord
parent64eecdd131c93d7d6c8ef9c6a7ae6b9d76c72a8b (diff)
downloadrails-f8d3c72c39ad209abca7f3613f91fb3a03805261.tar.gz
rails-f8d3c72c39ad209abca7f3613f91fb3a03805261.tar.bz2
rails-f8d3c72c39ad209abca7f3613f91fb3a03805261.zip
Extract generic attribute method generation to AMo
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb251
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb2
-rwxr-xr-xactiverecord/lib/active_record/base.rb5
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb47
-rw-r--r--activerecord/test/cases/finder_test.rb2
7 files changed, 16 insertions, 299 deletions
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index be275f5cb6..ab7ad34b9e 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -3,191 +3,13 @@ require 'active_support/core_ext/enumerable'
module ActiveRecord
module AttributeMethods #:nodoc:
extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
- class AttributeMethodMatcher
- attr_reader :prefix, :suffix
-
- 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
-
- # Declare and check for suffixed attribute methods.
module ClassMethods
- # 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
-
- # Declares a method available for all attributes with the given suffix.
- # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
- #
- # #{attr}#{suffix}(*args, &block)
- #
- # to
- #
- # attribute#{suffix}(#{attr}, *args, &block)
- #
- # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
- # the +attr+ argument.
- #
- # For example:
- #
- # class Person < ActiveRecord::Base
- # attribute_method_suffix '_short?'
- #
- # private
- # def attribute_short?(attr)
- # ...
- # end
- # end
- #
- # person = Person.find(1)
- # person.name # => 'Gem'
- # person.name_short? # => true
- def attribute_method_suffix(*suffixes)
- attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix })
- undefine_attribute_methods
- end
-
- # 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
-
- def matching_attribute_methods(method_name)
- attribute_method_matchers.collect { |method| method.match(method_name) }.compact
- end
-
- # 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
-
- def generated_methods #:nodoc:
- @generated_methods ||= begin
- mod = Module.new
- include mod
- mod
- end
- end
-
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
- return unless generated_methods.instance_methods.empty?
- columns_hash.keys.each do |name|
- attribute_method_matchers.each do |method|
- method_name = "#{method.prefix}#{name}#{method.suffix}"
- unless instance_method_already_implemented?(method_name)
- generate_method = "define_method_#{method.prefix}attribute#{method.suffix}"
-
- if respond_to?(generate_method)
- send(generate_method, name)
- else
- generated_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
- end
- end
- end
- end
- end
-
- def undefine_attribute_methods
- generated_methods.module_eval do
- instance_methods.each { |m| undef_method(m) }
- end
+ super(columns_hash.keys)
end
# Checks whether the method is defined in the model or any of its subclasses
@@ -200,83 +22,30 @@ module ActiveRecord
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
@_defined_class_methods.include?(method_name)
end
-
- private
- # Default to *=, *? and *_before_type_cast
- def attribute_method_matchers
- @@attribute_method_matchers ||= []
- end
- end
-
- # 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
- # 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+ 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 instantiate master through Client#master.
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.instance_methods.empty?
+ if !self.class.attribute_methods_generated?
self.class.define_attribute_methods
+ method_name = method_id.to_s
guard_private_attribute_method!(method_name, args)
- if self.class.generated_methods.instance_methods.include?(method_name)
+ if self.class.generated_attribute_methods.instance_methods.include?(method_name)
return self.send(method_id, *args, &block)
end
end
-
- 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)
- end
super
end
- # 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>
- # which will all return +true+.
- alias :respond_to_without_attributes? :respond_to?
- def respond_to?(method, include_private_methods = false)
- method_name = method.to_s
- if super
- return true
- elsif !include_private_methods && super(method, true)
- # If we're here then we haven't found among non-private methods
- # but found among all methods. Which means that given method is private.
- return false
- elsif self.class.generated_methods.instance_methods.empty?
- self.class.define_attribute_methods
- if self.class.generated_methods.instance_methods.include?(method_name)
- return true
- end
- elsif match_attribute_method?(method_name)
- return true
- end
+ def respond_to?(*args)
+ self.class.define_attribute_methods
super
end
- private
- # 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
-
- def missing_attribute(attr_name, stack)
- raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
+ protected
+ def attribute_method?(attr_name)
+ attr_name == 'id' || attributes.include?(attr_name)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 90acb769a9..0b7d6d9094 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -51,7 +51,7 @@ module ActiveRecord
private
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
- generated_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
+ generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
end
# Define an attribute reader method. Cope with nil column.
@@ -66,7 +66,7 @@ module ActiveRecord
if cache_attribute?(attr_name)
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
end
- generated_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__)
+ generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__)
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index b9cfe59971..a8e3e28a7a 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -25,7 +25,7 @@ module ActiveRecord
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
end
EOV
- generated_methods.module_eval(method_body, __FILE__, __LINE__)
+ generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
else
super
end
@@ -44,7 +44,7 @@ module ActiveRecord
write_attribute(:#{attr_name}, time)
end
EOV
- generated_methods.module_eval(method_body, __FILE__, __LINE__)
+ generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
else
super
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 79118855cf..e31acac050 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -10,7 +10,7 @@ module ActiveRecord
module ClassMethods
protected
def define_method_attribute=(attr_name)
- generated_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
+ generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index ce93ea8eee..e358564ead 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -148,11 +148,6 @@ module ActiveRecord #:nodoc:
class DangerousAttributeError < ActiveRecordError
end
- # Raised when you've tried to access a column which wasn't loaded by your finder.
- # Typically this is because <tt>:select</tt> has been specified.
- class MissingAttributeError < NoMethodError
- end
-
# Raised when unknown attributes are supplied via mass assignment.
class UnknownAttributeError < NoMethodError
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index a5f4a67200..ab8768ea3e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -16,53 +16,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
- def test_match_attribute_method_query_returns_default_match_data
- topic = @target.new(:title => 'Budget')
- assert_not_nil match = topic.match_attribute_method?('title=')
- assert_equal '', match.prefix
- assert_equal 'title', match.base
- assert_equal '=', match.suffix
- end
-
- def test_match_attribute_method_query_returns_match_data_for_prefixes
- topic = @target.new(:title => 'Budget')
- %w(default_ title_).each do |prefix|
- @target.class_eval "def #{prefix}attribute(*args) args end"
- @target.attribute_method_prefix prefix
-
- assert_not_nil match = topic.match_attribute_method?("#{prefix}title")
- assert_equal prefix, match.prefix
- assert_equal 'title', match.base
- assert_equal '', match.suffix
- end
- end
-
- def test_match_attribute_method_query_returns_match_data_for_suffixes
- topic = @target.new(:title => 'Budget')
- %w(_default _title_default it! _candidate= _maybe?).each do |suffix|
- @target.class_eval "def attribute#{suffix}(*args) args end"
- @target.attribute_method_suffix suffix
-
- assert_not_nil match = topic.match_attribute_method?("title#{suffix}")
- assert_equal '', match.prefix
- assert_equal 'title', match.base
- assert_equal suffix, match.suffix
- end
- end
-
- def test_match_attribute_method_query_returns_match_data_for_affixes
- topic = @target.new(:title => 'Budget')
- [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
- @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
- @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
-
- assert_not_nil match = topic.match_attribute_method?("#{prefix}title#{suffix}")
- assert_equal prefix, match.prefix
- assert_equal 'title', match.base
- assert_equal suffix, match.suffix
- end
- end
-
def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing
topic = @target.new(:title => 'Budget')
assert topic.respond_to?('title')
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 55ef0d45eb..893fc34c36 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -251,7 +251,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_only_some_columns
topic = Topic.find(1, :select => "author_name")
- assert_raise(ActiveRecord::MissingAttributeError) {topic.title}
+ assert_raise(ActiveModel::MissingAttributeError) {topic.title}
assert_equal "David", topic.author_name
assert !topic.attribute_present?("title")
#assert !topic.respond_to?("title")