aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG2
-rwxr-xr-xactiverecord/lib/active_record.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb75
-rwxr-xr-xactiverecord/lib/active_record/base.rb14
-rwxr-xr-xactiverecord/test/attribute_methods_test.rb30
5 files changed, 114 insertions, 9 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 10387d04ef..7900524a74 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Factor the attribute#{suffix} methods out of method_missing for easier extension. [Jeremy Kemper]
+
* Patch sql injection vulnerability when using integer or float columns. [Jamis Buck]
* Allow #count through a has_many association to accept :include. [Dan Peterson]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 0fb36dbfb3..34ab69f872 100755
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -52,6 +52,7 @@ require 'active_record/migration'
require 'active_record/schema'
require 'active_record/calculations'
require 'active_record/xml_serialization'
+require 'active_record/attribute_methods'
ActiveRecord::Base.class_eval do
include ActiveRecord::Validations
@@ -69,6 +70,7 @@ ActiveRecord::Base.class_eval do
include ActiveRecord::Acts::NestedSet
include ActiveRecord::Calculations
include ActiveRecord::XmlSerialization
+ include ActiveRecord::AttributeMethods
end
unless defined?(RAILS_CONNECTION_ADAPTERS)
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
new file mode 100644
index 0000000000..adc6eb6559
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -0,0 +1,75 @@
+module ActiveRecord
+ module AttributeMethods #:nodoc:
+ DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
+
+ def self.included(base)
+ base.extend ClassMethods
+ base.attribute_method_suffix *DEFAULT_SUFFIXES
+ end
+
+ # Declare and check for suffixed attribute methods.
+ module ClassMethods
+ # Declare a method available for all attributes with the given suffix.
+ # Uses method_missing and respond_to? to rewrite the method
+ # #{attr}#{suffix}(*args, &block)
+ # to
+ # attribute#{suffix}(#{attr}, *args, &block)
+ #
+ # An attribute#{suffix} instance method must exist and accept at least
+ # the attr argument.
+ #
+ # For example:
+ # 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
+
+ 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
+
+ # Default to =, ?, _before_type_cast
+ def attribute_method_suffixes
+ @@attribute_method_suffixes ||= []
+ end
+ end
+
+ private
+ # Handle *? for method_missing.
+ def attribute?(attribute_name)
+ query_attribute(attribute_name)
+ end
+
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ write_attribute(attribute_name, value)
+ end
+
+ # Handle *_before_type_cast for method_missing.
+ def attribute_before_type_cast(attribute_name)
+ read_attribute_before_type_cast(attribute_name)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b852a0354f..820e6a80ce 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1671,13 +1671,13 @@ module ActiveRecord #:nodoc:
# person.respond_to?("name?") which will all return true.
def respond_to?(method, include_priv = false)
if @attributes.nil?
- return super
+ return super
elsif attr_name = self.class.column_methods_hash[method.to_sym]
return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key
return false if self.class.read_methods.include?(attr_name)
elsif @attributes.include?(method_name = method.to_s)
return true
- elsif md = /(=|\?|_before_type_cast)$/.match(method_name)
+ elsif md = self.class.match_attribute_method?(method.to_s)
return true if @attributes.include?(md.pre_match)
end
# super must be called at the end of the method, because the inherited respond_to?
@@ -1750,6 +1750,7 @@ module ActiveRecord #:nodoc:
end
end
+
# Allows access to the object attributes, which are held in the @attributes hash, as were
# they 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
@@ -1767,15 +1768,10 @@ module ActiveRecord #:nodoc:
md ? query_attribute(method_name) : read_attribute(method_name)
elsif self.class.primary_key.to_s == method_name
id
- elsif md = /(=|_before_type_cast)$/.match(method_name)
+ elsif md = self.class.match_attribute_method?(method_name)
attribute_name, method_type = md.pre_match, md.to_s
if @attributes.include?(attribute_name)
- case method_type
- when '='
- write_attribute(attribute_name, args.first)
- when '_before_type_cast'
- read_attribute_before_type_cast(attribute_name)
- end
+ __send__("attribute#{method_type}", attribute_name, *args, &block)
else
super
end
diff --git a/activerecord/test/attribute_methods_test.rb b/activerecord/test/attribute_methods_test.rb
new file mode 100755
index 0000000000..8dcca2fc4a
--- /dev/null
+++ b/activerecord/test/attribute_methods_test.rb
@@ -0,0 +1,30 @@
+require 'abstract_unit'
+
+class AttributeMethodsTest < Test::Unit::TestCase
+ def setup
+ @target = Class.new(ActiveRecord::Base)
+ @target.table_name = 'topics'
+ end
+
+ def test_match_attribute_method_query_returns_match_data
+ assert_not_nil md = @target.match_attribute_method?('title=')
+ assert_equal 'title', md.pre_match
+ assert_equal ['='], md.captures
+ end
+
+ def test_declared_attribute_method_affects_respond_to_and_method_missing
+ topic = @target.new(:title => 'Budget')
+ assert topic.respond_to?('title')
+ assert_equal 'Budget', topic.title
+ assert !topic.respond_to?('title_hello_world')
+ assert_raise(NoMethodError) { topic.title_hello_world }
+
+ @target.class_eval "def attribute_hello_world(*args) args end"
+ @target.attribute_method_suffix '_hello_world'
+
+ assert topic.respond_to?('title_hello_world')
+ assert_equal ['title'], topic.title_hello_world
+ assert_equal ['title', 'a'], topic.title_hello_world('a')
+ assert_equal ['title', 1, 2, 3], topic.title_hello_world(1, 2, 3)
+ end
+end