1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

r4854@ks: jeremy | 2006-07-30 00:59:18 -0700

Attribute methods
 r4877@ks:  jeremy | 2006-07-30 20:23:53 -0700
 Factor the attribute#{suffix} methods out of method_missing for easier extension.
 r4878@ks:  jeremy | 2006-07-30 20:42:23 -0700
 More specific method naming, declare many attribute method suffixes, set up default suffixes at module include rather than lazily.


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4632 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jeremy Kemper 2006-07-31 03:43:03 +00:00
parent efff453148
commit 2b3cc2478f
5 changed files with 114 additions and 9 deletions

View file

@ -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]

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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