Skip to content

Instantly share code, notes, and snippets.

@AquaGeek
Created May 13, 2011 03:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AquaGeek/969910 to your computer and use it in GitHub Desktop.
Save AquaGeek/969910 to your computer and use it in GitHub Desktop.
Rails Lighthouse ticket #950
From 64dc359547f205eccf78c63540bcb0f8db91fc85 Mon Sep 17 00:00:00 2001
From: Eloy Duran <eloy.de.enige@gmail.com>
Date: Fri, 5 Dec 2008 13:22:23 +0100
Subject: [PATCH] Updated for current HEAD.
---
activerecord/lib/active_record.rb | 1 +
activerecord/lib/active_record/aggregations.rb | 2 +
.../lib/active_record/attribute_decorator.rb | 187 +++++++++++++++++
activerecord/lib/active_record/base.rb | 2 +
activerecord/lib/active_record/reflection.rb | 19 ++
activerecord/test/cases/aggregations_test.rb | 16 +-
.../test/cases/attribute_decorator_test.rb | 216 ++++++++++++++++++++
activerecord/test/cases/base_test.rb | 20 ++
activerecord/test/cases/reflection_test.rb | 25 +++
activerecord/test/models/artist.rb | 81 ++++++++
activerecord/test/models/customer.rb | 12 +-
activerecord/test/models/developer.rb | 14 +-
activerecord/test/schema/schema.rb | 8 +
13 files changed, 589 insertions(+), 14 deletions(-)
create mode 100644 activerecord/lib/active_record/attribute_decorator.rb
create mode 100644 activerecord/test/cases/attribute_decorator_test.rb
create mode 100644 activerecord/test/models/artist.rb
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 348e5b9..5aa3cac 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -41,6 +41,7 @@ module ActiveRecord
autoload :ConnectionNotEstablished, 'active_record/base'
autoload :Aggregations, 'active_record/aggregations'
+ autoload :AttributeDecorator, 'active_record/attribute_decorator'
autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods'
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 1eefebb..04a67ae 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -192,6 +192,8 @@ module ActiveRecord
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
#
def composed_of(part_id, options = {}, &block)
+ ActiveSupport::Deprecation.warn("ActiveRecord::Aggregations::composed_of has been deprecated. Please use ActiveRecord::AttributeDecorator::attribute_decorator.")
+
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
name = part_id.id2name
diff --git a/activerecord/lib/active_record/attribute_decorator.rb b/activerecord/lib/active_record/attribute_decorator.rb
new file mode 100644
index 0000000..66ee4d8
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_decorator.rb
@@ -0,0 +1,187 @@
+module ActiveRecord
+ module AttributeDecorator #:nodoc:
+ def self.included(klass)
+ klass.extend ClassMethods
+ end
+
+ def clear_attribute_decorator_cache
+ self.class.reflect_on_all_attribute_decorators.each do |attribute_decorator|
+ instance_variable_set "@#{attribute_decorator.name}_before_type_cast", nil
+ end unless new_record?
+ end
+
+ module ClassMethods
+ # Adds reader and writer methods for decorating one or more attributes:
+ # <tt>attribute_decorator :date_of_birth</tt> adds <tt>date_of_birth</tt> and <tt>date_of_birth=(new_date_of_birth)</tt> methods.
+ #
+ # Options are:
+ # * <tt>:class</tt> - specify the decorator class.
+ # * <tt>:class_name</tt> - specify the class name of the decorator class,
+ # this should be used if, at the time of loading the model class, the decorator class is not yet available.
+ # * <tt>:decorates</tt> - specifies the attributes that should be wrapped by the decorator class.
+ # Takes an array of attributes or a single attribute. If none is specified the same name as the name of the attribute_decorator is assumed.
+ #
+ # The decorator class should implement a class method called <tt>parse</tt>, which takes 1 argument.
+ # In that method your decorator class is responsible for returning an instance of itself with the attribute(s) parsed and assigned.
+ #
+ # Your decorator class’s initialize method should take as it’s arguments the attributes that were specified
+ # to the <tt>:decorates</tt> option and in the same order as they were specified.
+ # You should also implement a <tt>to_a</tt> method which should return the parsed values as an array,
+ # again in the same order as specified with the <tt>:decorates</tt> option.
+ #
+ # If you wish to use <tt>validates_decorator</tt>, your decorator class should also implement a <tt>valid?</tt> instance method,
+ # which is responsible for checking the validity of the value(s). See <tt>validates_decorator</tt> for more info.
+ #
+ # class CompositeDate
+ # attr_accessor :day, :month, :year
+ #
+ # # Gets the value from Artist#date_of_birth= and will return a CompositeDate instance with the :day, :month and :year attributes set.
+ # def self.parse(value)
+ # day, month, year = value.scan(/(\d+)-(\d+)-(\d{4})/).flatten.map { |x| x.to_i }
+ # new(day, month, year)
+ # end
+ #
+ # # Notice that the order of arguments is the same as specified with the :decorates option.
+ # def initialize(day, month, year)
+ # @day, @month, @year = day, month, year
+ # end
+ #
+ # # Here we return the parsed values in the same order as specified with the :decorates option.
+ # def to_a
+ # [@day, @month, @year]
+ # end
+ #
+ # # Here we return a string representation of the value, this will for instance be used by the form helpers.
+ # def to_s
+ # "#{@day}-#{@month}-#{@year}"
+ # end
+ #
+ # # Returns wether or not this CompositeDate instance is valid.
+ # def valid?
+ # @day != 0 && @month != 0 && @year != 0
+ # end
+ # end
+ #
+ # class Artist < ActiveRecord::Base
+ # attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
+ # validates_decorator :date_of_birth, :message => 'is not a valid date'
+ # end
+ #
+ # Option examples:
+ # attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
+ # attribute_decorator :gps_location, :class_name => 'GPSCoordinator', :decorates => :location
+ # attribute_decorator :balance, :class_name => 'Money'
+ # attribute_decorator :english_date_of_birth, :class => (Class.new(CompositeDate) do
+ # # This is a anonymous subclass of CompositeDate that supports the date in English order
+ # def to_s
+ # "#{@month}/#{@day}/#{@year}"
+ # end
+ #
+ # def self.parse(value)
+ # month, day, year = value.scan(/(\d+)\/(\d+)\/(\d{4})/).flatten.map { |x| x.to_i }
+ # new(day, month, year)
+ # end
+ # end)
+ def attribute_decorator(attr, options)
+ options.assert_valid_keys(:class, :class_name, :decorates)
+
+ if options[:decorates].nil?
+ options[:decorates] = [attr]
+ elsif !options[:decorates].is_a?(Array)
+ options[:decorates] = [options[:decorates]]
+ end
+
+ define_attribute_decorator_reader(attr, options)
+ define_attribute_decorator_writer(attr, options)
+
+ create_reflection(:attribute_decorator, attr, options, self)
+ end
+
+ # Validates wether the decorated attribute is valid by sending the decorator instance the <tt>valid?</tt> message.
+ #
+ # class CompositeDate
+ # attr_accessor :day, :month, :year
+ #
+ # def self.parse(value)
+ # day, month, year = value.scan(/(\d\d)-(\d\d)-(\d{4})/).flatten.map { |x| x.to_i }
+ # new(day, month, year)
+ # end
+ #
+ # def initialize(day, month, year)
+ # @day, @month, @year = day, month, year
+ # end
+ #
+ # def to_a
+ # [@day, @month, @year]
+ # end
+ #
+ # def to_s
+ # "#{@day}-#{@month}-#{@year}"
+ # end
+ #
+ # # Returns wether or not this CompositeDate instance is valid.
+ # def valid?
+ # @day != 0 && @month != 0 && @year != 0
+ # end
+ # end
+ #
+ # class Artist < ActiveRecord::Base
+ # attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
+ # validates_decorator :date_of_birth, :message => 'is not a valid date'
+ # end
+ #
+ # artist = Artist.new
+ # artist.date_of_birth = '31-12-1999'
+ # artist.valid? # => true
+ # artist.date_of_birth = 'foo-bar-baz'
+ # artist.valid? # => false
+ # artist.errors.on(:date_of_birth) # => "is not a valid date"
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid").
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
+ # method, proc or string should return or evaluate to a true or false value.
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
+ # method, proc or string should return or evaluate to a true or false value.
+ def validates_decorator(*attrs)
+ configuration = { :message => I18n.translate('active_record.error_messages')[:invalid], :on => :save }
+ configuration.update attrs.extract_options!
+
+ invalid_keys = configuration.keys.select { |key| key == :allow_nil || key == :allow_blank }
+ raise ArgumentError, "Unknown key(s): #{ invalid_keys.join(', ') }" unless invalid_keys.empty?
+
+ validates_each(attrs, configuration) do |record, attr, value|
+ record.errors.add(attr, configuration[:message]) unless record.send(attr).valid?
+ end
+ end
+
+ private
+
+ def define_attribute_decorator_reader(attr, options)
+ class_eval do
+ define_method(attr) do
+ (options[:class] ||= options[:class_name].constantize).new(*options[:decorates].map { |attribute| read_attribute(attribute) })
+ end
+ end
+ end
+
+ def define_attribute_decorator_writer(attr, options)
+ class_eval do
+ define_method("#{attr}_before_type_cast") do
+ instance_variable_get("@#{attr}_before_type_cast") || send(attr).to_s
+ end
+
+ define_method("#{attr}=") do |value|
+ instance_variable_set("@#{attr}_before_type_cast", value)
+ values = (options[:class] ||= options[:class_name].constantize).parse(value).to_a
+ options[:decorates].each_with_index { |attribute, index| write_attribute attribute, values[index] }
+ value
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index a23518b..43b46f4 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -2578,6 +2578,7 @@ module ActiveRecord #:nodoc:
# an exclusive row lock.
def reload(options = nil)
clear_aggregation_cache
+ clear_attribute_decorator_cache
clear_association_cache
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
@attributes_cache = {}
@@ -3014,6 +3015,7 @@ module ActiveRecord #:nodoc:
extend QueryCache
include Validations
include Locking::Optimistic, Locking::Pessimistic
+ include AttributeDecorator
include AttributeMethods
include Dirty
include Callbacks, Observing, Timestamp
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index dbff4f2..3f3d3a1 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -17,6 +17,8 @@ module ActiveRecord
reflection = klass.new(macro, name, options, active_record)
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
+ when :attribute_decorator
+ reflection = AttributeDecoratorReflection.new(macro, name, options, active_record)
end
write_inheritable_hash :reflections, name => reflection
reflection
@@ -45,6 +47,19 @@ module ActiveRecord
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
end
+ # Returns an array of DecoratorReflection objects for all the attribute decorators in the class.
+ def reflect_on_all_attribute_decorators
+ reflections.values.select { |reflection| reflection.is_a?(AttributeDecoratorReflection) }
+ end
+
+ # Returns the DecoratorReflection object for the named <tt>attribute decorator</tt> (use the symbol). Example:
+ #
+ # Account.reflect_on_decorator(:balance) # returns the balance DecoratorReflection
+ #
+ def reflect_on_attribute_decorator(attribute_decorator)
+ reflections[attribute_decorator].is_a?(AttributeDecoratorReflection) ? reflections[attribute_decorator] : nil
+ end
+
# Returns an array of AssociationReflection objects for all the associations in the class. If you only want to reflect on a
# certain association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, <tt>:belongs_to</tt>) for that as the first parameter.
# Example:
@@ -133,6 +148,10 @@ module ActiveRecord
class AggregateReflection < MacroReflection #:nodoc:
end
+ # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
+ class AttributeDecoratorReflection < MacroReflection #:nodoc:
+ end
+
# Holds all the meta-data about an association as it was specified in the Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
# Returns the target association's class:
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index 4e0e1c7..62e27b3 100644
--- a/activerecord/test/cases/aggregations_test.rb
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -1,5 +1,7 @@
require "cases/helper"
-require 'models/customer'
+ActiveSupport::Deprecation.silence do
+ require 'models/customer'
+end
class AggregationsTest < ActiveRecord::TestCase
fixtures :customers
@@ -152,12 +154,14 @@ class OverridingAggregationsTest < ActiveRecord::TestCase
class Name; end
class DifferentName; end
- class Person < ActiveRecord::Base
- composed_of :composed_of, :mapping => %w(person_first_name first_name)
- end
+ ActiveSupport::Deprecation.silence do
+ class Person < ActiveRecord::Base
+ composed_of :composed_of, :mapping => %w(person_first_name first_name)
+ end
- class DifferentPerson < Person
- composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name)
+ class DifferentPerson < Person
+ composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name)
+ end
end
def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited
diff --git a/activerecord/test/cases/attribute_decorator_test.rb b/activerecord/test/cases/attribute_decorator_test.rb
new file mode 100644
index 0000000..1527707
--- /dev/null
+++ b/activerecord/test/cases/attribute_decorator_test.rb
@@ -0,0 +1,216 @@
+require "cases/helper"
+require 'models/artist'
+
+class AttributeDecoratorClassMethodTest < ActiveRecord::TestCase
+ def test_should_take_a_name_for_the_decorator_and_define_a_reader_and_writer_method_for_it
+ %w{ date_of_birth date_of_birth= }.each { |method| assert Artist.instance_methods.include?(method) }
+ end
+
+ def test_should_not_take_any_options_other_than_class_and_class_name_and_decorates
+ assert_raise(ArgumentError) do
+ Artist.class_eval do
+ attribute_decorator :foo, :some_other_option => true
+ end
+ end
+ end
+end
+
+class AttributeDecoratorInGeneralTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:day => 31, :month => 12, :year => 1999)
+ end
+
+ def teardown
+ Artist.class_eval do
+ attribute_decorator :date_of_birth, :class_name => 'Decorators::CompositeDate', :decorates => [:day, :month, :year]
+ end
+ end
+
+ uses_mocha('should_only_use_constantize_once_and_cache_the_result') do
+ def test_should_only_use_constantize_once_and_cache_the_result
+ klass_name_string = 'CompositeDate'
+
+ Artist.class_eval do
+ attribute_decorator :date_of_birth, :class_name => klass_name_string, :decorates => [:day, :month, :year]
+ end
+
+ klass_name_string.expects(:constantize).times(1).returns(Decorators::CompositeDate)
+ 2.times { @artist.date_of_birth }
+ end
+ end
+
+ def test_should_work_with_a_real_pointer_to_a_wrapper_class_instead_of_a_string
+ Artist.class_eval do
+ attribute_decorator :date_of_birth, :class => Decorators::CompositeDate, :decorates => [:day, :month, :year]
+ end
+
+ assert_equal "31-12-1999", @artist.date_of_birth.to_s
+ end
+
+ uses_mocha('should_also_work_with_an_anonymous_wrapper_class') do
+ def test_should_also_work_with_an_anonymous_wrapper_class
+ Artist.class_eval do
+ attribute_decorator :date_of_birth, :decorates => [:day, :month, :year], :class => (Class.new(Decorators::CompositeDate) do
+ # Reversed implementation of the super class.
+ def to_s
+ "#{@year}-#{@month}-#{@day}"
+ end
+ end)
+ end
+
+ 2.times { assert_equal "1999-12-31", @artist.date_of_birth.to_s }
+ end
+ end
+
+ def test_should_reset_the_before_type_cast_values_on_reload
+ @artist.date_of_birth = '01-01-1111'
+ Artist.find(@artist.id).update_attribute(:day, 13)
+ @artist.reload
+
+ assert_equal "13-12-1999", @artist.date_of_birth_before_type_cast
+ end
+end
+
+class AttributeDecoratorForMultipleAttributesTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:day => 31, :month => 12, :year => 1999)
+ @decorator = @artist.date_of_birth
+ end
+
+ def test_should_return_an_instance_of_the_decorator_class_specified_by_the_class_name_option
+ assert_instance_of Decorators::CompositeDate, @artist.date_of_birth
+ end
+
+ def test_should_have_assigned_values_to_decorate_to_the_decorator_instance
+ assert_equal 31, @decorator.day
+ assert_equal 12, @decorator.month
+ assert_equal 1999, @decorator.year
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
+ @artist.date_of_birth = '01-02-2000'
+ assert_equal '01-02-2000', @artist.date_of_birth_before_type_cast
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_just_read_from_the_database
+ date_of_birth_as_string = @artist.date_of_birth.to_s
+ @artist.reload
+ assert_equal date_of_birth_as_string, @artist.date_of_birth_before_type_cast
+ end
+
+ def test_should_parse_the_value_assigned_through_the_setter_method_and_assign_them_to_the_model_instance
+ @artist.date_of_birth = '01-02-2000'
+ assert_equal 1, @artist.day
+ assert_equal 2, @artist.month
+ assert_equal 2000, @artist.year
+ end
+end
+
+class AttributeDecoratorForOneAttributeTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:location => 'amsterdam')
+ end
+
+ def test_should_return_an_instance_of_the_decorator_class_specified_by_the_class_name_option
+ assert_instance_of Decorators::GPSCoordinator, @artist.gps_location
+ end
+
+ def test_should_have_assigned_the_value_to_decorate_to_the_decorator_instance
+ assert_equal 'amsterdam', @artist.gps_location.location
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
+ @artist.gps_location = 'rotterdam'
+ assert_equal 'rotterdam', @artist.gps_location_before_type_cast
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_just_read_from_the_database
+ gps_location_as_string = @artist.gps_location.to_s
+ @artist.reload
+ assert_equal gps_location_as_string, @artist.gps_location_before_type_cast
+ end
+
+ def test_should_parse_the_value_assigned_through_the_setter_method_and_assign_it_to_the_model_instance
+ @artist.gps_location = 'amsterdam'
+ assert_equal '+1, +1', @artist.location
+
+ @artist.gps_location = 'rotterdam'
+ assert_equal '-1, -1', @artist.location
+ end
+end
+
+class AttributeDecoratorForAnAlreadyExistingAttributeTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:start_year => 1999)
+ @decorator = @artist.start_year
+ end
+
+ def test_should_return_an_instance_of_the_decorator_class_specified_by_the_class_option
+ assert_instance_of Decorators::GPSCoordinator, @artist.gps_location
+ end
+
+ def test_should_have_assigned_the_value_to_decorate_to_the_decorator_instance
+ assert_equal 1999, @decorator.start_year
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
+ @artist.start_year = '40 bc'
+ assert_equal '40 bc', @artist.start_year_before_type_cast
+ end
+
+ def test_should_parse_and_write_the_value_assigned_through_the_setter_method_and_assign_it_to_the_model_instance
+ @artist.start_year = '40 bc'
+ assert_equal -41, @artist.read_attribute(:start_year)
+ end
+end
+
+class AttributeDecoratorValidatorTest < ActiveRecord::TestCase
+ def teardown
+ Artist.instance_variable_set(:@validate_callbacks, [])
+ Artist.instance_variable_set(:@validate_on_update_callbacks, [])
+ end
+
+ def test_should_delegate_validation_to_the_decorator
+ Artist.class_eval do
+ validates_decorator :date_of_birth, :start_year
+ end
+
+ artist = Artist.create(:start_year => 1999)
+
+ artist.start_year = 40
+ assert artist.valid?
+
+ artist.start_year = 'abcde'
+ assert !artist.valid?
+ assert_equal "is invalid", artist.errors.on(:start_year)
+ end
+
+ def test_should_take_a_options_hash_for_more_detailed_configuration
+ Artist.class_eval do
+ validates_decorator :start_year, :message => 'is not a valid date', :on => :update
+ end
+
+ artist = Artist.new(:start_year => 'abcde')
+ assert artist.valid?
+
+ artist.save!
+ assert !artist.valid?
+ assert_equal 'is not a valid date', artist.errors.on(:start_year)
+ end
+
+ def test_should_not_take_the_allow_nil_option
+ assert_raise(ArgumentError) do
+ Artist.class_eval do
+ validates_decorator :start_year, :allow_nil => true
+ end
+ end
+ end
+
+ def test_should_not_take_the_allow_blank_option
+ assert_raise(ArgumentError) do
+ Artist.class_eval do
+ validates_decorator :start_year, :allow_blank => true
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 5f54931..ebb7e72 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1245,6 +1245,26 @@ class BasicsTest < ActiveRecord::TestCase
assert clone.id != dev.id
end
+ def test_clone_with_attribute_decorator_of_same_name_as_attribute
+ dev = DeveloperWithAttributeDecorator.find(1)
+ assert_kind_of DeveloperSalaryDecorator, dev.salary
+
+ clone = nil
+ assert_nothing_raised { clone = dev.clone }
+ assert_kind_of DeveloperSalaryDecorator, clone.salary
+ assert_equal dev.salary.amount, clone.salary.amount
+ assert clone.new_record?
+
+ # test if the attributes have been cloned
+ original_amount = clone.salary.amount
+ dev.salary.amount = 1
+ assert_equal original_amount, clone.salary.amount
+
+ assert clone.save
+ assert !clone.new_record?
+ assert clone.id != dev.id
+ end
+
def test_clone_preserves_subtype
clone = nil
assert_nothing_raised { clone = Company.find(3).clone }
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index e0ed3e5..6a4836e 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/topic'
+require 'models/artist'
require 'models/customer'
require 'models/company'
require 'models/company_in_module'
@@ -91,6 +92,30 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal Money, Customer.reflect_on_aggregation(:balance).klass
end
+ def test_attribute_decorator_reflection
+ reflection_for_date_of_birth = ActiveRecord::Reflection::AttributeDecoratorReflection.new(
+ :attribute_decorator, :date_of_birth, {
+ :class_name => 'Decorators::CompositeDate',
+ :decorates => [:day, :month, :year]
+ }, Artist
+ )
+
+ reflection_for_gps_location = ActiveRecord::Reflection::AttributeDecoratorReflection.new(
+ :attribute_decorator, :gps_location, { :class_name => 'Decorators::GPSCoordinator', :decorates => :location }, Artist
+ )
+
+ reflection_for_start_year = ActiveRecord::Reflection::AttributeDecoratorReflection.new(
+ :attribute_decorator, :start_year, { :class_name => 'Decorators::Year' }, Artist
+ )
+
+ [reflection_for_date_of_birth, reflection_for_gps_location, reflection_for_start_year].each do |reflection|
+ assert Artist.reflect_on_all_attribute_decorators.include?(reflection)
+ end
+
+ assert_equal reflection_for_date_of_birth, Artist.reflect_on_attribute_decorator(:date_of_birth)
+ assert_equal Decorators::CompositeDate, Artist.reflect_on_attribute_decorator(:date_of_birth).klass
+ end
+
def test_has_many_reflection
reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm)
diff --git a/activerecord/test/models/artist.rb b/activerecord/test/models/artist.rb
new file mode 100644
index 0000000..f2e4d1b
--- /dev/null
+++ b/activerecord/test/models/artist.rb
@@ -0,0 +1,81 @@
+class Artist < ActiveRecord::Base
+ # Defines a non existing attribute decorating multiple existing attributes
+ attribute_decorator :date_of_birth, :class_name => 'Decorators::CompositeDate', :decorates => [:day, :month, :year]
+
+ # Defines a decorates for one attribute.
+ attribute_decorator :gps_location, :class_name => 'Decorators::GPSCoordinator', :decorates => :location
+
+ # Defines a decorator for an existing attribute.
+ attribute_decorator :start_year, :class_name => 'Decorators::Year'
+
+ # These validations are defined inline in the test cases. See attribute_decorator_test.rb.
+ #
+ # validates_decorator :date_of_birth, :start_year
+ # validates_decorator :start_year, :message => 'is not a valid date', :on => :update
+end
+
+module Decorators
+ class CompositeDate
+ attr_reader :day, :month, :year
+
+ def self.parse(value)
+ new *value.scan(/(\d\d)-(\d\d)-(\d{4})/).flatten.map { |x| x.to_i }
+ end
+
+ def initialize(day, month, year)
+ @day, @month, @year = day, month, year
+ end
+
+ def valid?
+ true
+ end
+
+ def to_a
+ [@day, @month, @year]
+ end
+
+ def to_s
+ "#{@day}-#{@month}-#{@year}"
+ end
+ end
+
+ class GPSCoordinator
+ attr_reader :location
+
+ def self.parse(value)
+ new(value == 'amsterdam' ? '+1, +1' : '-1, -1')
+ end
+
+ def initialize(location)
+ @location = location
+ end
+
+ def to_a
+ [@location]
+ end
+
+ def to_s
+ @location
+ end
+ end
+
+ class Year
+ attr_reader :start_year
+
+ def self.parse(value)
+ new(value == '40 bc' ? -41 : value.to_i)
+ end
+
+ def initialize(start_year)
+ @start_year = start_year
+ end
+
+ def valid?
+ @start_year != 0
+ end
+
+ def to_a
+ [@start_year]
+ end
+ end
+end
\ No newline at end of file
diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb
index e258ccd..ba28793 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -1,8 +1,10 @@
-class Customer < ActiveRecord::Base
- composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
- composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
- composed_of :gps_location, :allow_nil => true
- composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse
+ActiveSupport::Deprecation.silence do
+ class Customer < ActiveRecord::Base
+ composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
+ composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
+ composed_of :gps_location, :allow_nil => true
+ composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse
+ end
end
class Address
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 92039a4..14357bc 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -62,10 +62,18 @@ class AuditLog < ActiveRecord::Base
belongs_to :unvalidated_developer, :class_name => 'Developer'
end
-DeveloperSalary = Struct.new(:amount)
-class DeveloperWithAggregate < ActiveRecord::Base
+ActiveSupport::Deprecation.silence do
+ DeveloperSalary = Struct.new(:amount)
+ class DeveloperWithAggregate < ActiveRecord::Base
+ self.table_name = 'developers'
+ composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)]
+ end
+end
+
+DeveloperSalaryDecorator = Struct.new(:amount)
+class DeveloperWithAttributeDecorator < ActiveRecord::Base
self.table_name = 'developers'
- composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)]
+ attribute_decorator :salary, :class => DeveloperSalaryDecorator
end
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 6217e3b..a08ab25 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -26,6 +26,14 @@ ActiveRecord::Schema.define do
t.integer :credit_limit
end
+ create_table :artists, :force => true do |t|
+ t.integer :day
+ t.integer :month
+ t.integer :year
+ t.integer :start_year
+ t.string :location
+ end
+
create_table :audit_logs, :force => true do |t|
t.column :message, :string, :null=>false
t.column :developer_id, :integer, :null=>false
--
1.5.5.3
From 57273c7785805b943f38e362b8a54677bc00073b Mon Sep 17 00:00:00 2001
From: Eloy Duran <eloy.de.enige@gmail.com>
Date: Wed, 10 Dec 2008 21:04:05 +0100
Subject: [PATCH] Changed API as discussed on LH.
Fixed whitespace.
---
activerecord/lib/active_record.rb | 2 +-
.../lib/active_record/attribute_decorator.rb | 187 -----------------
activerecord/lib/active_record/attribute_view.rb | 189 +++++++++++++++++
activerecord/lib/active_record/base.rb | 4 +-
activerecord/lib/active_record/reflection.rb | 25 ++-
.../test/cases/attribute_decorator_test.rb | 216 --------------------
activerecord/test/cases/attribute_view_test.rb | 191 +++++++++++++++++
activerecord/test/cases/base_test.rb | 6 +-
activerecord/test/cases/reflection_test.rb | 24 +-
activerecord/test/models/artist.rb | 64 +++---
activerecord/test/models/developer.rb | 6 +-
11 files changed, 447 insertions(+), 467 deletions(-)
delete mode 100644 activerecord/lib/active_record/attribute_decorator.rb
create mode 100644 activerecord/lib/active_record/attribute_view.rb
delete mode 100644 activerecord/test/cases/attribute_decorator_test.rb
create mode 100644 activerecord/test/cases/attribute_view_test.rb
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 5aa3cac..225e18a 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -41,10 +41,10 @@ module ActiveRecord
autoload :ConnectionNotEstablished, 'active_record/base'
autoload :Aggregations, 'active_record/aggregations'
- autoload :AttributeDecorator, 'active_record/attribute_decorator'
autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods'
+ autoload :AttributeView, 'active_record/attribute_view'
autoload :Base, 'active_record/base'
autoload :Calculations, 'active_record/calculations'
autoload :Callbacks, 'active_record/callbacks'
diff --git a/activerecord/lib/active_record/attribute_decorator.rb b/activerecord/lib/active_record/attribute_decorator.rb
deleted file mode 100644
index 66ee4d8..0000000
--- a/activerecord/lib/active_record/attribute_decorator.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-module ActiveRecord
- module AttributeDecorator #:nodoc:
- def self.included(klass)
- klass.extend ClassMethods
- end
-
- def clear_attribute_decorator_cache
- self.class.reflect_on_all_attribute_decorators.each do |attribute_decorator|
- instance_variable_set "@#{attribute_decorator.name}_before_type_cast", nil
- end unless new_record?
- end
-
- module ClassMethods
- # Adds reader and writer methods for decorating one or more attributes:
- # <tt>attribute_decorator :date_of_birth</tt> adds <tt>date_of_birth</tt> and <tt>date_of_birth=(new_date_of_birth)</tt> methods.
- #
- # Options are:
- # * <tt>:class</tt> - specify the decorator class.
- # * <tt>:class_name</tt> - specify the class name of the decorator class,
- # this should be used if, at the time of loading the model class, the decorator class is not yet available.
- # * <tt>:decorates</tt> - specifies the attributes that should be wrapped by the decorator class.
- # Takes an array of attributes or a single attribute. If none is specified the same name as the name of the attribute_decorator is assumed.
- #
- # The decorator class should implement a class method called <tt>parse</tt>, which takes 1 argument.
- # In that method your decorator class is responsible for returning an instance of itself with the attribute(s) parsed and assigned.
- #
- # Your decorator class’s initialize method should take as it’s arguments the attributes that were specified
- # to the <tt>:decorates</tt> option and in the same order as they were specified.
- # You should also implement a <tt>to_a</tt> method which should return the parsed values as an array,
- # again in the same order as specified with the <tt>:decorates</tt> option.
- #
- # If you wish to use <tt>validates_decorator</tt>, your decorator class should also implement a <tt>valid?</tt> instance method,
- # which is responsible for checking the validity of the value(s). See <tt>validates_decorator</tt> for more info.
- #
- # class CompositeDate
- # attr_accessor :day, :month, :year
- #
- # # Gets the value from Artist#date_of_birth= and will return a CompositeDate instance with the :day, :month and :year attributes set.
- # def self.parse(value)
- # day, month, year = value.scan(/(\d+)-(\d+)-(\d{4})/).flatten.map { |x| x.to_i }
- # new(day, month, year)
- # end
- #
- # # Notice that the order of arguments is the same as specified with the :decorates option.
- # def initialize(day, month, year)
- # @day, @month, @year = day, month, year
- # end
- #
- # # Here we return the parsed values in the same order as specified with the :decorates option.
- # def to_a
- # [@day, @month, @year]
- # end
- #
- # # Here we return a string representation of the value, this will for instance be used by the form helpers.
- # def to_s
- # "#{@day}-#{@month}-#{@year}"
- # end
- #
- # # Returns wether or not this CompositeDate instance is valid.
- # def valid?
- # @day != 0 && @month != 0 && @year != 0
- # end
- # end
- #
- # class Artist < ActiveRecord::Base
- # attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
- # validates_decorator :date_of_birth, :message => 'is not a valid date'
- # end
- #
- # Option examples:
- # attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
- # attribute_decorator :gps_location, :class_name => 'GPSCoordinator', :decorates => :location
- # attribute_decorator :balance, :class_name => 'Money'
- # attribute_decorator :english_date_of_birth, :class => (Class.new(CompositeDate) do
- # # This is a anonymous subclass of CompositeDate that supports the date in English order
- # def to_s
- # "#{@month}/#{@day}/#{@year}"
- # end
- #
- # def self.parse(value)
- # month, day, year = value.scan(/(\d+)\/(\d+)\/(\d{4})/).flatten.map { |x| x.to_i }
- # new(day, month, year)
- # end
- # end)
- def attribute_decorator(attr, options)
- options.assert_valid_keys(:class, :class_name, :decorates)
-
- if options[:decorates].nil?
- options[:decorates] = [attr]
- elsif !options[:decorates].is_a?(Array)
- options[:decorates] = [options[:decorates]]
- end
-
- define_attribute_decorator_reader(attr, options)
- define_attribute_decorator_writer(attr, options)
-
- create_reflection(:attribute_decorator, attr, options, self)
- end
-
- # Validates wether the decorated attribute is valid by sending the decorator instance the <tt>valid?</tt> message.
- #
- # class CompositeDate
- # attr_accessor :day, :month, :year
- #
- # def self.parse(value)
- # day, month, year = value.scan(/(\d\d)-(\d\d)-(\d{4})/).flatten.map { |x| x.to_i }
- # new(day, month, year)
- # end
- #
- # def initialize(day, month, year)
- # @day, @month, @year = day, month, year
- # end
- #
- # def to_a
- # [@day, @month, @year]
- # end
- #
- # def to_s
- # "#{@day}-#{@month}-#{@year}"
- # end
- #
- # # Returns wether or not this CompositeDate instance is valid.
- # def valid?
- # @day != 0 && @month != 0 && @year != 0
- # end
- # end
- #
- # class Artist < ActiveRecord::Base
- # attribute_decorator :date_of_birth, :class => CompositeDate, :decorates => [:day, :month, :year]
- # validates_decorator :date_of_birth, :message => 'is not a valid date'
- # end
- #
- # artist = Artist.new
- # artist.date_of_birth = '31-12-1999'
- # artist.valid? # => true
- # artist.date_of_birth = 'foo-bar-baz'
- # artist.valid? # => false
- # artist.errors.on(:date_of_birth) # => "is not a valid date"
- #
- # Configuration options:
- # * <tt>:message</tt> - A custom error message (default is: "is invalid").
- # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
- # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
- # method, proc or string should return or evaluate to a true or false value.
- # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
- # method, proc or string should return or evaluate to a true or false value.
- def validates_decorator(*attrs)
- configuration = { :message => I18n.translate('active_record.error_messages')[:invalid], :on => :save }
- configuration.update attrs.extract_options!
-
- invalid_keys = configuration.keys.select { |key| key == :allow_nil || key == :allow_blank }
- raise ArgumentError, "Unknown key(s): #{ invalid_keys.join(', ') }" unless invalid_keys.empty?
-
- validates_each(attrs, configuration) do |record, attr, value|
- record.errors.add(attr, configuration[:message]) unless record.send(attr).valid?
- end
- end
-
- private
-
- def define_attribute_decorator_reader(attr, options)
- class_eval do
- define_method(attr) do
- (options[:class] ||= options[:class_name].constantize).new(*options[:decorates].map { |attribute| read_attribute(attribute) })
- end
- end
- end
-
- def define_attribute_decorator_writer(attr, options)
- class_eval do
- define_method("#{attr}_before_type_cast") do
- instance_variable_get("@#{attr}_before_type_cast") || send(attr).to_s
- end
-
- define_method("#{attr}=") do |value|
- instance_variable_set("@#{attr}_before_type_cast", value)
- values = (options[:class] ||= options[:class_name].constantize).parse(value).to_a
- options[:decorates].each_with_index { |attribute, index| write_attribute attribute, values[index] }
- value
- end
- end
- end
- end
- end
-end
\ No newline at end of file
diff --git a/activerecord/lib/active_record/attribute_view.rb b/activerecord/lib/active_record/attribute_view.rb
new file mode 100644
index 0000000..5e9968a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_view.rb
@@ -0,0 +1,189 @@
+module ActiveRecord
+ module AttributeView #:nodoc:
+ def self.included(klass)
+ klass.extend ClassMethods
+ end
+
+ private
+
+ def clear_attribute_view_cache
+ self.class.reflect_on_all_attribute_views.each do |attribute_view|
+ instance_variable_set "@#{attribute_view.name}_before_type_cast", nil
+ end unless new_record?
+ end
+
+ module ClassMethods
+ # Adds reader and writer methods for decorating one or more attributes:
+ # <tt>attribute_decorator :date_of_birth</tt> adds <tt>date_of_birth</tt> and <tt>date_of_birth=(new_date_of_birth)</tt> methods.
+ #
+ # Options are:
+ # * <tt>:class</tt> - specify the decorator class.
+ # * <tt>:class_name</tt> - specify the class name of the decorator class,
+ # this should be used if, at the time of loading the model class, the decorator class is not yet available.
+ # * <tt>:decorating</tt> - specifies the attributes that should be wrapped by the decorator class.
+ # Takes an array of attributes or a single attribute. If none is specified the same name as the name of the attribute_decorator is assumed.
+ #
+ # The decorator class should implement a class method called <tt>parse</tt>, which takes 1 argument.
+ # In that method your decorator class is responsible for returning an instance of itself with the attribute(s) parsed and assigned.
+ #
+ # Your decorator class’s initialize method should take as it’s arguments the attributes that were specified
+ # to the <tt>:decorating</tt> option and in the same order as they were specified.
+ # You should also implement a <tt>to_a</tt> method which should return the parsed values as an array,
+ # again in the same order as specified with the <tt>:decorating</tt> option.
+ #
+ # If you wish to use <tt>validates_decorator</tt>, your decorator class should also implement a <tt>valid?</tt> instance method,
+ # which is responsible for checking the validity of the value(s). See <tt>validates_decorator</tt> for more info.
+ #
+ # class CompositeDate
+ # attr_accessor :day, :month, :year
+ #
+ # # Gets the value from Artist#date_of_birth= and will return a CompositeDate instance with the :day, :month and :year attributes set.
+ # def self.parse(value)
+ # day, month, year = value.scan(/(\d+)-(\d+)-(\d{4})/).flatten.map { |x| x.to_i }
+ # new(day, month, year)
+ # end
+ #
+ # # Notice that the order of arguments is the same as specified with the :decorating option.
+ # def initialize(day, month, year)
+ # @day, @month, @year = day, month, year
+ # end
+ #
+ # # Here we return the parsed values in the same order as specified with the :decorating option.
+ # def to_a
+ # [@day, @month, @year]
+ # end
+ #
+ # # Here we return a string representation of the value, this will for instance be used by the form helpers.
+ # def to_s
+ # "#{@day}-#{@month}-#{@year}"
+ # end
+ #
+ # # Returns wether or not this CompositeDate instance is valid.
+ # def valid?
+ # @day != 0 && @month != 0 && @year != 0
+ # end
+ # end
+ #
+ # class Artist < ActiveRecord::Base
+ # attribute_decorator :date_of_birth, :class => CompositeDate, :decorating => [:day, :month, :year]
+ # validates_decorator :date_of_birth, :message => 'is not a valid date'
+ # end
+ #
+ # Option examples:
+ # attribute_decorator :date_of_birth, :class => CompositeDate, :decorating => [:day, :month, :year]
+ # attribute_decorator :gps_location, :class_name => 'GPSCoordinator', :decorating => :location
+ # attribute_decorator :balance, :class_name => 'Money'
+ # attribute_decorator :english_date_of_birth, :class => (Class.new(CompositeDate) do
+ # # This is a anonymous subclass of CompositeDate that supports the date in English order
+ # def to_s
+ # "#{@month}/#{@day}/#{@year}"
+ # end
+ #
+ # def self.parse(value)
+ # month, day, year = value.scan(/(\d+)\/(\d+)\/(\d{4})/).flatten.map { |x| x.to_i }
+ # new(day, month, year)
+ # end
+ # end)
+ def view(attr, options)
+ options.assert_valid_keys(:as, :decorating)
+
+ if options[:decorating].nil?
+ options[:decorating] = [attr]
+ elsif !options[:decorating].is_a?(Array)
+ options[:decorating] = [options[:decorating]]
+ end
+
+ define_attribute_view_reader(attr, options)
+ define_attribute_view_writer(attr, options)
+
+ create_reflection(:view, attr, options, self)
+ end
+
+ # Validates wether the decorated attribute is valid by sending the decorator instance the <tt>valid?</tt> message.
+ #
+ # class CompositeDate
+ # attr_accessor :day, :month, :year
+ #
+ # def self.parse(value)
+ # day, month, year = value.scan(/(\d\d)-(\d\d)-(\d{4})/).flatten.map { |x| x.to_i }
+ # new(day, month, year)
+ # end
+ #
+ # def initialize(day, month, year)
+ # @day, @month, @year = day, month, year
+ # end
+ #
+ # def to_a
+ # [@day, @month, @year]
+ # end
+ #
+ # def to_s
+ # "#{@day}-#{@month}-#{@year}"
+ # end
+ #
+ # # Returns wether or not this CompositeDate instance is valid.
+ # def valid?
+ # @day != 0 && @month != 0 && @year != 0
+ # end
+ # end
+ #
+ # class Artist < ActiveRecord::Base
+ # attribute_decorator :date_of_birth, :class => CompositeDate, :decorating => [:day, :month, :year]
+ # validates_decorator :date_of_birth, :message => 'is not a valid date'
+ # end
+ #
+ # artist = Artist.new
+ # artist.date_of_birth = '31-12-1999'
+ # artist.valid? # => true
+ # artist.date_of_birth = 'foo-bar-baz'
+ # artist.valid? # => false
+ # artist.errors.on(:date_of_birth) # => "is not a valid date"
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid").
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
+ # method, proc or string should return or evaluate to a true or false value.
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
+ # method, proc or string should return or evaluate to a true or false value.
+ def validates_view(*attrs)
+ configuration = { :message => I18n.translate('active_record.error_messages')[:invalid], :on => :save }
+ configuration.update attrs.extract_options!
+
+ invalid_keys = configuration.keys.select { |key| key == :allow_nil || key == :allow_blank }
+ raise ArgumentError, "Unknown key(s): #{ invalid_keys.join(', ') }" unless invalid_keys.empty?
+
+ validates_each(attrs, configuration) do |record, attr, value|
+ record.errors.add(attr, configuration[:message]) unless record.send(attr).valid?
+ end
+ end
+
+ private
+
+ def define_attribute_view_reader(attr, options)
+ class_eval do
+ define_method(attr) do
+ options[:as].new(*options[:decorating].map { |attribute| read_attribute(attribute) })
+ end
+ end
+ end
+
+ def define_attribute_view_writer(attr, options)
+ class_eval do
+ define_method("#{attr}_before_type_cast") do
+ instance_variable_get("@#{attr}_before_type_cast") || send(attr).to_s
+ end
+
+ define_method("#{attr}=") do |value|
+ instance_variable_set("@#{attr}_before_type_cast", value)
+ values = options[:as].parse(value).to_a
+ options[:decorating].each_with_index { |attribute, index| write_attribute attribute, values[index] }
+ value
+ end
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 43b46f4..d01730e 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -2578,8 +2578,8 @@ module ActiveRecord #:nodoc:
# an exclusive row lock.
def reload(options = nil)
clear_aggregation_cache
- clear_attribute_decorator_cache
clear_association_cache
+ clear_attribute_view_cache
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
@attributes_cache = {}
self
@@ -3015,8 +3015,8 @@ module ActiveRecord #:nodoc:
extend QueryCache
include Validations
include Locking::Optimistic, Locking::Pessimistic
- include AttributeDecorator
include AttributeMethods
+ include AttributeView
include Dirty
include Callbacks, Observing, Timestamp
include Associations, AssociationPreload, NamedScope
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3f3d3a1..ea5cfd6 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -17,8 +17,8 @@ module ActiveRecord
reflection = klass.new(macro, name, options, active_record)
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
- when :attribute_decorator
- reflection = AttributeDecoratorReflection.new(macro, name, options, active_record)
+ when :view
+ reflection = AttributeViewReflection.new(macro, name, options, active_record)
end
write_inheritable_hash :reflections, name => reflection
reflection
@@ -47,17 +47,17 @@ module ActiveRecord
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
end
- # Returns an array of DecoratorReflection objects for all the attribute decorators in the class.
- def reflect_on_all_attribute_decorators
- reflections.values.select { |reflection| reflection.is_a?(AttributeDecoratorReflection) }
+ # Returns an array of AttrbuteViewReflection objects for all the attribute views in the class.
+ def reflect_on_all_attribute_views
+ reflections.values.select { |reflection| reflection.is_a?(AttributeViewReflection) }
end
- # Returns the DecoratorReflection object for the named <tt>attribute decorator</tt> (use the symbol). Example:
+ # Returns the AttributeViewReflection object for the named <tt>view</tt> (use the symbol). Example:
#
- # Account.reflect_on_decorator(:balance) # returns the balance DecoratorReflection
+ # Account.reflect_on_attribute_view(:balance) # returns the balance AttributeViewReflection
#
- def reflect_on_attribute_decorator(attribute_decorator)
- reflections[attribute_decorator].is_a?(AttributeDecoratorReflection) ? reflections[attribute_decorator] : nil
+ def reflect_on_attribute_view(attribute_view)
+ reflections[attribute_view].is_a?(AttributeViewReflection) ? reflections[attribute_view] : nil
end
# Returns an array of AssociationReflection objects for all the associations in the class. If you only want to reflect on a
@@ -148,8 +148,11 @@ module ActiveRecord
class AggregateReflection < MacroReflection #:nodoc:
end
- # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
- class AttributeDecoratorReflection < MacroReflection #:nodoc:
+ # Holds all the meta-data about an attribute view as it was specified in the Active Record class.
+ class AttributeViewReflection < MacroReflection #:nodoc:
+ def klass
+ options[:as]
+ end
end
# Holds all the meta-data about an association as it was specified in the Active Record class.
diff --git a/activerecord/test/cases/attribute_decorator_test.rb b/activerecord/test/cases/attribute_decorator_test.rb
deleted file mode 100644
index 1527707..0000000
--- a/activerecord/test/cases/attribute_decorator_test.rb
+++ /dev/null
@@ -1,216 +0,0 @@
-require "cases/helper"
-require 'models/artist'
-
-class AttributeDecoratorClassMethodTest < ActiveRecord::TestCase
- def test_should_take_a_name_for_the_decorator_and_define_a_reader_and_writer_method_for_it
- %w{ date_of_birth date_of_birth= }.each { |method| assert Artist.instance_methods.include?(method) }
- end
-
- def test_should_not_take_any_options_other_than_class_and_class_name_and_decorates
- assert_raise(ArgumentError) do
- Artist.class_eval do
- attribute_decorator :foo, :some_other_option => true
- end
- end
- end
-end
-
-class AttributeDecoratorInGeneralTest < ActiveRecord::TestCase
- def setup
- @artist = Artist.create(:day => 31, :month => 12, :year => 1999)
- end
-
- def teardown
- Artist.class_eval do
- attribute_decorator :date_of_birth, :class_name => 'Decorators::CompositeDate', :decorates => [:day, :month, :year]
- end
- end
-
- uses_mocha('should_only_use_constantize_once_and_cache_the_result') do
- def test_should_only_use_constantize_once_and_cache_the_result
- klass_name_string = 'CompositeDate'
-
- Artist.class_eval do
- attribute_decorator :date_of_birth, :class_name => klass_name_string, :decorates => [:day, :month, :year]
- end
-
- klass_name_string.expects(:constantize).times(1).returns(Decorators::CompositeDate)
- 2.times { @artist.date_of_birth }
- end
- end
-
- def test_should_work_with_a_real_pointer_to_a_wrapper_class_instead_of_a_string
- Artist.class_eval do
- attribute_decorator :date_of_birth, :class => Decorators::CompositeDate, :decorates => [:day, :month, :year]
- end
-
- assert_equal "31-12-1999", @artist.date_of_birth.to_s
- end
-
- uses_mocha('should_also_work_with_an_anonymous_wrapper_class') do
- def test_should_also_work_with_an_anonymous_wrapper_class
- Artist.class_eval do
- attribute_decorator :date_of_birth, :decorates => [:day, :month, :year], :class => (Class.new(Decorators::CompositeDate) do
- # Reversed implementation of the super class.
- def to_s
- "#{@year}-#{@month}-#{@day}"
- end
- end)
- end
-
- 2.times { assert_equal "1999-12-31", @artist.date_of_birth.to_s }
- end
- end
-
- def test_should_reset_the_before_type_cast_values_on_reload
- @artist.date_of_birth = '01-01-1111'
- Artist.find(@artist.id).update_attribute(:day, 13)
- @artist.reload
-
- assert_equal "13-12-1999", @artist.date_of_birth_before_type_cast
- end
-end
-
-class AttributeDecoratorForMultipleAttributesTest < ActiveRecord::TestCase
- def setup
- @artist = Artist.create(:day => 31, :month => 12, :year => 1999)
- @decorator = @artist.date_of_birth
- end
-
- def test_should_return_an_instance_of_the_decorator_class_specified_by_the_class_name_option
- assert_instance_of Decorators::CompositeDate, @artist.date_of_birth
- end
-
- def test_should_have_assigned_values_to_decorate_to_the_decorator_instance
- assert_equal 31, @decorator.day
- assert_equal 12, @decorator.month
- assert_equal 1999, @decorator.year
- end
-
- def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
- @artist.date_of_birth = '01-02-2000'
- assert_equal '01-02-2000', @artist.date_of_birth_before_type_cast
- end
-
- def test_should_return_the_value_before_type_cast_when_the_value_was_just_read_from_the_database
- date_of_birth_as_string = @artist.date_of_birth.to_s
- @artist.reload
- assert_equal date_of_birth_as_string, @artist.date_of_birth_before_type_cast
- end
-
- def test_should_parse_the_value_assigned_through_the_setter_method_and_assign_them_to_the_model_instance
- @artist.date_of_birth = '01-02-2000'
- assert_equal 1, @artist.day
- assert_equal 2, @artist.month
- assert_equal 2000, @artist.year
- end
-end
-
-class AttributeDecoratorForOneAttributeTest < ActiveRecord::TestCase
- def setup
- @artist = Artist.create(:location => 'amsterdam')
- end
-
- def test_should_return_an_instance_of_the_decorator_class_specified_by_the_class_name_option
- assert_instance_of Decorators::GPSCoordinator, @artist.gps_location
- end
-
- def test_should_have_assigned_the_value_to_decorate_to_the_decorator_instance
- assert_equal 'amsterdam', @artist.gps_location.location
- end
-
- def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
- @artist.gps_location = 'rotterdam'
- assert_equal 'rotterdam', @artist.gps_location_before_type_cast
- end
-
- def test_should_return_the_value_before_type_cast_when_the_value_was_just_read_from_the_database
- gps_location_as_string = @artist.gps_location.to_s
- @artist.reload
- assert_equal gps_location_as_string, @artist.gps_location_before_type_cast
- end
-
- def test_should_parse_the_value_assigned_through_the_setter_method_and_assign_it_to_the_model_instance
- @artist.gps_location = 'amsterdam'
- assert_equal '+1, +1', @artist.location
-
- @artist.gps_location = 'rotterdam'
- assert_equal '-1, -1', @artist.location
- end
-end
-
-class AttributeDecoratorForAnAlreadyExistingAttributeTest < ActiveRecord::TestCase
- def setup
- @artist = Artist.create(:start_year => 1999)
- @decorator = @artist.start_year
- end
-
- def test_should_return_an_instance_of_the_decorator_class_specified_by_the_class_option
- assert_instance_of Decorators::GPSCoordinator, @artist.gps_location
- end
-
- def test_should_have_assigned_the_value_to_decorate_to_the_decorator_instance
- assert_equal 1999, @decorator.start_year
- end
-
- def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
- @artist.start_year = '40 bc'
- assert_equal '40 bc', @artist.start_year_before_type_cast
- end
-
- def test_should_parse_and_write_the_value_assigned_through_the_setter_method_and_assign_it_to_the_model_instance
- @artist.start_year = '40 bc'
- assert_equal -41, @artist.read_attribute(:start_year)
- end
-end
-
-class AttributeDecoratorValidatorTest < ActiveRecord::TestCase
- def teardown
- Artist.instance_variable_set(:@validate_callbacks, [])
- Artist.instance_variable_set(:@validate_on_update_callbacks, [])
- end
-
- def test_should_delegate_validation_to_the_decorator
- Artist.class_eval do
- validates_decorator :date_of_birth, :start_year
- end
-
- artist = Artist.create(:start_year => 1999)
-
- artist.start_year = 40
- assert artist.valid?
-
- artist.start_year = 'abcde'
- assert !artist.valid?
- assert_equal "is invalid", artist.errors.on(:start_year)
- end
-
- def test_should_take_a_options_hash_for_more_detailed_configuration
- Artist.class_eval do
- validates_decorator :start_year, :message => 'is not a valid date', :on => :update
- end
-
- artist = Artist.new(:start_year => 'abcde')
- assert artist.valid?
-
- artist.save!
- assert !artist.valid?
- assert_equal 'is not a valid date', artist.errors.on(:start_year)
- end
-
- def test_should_not_take_the_allow_nil_option
- assert_raise(ArgumentError) do
- Artist.class_eval do
- validates_decorator :start_year, :allow_nil => true
- end
- end
- end
-
- def test_should_not_take_the_allow_blank_option
- assert_raise(ArgumentError) do
- Artist.class_eval do
- validates_decorator :start_year, :allow_blank => true
- end
- end
- end
-end
\ No newline at end of file
diff --git a/activerecord/test/cases/attribute_view_test.rb b/activerecord/test/cases/attribute_view_test.rb
new file mode 100644
index 0000000..3d60b1c
--- /dev/null
+++ b/activerecord/test/cases/attribute_view_test.rb
@@ -0,0 +1,191 @@
+require "cases/helper"
+require 'models/artist'
+
+class AttributeViewClassMethodTest < ActiveRecord::TestCase
+ def test_should_take_a_name_for_the_view_and_define_a_reader_and_writer_method_for_it
+ %w{ date_of_birth date_of_birth= }.each { |method| assert Artist.instance_methods.include?(method) }
+ end
+
+ def test_should_not_take_any_options_for_the_view_other_than_class_and_class_name_and_decorating
+ assert_raise(ArgumentError) do
+ Artist.class_eval do
+ view :foo, :some_other_option => true
+ end
+ end
+ end
+end
+
+class AttributeViewInGeneralTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:day => 31, :month => 12, :year => 1999)
+ end
+
+ def test_should_also_work_with_an_anonymous_wrapper_class
+ Artist.class_eval do
+ view :date_of_birth, :decorating => [:day, :month, :year], :as => (Class.new(AttributeViews::CompositeDate) do
+ # Reversed implementation of the super class.
+ def to_s
+ "#{@year}-#{@month}-#{@day}"
+ end
+ end)
+ end
+
+ 2.times { assert_equal "1999-12-31", @artist.date_of_birth.to_s }
+
+ Artist.class_eval do
+ view :date_of_birth, :as => AttributeViews::CompositeDate, :decorating => [:day, :month, :year]
+ end
+ end
+
+ def test_should_reset_the_before_type_cast_values_on_reload
+ @artist.date_of_birth = '01-01-1111'
+ Artist.find(@artist.id).update_attribute(:day, 13)
+ @artist.reload
+
+ assert_equal "13-12-1999", @artist.date_of_birth_before_type_cast
+ end
+end
+
+class AttributeViewForMultipleAttributesTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:day => 31, :month => 12, :year => 1999)
+ @view = @artist.date_of_birth
+ end
+
+ def test_should_return_an_instance_of_the_view_class_specified_by_the_class_name_option
+ assert_instance_of AttributeViews::CompositeDate, @artist.date_of_birth
+ end
+
+ def test_should_have_assigned_the_values_it_decorates_to_the_view_instance
+ assert_equal 31, @view.day
+ assert_equal 12, @view.month
+ assert_equal 1999, @view.year
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
+ @artist.date_of_birth = '01-02-2000'
+ assert_equal '01-02-2000', @artist.date_of_birth_before_type_cast
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_just_read_from_the_database
+ date_of_birth_as_string = @artist.date_of_birth.to_s
+ @artist.reload
+ assert_equal date_of_birth_as_string, @artist.date_of_birth_before_type_cast
+ end
+
+ def test_should_parse_the_value_assigned_through_the_setter_method_and_assign_them_to_the_model_instance
+ @artist.date_of_birth = '01-02-2000'
+ assert_equal 1, @artist.day
+ assert_equal 2, @artist.month
+ assert_equal 2000, @artist.year
+ end
+end
+
+class AttributeViewForOneAttributeTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:location => 'amsterdam')
+ end
+
+ def test_should_return_an_instance_of_the_view_class_specified_by_the_as_option
+ assert_instance_of AttributeViews::GPSCoordinator, @artist.gps_location
+ end
+
+ def test_should_have_assigned_the_value_to_decorate_to_the_view_instance
+ assert_equal 'amsterdam', @artist.gps_location.location
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
+ @artist.gps_location = 'rotterdam'
+ assert_equal 'rotterdam', @artist.gps_location_before_type_cast
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_just_read_from_the_database
+ gps_location_as_string = @artist.gps_location.to_s
+ @artist.reload
+ assert_equal gps_location_as_string, @artist.gps_location_before_type_cast
+ end
+
+ def test_should_parse_the_value_assigned_through_the_setter_method_and_assign_it_to_the_model_instance
+ @artist.gps_location = 'amsterdam'
+ assert_equal '+1, +1', @artist.location
+
+ @artist.gps_location = 'rotterdam'
+ assert_equal '-1, -1', @artist.location
+ end
+end
+
+class AttributeViewForAnAlreadyExistingAttributeTest < ActiveRecord::TestCase
+ def setup
+ @artist = Artist.create(:start_year => 1999)
+ @view = @artist.start_year
+ end
+
+ def test_should_return_an_instance_of_the_view_class_specified_by_the_as_option
+ assert_instance_of AttributeViews::GPSCoordinator, @artist.gps_location
+ end
+
+ def test_should_have_assigned_the_value_to_decorate_to_the_view_instance
+ assert_equal 1999, @view.start_year
+ end
+
+ def test_should_return_the_value_before_type_cast_when_the_value_was_set_with_the_setter
+ @artist.start_year = '40 bc'
+ assert_equal '40 bc', @artist.start_year_before_type_cast
+ end
+
+ def test_should_parse_and_write_the_value_assigned_through_the_setter_method_and_assign_it_to_the_model_instance
+ @artist.start_year = '40 bc'
+ assert_equal -41, @artist.read_attribute(:start_year)
+ end
+end
+
+class AttributeViewValidatorTest < ActiveRecord::TestCase
+ def teardown
+ Artist.instance_variable_set(:@validate_callbacks, [])
+ Artist.instance_variable_set(:@validate_on_update_callbacks, [])
+ end
+
+ def test_should_delegate_validation_to_the_view
+ Artist.class_eval do
+ validates_view :date_of_birth, :start_year
+ end
+
+ artist = Artist.create(:start_year => 1999)
+
+ artist.start_year = 40
+ assert artist.valid?
+
+ artist.start_year = 'abcde'
+ assert !artist.valid?
+ assert_equal "is invalid", artist.errors.on(:start_year)
+ end
+
+ def test_should_take_an_options_hash_for_more_detailed_configuration
+ Artist.class_eval do
+ validates_view :start_year, :message => 'is not a valid date', :on => :update
+ end
+
+ artist = Artist.new(:start_year => 'abcde')
+ assert artist.valid?
+
+ artist.save!
+ assert !artist.valid?
+ assert_equal 'is not a valid date', artist.errors.on(:start_year)
+ end
+
+ def test_should_not_take_the_allow_nil_option
+ assert_raise(ArgumentError) do
+ Artist.class_eval do
+ validates_view :start_year, :allow_nil => true
+ end
+ end
+ end
+
+ def test_should_not_take_the_allow_blank_option
+ assert_raise(ArgumentError) do
+ Artist.class_eval do
+ validates_view :start_year, :allow_blank => true
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index ebb7e72..e95d7ab 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1246,12 +1246,12 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_clone_with_attribute_decorator_of_same_name_as_attribute
- dev = DeveloperWithAttributeDecorator.find(1)
- assert_kind_of DeveloperSalaryDecorator, dev.salary
+ dev = DeveloperWithAttributeView.find(1)
+ assert_kind_of DeveloperSalaryView, dev.salary
clone = nil
assert_nothing_raised { clone = dev.clone }
- assert_kind_of DeveloperSalaryDecorator, clone.salary
+ assert_kind_of DeveloperSalaryView, clone.salary
assert_equal dev.salary.amount, clone.salary.amount
assert clone.new_record?
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 6a4836e..9cb7083 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -92,28 +92,28 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal Money, Customer.reflect_on_aggregation(:balance).klass
end
- def test_attribute_decorator_reflection
- reflection_for_date_of_birth = ActiveRecord::Reflection::AttributeDecoratorReflection.new(
- :attribute_decorator, :date_of_birth, {
- :class_name => 'Decorators::CompositeDate',
- :decorates => [:day, :month, :year]
+ def test_attribute_view_reflection
+ reflection_for_date_of_birth = ActiveRecord::Reflection::AttributeViewReflection.new(
+ :view, :date_of_birth, {
+ :as => AttributeViews::CompositeDate,
+ :decorating => [:day, :month, :year]
}, Artist
)
- reflection_for_gps_location = ActiveRecord::Reflection::AttributeDecoratorReflection.new(
- :attribute_decorator, :gps_location, { :class_name => 'Decorators::GPSCoordinator', :decorates => :location }, Artist
+ reflection_for_gps_location = ActiveRecord::Reflection::AttributeViewReflection.new(
+ :view, :gps_location, { :as => AttributeViews::GPSCoordinator, :decorating => :location }, Artist
)
- reflection_for_start_year = ActiveRecord::Reflection::AttributeDecoratorReflection.new(
- :attribute_decorator, :start_year, { :class_name => 'Decorators::Year' }, Artist
+ reflection_for_start_year = ActiveRecord::Reflection::AttributeViewReflection.new(
+ :view, :start_year, { :as => AttributeViews::Year }, Artist
)
[reflection_for_date_of_birth, reflection_for_gps_location, reflection_for_start_year].each do |reflection|
- assert Artist.reflect_on_all_attribute_decorators.include?(reflection)
+ assert Artist.reflect_on_all_attribute_views.include?(reflection)
end
- assert_equal reflection_for_date_of_birth, Artist.reflect_on_attribute_decorator(:date_of_birth)
- assert_equal Decorators::CompositeDate, Artist.reflect_on_attribute_decorator(:date_of_birth).klass
+ assert_equal reflection_for_date_of_birth, Artist.reflect_on_attribute_view(:date_of_birth)
+ assert_equal AttributeViews::CompositeDate, Artist.reflect_on_attribute_view(:date_of_birth).klass
end
def test_has_many_reflection
diff --git a/activerecord/test/models/artist.rb b/activerecord/test/models/artist.rb
index f2e4d1b..2a5f7a6 100644
--- a/activerecord/test/models/artist.rb
+++ b/activerecord/test/models/artist.rb
@@ -1,81 +1,81 @@
-class Artist < ActiveRecord::Base
- # Defines a non existing attribute decorating multiple existing attributes
- attribute_decorator :date_of_birth, :class_name => 'Decorators::CompositeDate', :decorates => [:day, :month, :year]
-
- # Defines a decorates for one attribute.
- attribute_decorator :gps_location, :class_name => 'Decorators::GPSCoordinator', :decorates => :location
-
- # Defines a decorator for an existing attribute.
- attribute_decorator :start_year, :class_name => 'Decorators::Year'
-
- # These validations are defined inline in the test cases. See attribute_decorator_test.rb.
- #
- # validates_decorator :date_of_birth, :start_year
- # validates_decorator :start_year, :message => 'is not a valid date', :on => :update
-end
-
-module Decorators
+module AttributeViews
class CompositeDate
attr_reader :day, :month, :year
-
+
def self.parse(value)
new *value.scan(/(\d\d)-(\d\d)-(\d{4})/).flatten.map { |x| x.to_i }
end
-
+
def initialize(day, month, year)
@day, @month, @year = day, month, year
end
-
+
def valid?
true
end
-
+
def to_a
[@day, @month, @year]
end
-
+
def to_s
"#{@day}-#{@month}-#{@year}"
end
end
-
+
class GPSCoordinator
attr_reader :location
-
+
def self.parse(value)
new(value == 'amsterdam' ? '+1, +1' : '-1, -1')
end
-
+
def initialize(location)
@location = location
end
-
+
def to_a
[@location]
end
-
+
def to_s
@location
end
end
-
+
class Year
attr_reader :start_year
-
+
def self.parse(value)
new(value == '40 bc' ? -41 : value.to_i)
end
-
+
def initialize(start_year)
@start_year = start_year
end
-
+
def valid?
@start_year != 0
end
-
+
def to_a
[@start_year]
end
end
+end
+
+class Artist < ActiveRecord::Base
+ # Defines a non existing attribute decorating multiple existing attributes
+ view :date_of_birth, :as => AttributeViews::CompositeDate, :decorating => [:day, :month, :year]
+
+ # Defines a decorates for one attribute.
+ view :gps_location, :as => AttributeViews::GPSCoordinator, :decorating => :location
+
+ # Defines a decorator for an existing attribute.
+ view :start_year, :as => AttributeViews::Year
+
+ # These validations are defined inline in the test cases. See attribute_decorator_test.rb.
+ #
+ # validates_view :date_of_birth, :start_year
+ # validates_view :start_year, :message => 'is not a valid date', :on => :update
end
\ No newline at end of file
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 14357bc..289bea2 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -70,10 +70,10 @@ ActiveSupport::Deprecation.silence do
end
end
-DeveloperSalaryDecorator = Struct.new(:amount)
-class DeveloperWithAttributeDecorator < ActiveRecord::Base
+DeveloperSalaryView = Struct.new(:amount)
+class DeveloperWithAttributeView < ActiveRecord::Base
self.table_name = 'developers'
- attribute_decorator :salary, :class => DeveloperSalaryDecorator
+ view :salary, :as => DeveloperSalaryView
end
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
--
1.5.5.3
From 553d02f2f05cd1a0a8d6a992828a874b7111d8a6 Mon Sep 17 00:00:00 2001
From: Eloy Duran <eloy.de.enige@gmail.com>
Date: Wed, 10 Dec 2008 21:43:18 +0100
Subject: [PATCH] Updated documentation for the API update.
---
activerecord/lib/active_record/attribute_view.rb | 47 +++++++++++-----------
activerecord/test/models/artist.rb | 8 ++--
2 files changed, 27 insertions(+), 28 deletions(-)
diff --git a/activerecord/lib/active_record/attribute_view.rb b/activerecord/lib/active_record/attribute_view.rb
index 5e9968a..a846309 100644
--- a/activerecord/lib/active_record/attribute_view.rb
+++ b/activerecord/lib/active_record/attribute_view.rb
@@ -13,26 +13,25 @@ module ActiveRecord
end
module ClassMethods
- # Adds reader and writer methods for decorating one or more attributes:
- # <tt>attribute_decorator :date_of_birth</tt> adds <tt>date_of_birth</tt> and <tt>date_of_birth=(new_date_of_birth)</tt> methods.
+ # Defines an attribute view, which adds a reader and a writer method for decorating one or more attributes:
+ # <tt>view :date_of_birth</tt> adds <tt>date_of_birth</tt> and <tt>date_of_birth=(new_date_of_birth)</tt> methods.
#
# Options are:
- # * <tt>:class</tt> - specify the decorator class.
- # * <tt>:class_name</tt> - specify the class name of the decorator class,
- # this should be used if, at the time of loading the model class, the decorator class is not yet available.
- # * <tt>:decorating</tt> - specifies the attributes that should be wrapped by the decorator class.
- # Takes an array of attributes or a single attribute. If none is specified the same name as the name of the attribute_decorator is assumed.
+ # * <tt>:as</tt> - specify the attribute view class.
+ # * <tt>:decorating</tt> - specifies the attributes that should be wrapped by the attribute view class.
+ # Takes an array of attributes or a single attribute. If none is specified the same name as the name of the view is assumed.
#
- # The decorator class should implement a class method called <tt>parse</tt>, which takes 1 argument.
- # In that method your decorator class is responsible for returning an instance of itself with the attribute(s) parsed and assigned.
+ # The attribute view class should implement a class method called <tt>parse</tt>, which should take 1 argument.
+ # In that method your attribute view class is responsible for returning an instance of itself with the attribute(s) parsed and assigned.
#
- # Your decorator class’s initialize method should take as it’s arguments the attributes that were specified
- # to the <tt>:decorating</tt> option and in the same order as they were specified.
+ # Your attribute view class’s initialize method should take, as it’s arguments, the attributes that were specified
+ # with the <tt>:decorating</tt> option and in the same order as they were specified.
# You should also implement a <tt>to_a</tt> method which should return the parsed values as an array,
# again in the same order as specified with the <tt>:decorating</tt> option.
+ # Lastly, an implementation of <tt>to_s</tt> is needed which will be used by, for instance, the form helpers.
#
- # If you wish to use <tt>validates_decorator</tt>, your decorator class should also implement a <tt>valid?</tt> instance method,
- # which is responsible for checking the validity of the value(s). See <tt>validates_decorator</tt> for more info.
+ # If you wish to use <tt>validates_view</tt>, your attribute view class should also implement a <tt>valid?</tt> instance method,
+ # which is responsible for checking the validity of the value(s). See <tt>validates_view</tt> for more info.
#
# class CompositeDate
# attr_accessor :day, :month, :year
@@ -53,7 +52,7 @@ module ActiveRecord
# [@day, @month, @year]
# end
#
- # # Here we return a string representation of the value, this will for instance be used by the form helpers.
+ # # Here we return a string representation of the value, this will, for instance, be used by the form helpers.
# def to_s
# "#{@day}-#{@month}-#{@year}"
# end
@@ -65,16 +64,16 @@ module ActiveRecord
# end
#
# class Artist < ActiveRecord::Base
- # attribute_decorator :date_of_birth, :class => CompositeDate, :decorating => [:day, :month, :year]
- # validates_decorator :date_of_birth, :message => 'is not a valid date'
+ # view :date_of_birth, :as => CompositeDate, :decorating => [:day, :month, :year]
+ # validates_view :date_of_birth, :message => 'is not a valid date'
# end
#
# Option examples:
- # attribute_decorator :date_of_birth, :class => CompositeDate, :decorating => [:day, :month, :year]
- # attribute_decorator :gps_location, :class_name => 'GPSCoordinator', :decorating => :location
- # attribute_decorator :balance, :class_name => 'Money'
- # attribute_decorator :english_date_of_birth, :class => (Class.new(CompositeDate) do
- # # This is a anonymous subclass of CompositeDate that supports the date in English order
+ # view :date_of_birth, :as => CompositeDate, :decorating => [:day, :month, :year]
+ # view :gps_location, :as => 'GPSCoordinator', :decorating => :location
+ # view :balance, :as => Money
+ # view :english_date_of_birth, :as => (Class.new(CompositeDate) do
+ # # This is an anonymous subclass of CompositeDate that supports the date in English order
# def to_s
# "#{@month}/#{@day}/#{@year}"
# end
@@ -99,7 +98,7 @@ module ActiveRecord
create_reflection(:view, attr, options, self)
end
- # Validates wether the decorated attribute is valid by sending the decorator instance the <tt>valid?</tt> message.
+ # Validates wether the attribute view is valid by sending it the <tt>valid?</tt> message.
#
# class CompositeDate
# attr_accessor :day, :month, :year
@@ -128,8 +127,8 @@ module ActiveRecord
# end
#
# class Artist < ActiveRecord::Base
- # attribute_decorator :date_of_birth, :class => CompositeDate, :decorating => [:day, :month, :year]
- # validates_decorator :date_of_birth, :message => 'is not a valid date'
+ # view :date_of_birth, :as => CompositeDate, :decorating => [:day, :month, :year]
+ # validates_view :date_of_birth, :message => 'is not a valid date'
# end
#
# artist = Artist.new
diff --git a/activerecord/test/models/artist.rb b/activerecord/test/models/artist.rb
index 2a5f7a6..02e1cce 100644
--- a/activerecord/test/models/artist.rb
+++ b/activerecord/test/models/artist.rb
@@ -65,16 +65,16 @@ module AttributeViews
end
class Artist < ActiveRecord::Base
- # Defines a non existing attribute decorating multiple existing attributes
+ # Defines an attribute view decorating multiple existing attributes
view :date_of_birth, :as => AttributeViews::CompositeDate, :decorating => [:day, :month, :year]
- # Defines a decorates for one attribute.
+ # Defines a view for one attribute.
view :gps_location, :as => AttributeViews::GPSCoordinator, :decorating => :location
- # Defines a decorator for an existing attribute.
+ # Defines a view for an existing attribute.
view :start_year, :as => AttributeViews::Year
- # These validations are defined inline in the test cases. See attribute_decorator_test.rb.
+ # These validations are defined inline in the test cases. See attribute_view_test.rb.
#
# validates_view :date_of_birth, :start_year
# validates_view :start_year, :message => 'is not a valid date', :on => :update
--
1.5.5.3
From 87f51f38260974cab7d129c0cb116fad5c88ed71 Mon Sep 17 00:00:00 2001
From: Eloy Duran <eloy.de.enige@gmail.com>
Date: Wed, 10 Dec 2008 21:57:37 +0100
Subject: [PATCH] Updated composed_of deprecation warning to point to ActiveRecord::AttributeView::view.
---
activerecord/lib/active_record/aggregations.rb | 2 +-
activerecord/test/cases/base_test.rb | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 04a67ae..94e1a4d 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -192,7 +192,7 @@ module ActiveRecord
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
#
def composed_of(part_id, options = {}, &block)
- ActiveSupport::Deprecation.warn("ActiveRecord::Aggregations::composed_of has been deprecated. Please use ActiveRecord::AttributeDecorator::attribute_decorator.")
+ ActiveSupport::Deprecation.warn("ActiveRecord::Aggregations::composed_of has been deprecated. Please use ActiveRecord::AttributeView::view.")
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index e95d7ab..84a2ae3 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1245,7 +1245,7 @@ class BasicsTest < ActiveRecord::TestCase
assert clone.id != dev.id
end
- def test_clone_with_attribute_decorator_of_same_name_as_attribute
+ def test_clone_with_attribute_view_of_same_name_as_attribute
dev = DeveloperWithAttributeView.find(1)
assert_kind_of DeveloperSalaryView, dev.salary
--
1.5.5.3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment