diff --git a/README.md b/README.md index d15a48f..0574f61 100644 --- a/README.md +++ b/README.md @@ -310,8 +310,10 @@ user.attributes ### IMPORTANT note about member coercions -ShallowAttributes performs coercions only when a value is being assigned. If you mutate the value -later on using its own interfaces then coercion won't be triggered. +ShallowAttributes guarantee coercions only when a value is being assigned. If you mutate the value +later on using its own interfaces then coercion may not be triggered. + +But ShallowAttributes uses `MutableArray < Array` with redefined mutation methods for `Array`s. Here's an example: @@ -331,7 +333,7 @@ library = Library.new # This will coerce Hash to a Book instance library.books = [ { :title => 'Introduction' } ] -# This WILL NOT COERCE the value because you mutate the books array with Array#<< +# This WILL COERCE the value because ShallowAttributes uses `MutableArray < Array` with redefined `#<<` library.books << { :title => 'Another Introduction' } ``` diff --git a/lib/shallow_attributes/class_methods.rb b/lib/shallow_attributes/class_methods.rb index 1353c66..95c1dc6 100644 --- a/lib/shallow_attributes/class_methods.rb +++ b/lib/shallow_attributes/class_methods.rb @@ -84,7 +84,7 @@ def attributes # # @since 0.1.0 def attribute(name, type, options = {}) - options[:default] ||= [] if type == Array + options[:default] ||= Type::MutableArray.new if type == Array default_values[name] = options.delete(:default) mandatory_attributes[name] = options.delete(:present) diff --git a/lib/shallow_attributes/type.rb b/lib/shallow_attributes/type.rb index de72d39..1393654 100644 --- a/lib/shallow_attributes/type.rb +++ b/lib/shallow_attributes/type.rb @@ -1,4 +1,5 @@ require 'shallow_attributes/type/array' +require 'shallow_attributes/type/mutable_array' require 'shallow_attributes/type/boolean' require 'shallow_attributes/type/date_time' require 'shallow_attributes/type/float' diff --git a/lib/shallow_attributes/type/array.rb b/lib/shallow_attributes/type/array.rb index 28d4083..d902389 100644 --- a/lib/shallow_attributes/type/array.rb +++ b/lib/shallow_attributes/type/array.rb @@ -27,16 +27,7 @@ def coerce(values, options = {}) unless values.is_a? ::Array raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{values}" for type "Array") end - values.map! do |value| - ShallowAttributes::Type.coerce(item_klass(options[:of]), value) - end - end - - private - - def item_klass(klass) - return klass unless klass.is_a? ::String - Object.const_get(klass) + MutableArray.new(values, **options) end end end diff --git a/lib/shallow_attributes/type/mutable_array.rb b/lib/shallow_attributes/type/mutable_array.rb new file mode 100644 index 0000000..ad182be --- /dev/null +++ b/lib/shallow_attributes/type/mutable_array.rb @@ -0,0 +1,46 @@ +module ShallowAttributes + module Type + # Class for mutable arrays. + # + # @abstract + # + # @since 0.9.1 + class MutableArray < ::Array + def initialize(items = [], of: nil) + @of = of + super( + items.map { |item| ShallowAttributes::Type.coerce(item_class, item) } + ) + end + + def <<(item) + super ShallowAttributes::Type.coerce(item_class, item) + end + + def push(*items) + super( + *items.map { |item| ShallowAttributes::Type.coerce(item_class, item) } + ) + end + + def concat(other) + super( + other.map { |item| ShallowAttributes::Type.coerce(item_class, item) } + ) + end + + def replace(other) + super( + other.map { |item| ShallowAttributes::Type.coerce(item_class, item) } + ) + end + + private + + def item_class + return @of unless @of.is_a? ::String + Object.const_get(@of) + end + end + end +end diff --git a/test/custom_types_test.rb b/test/custom_types_test.rb index b0955bf..ab16223 100644 --- a/test/custom_types_test.rb +++ b/test/custom_types_test.rb @@ -105,6 +105,64 @@ class Person person.addresses[0].must_be_instance_of Address end + describe 'change array by embedded values' do + it 'works for #<<' do + person.addresses << { street: 'Street 4/2', city: { name: 'Berlin' } } + + person.addresses.must_be_kind_of Array + person.addresses[2].must_be_instance_of Address + + person.addresses[2].street.must_be_instance_of String + person.addresses[2].street.must_equal 'Street 4/2' + + person.addresses[2].city.must_be_instance_of City + person.addresses[2].city.name.must_equal 'Berlin' + end + + it 'works for #push' do + person.addresses.push street: 'Street 4/2', city: { name: 'Berlin' } + + person.addresses.must_be_kind_of Array + person.addresses[2].must_be_instance_of Address + + person.addresses[2].street.must_be_instance_of String + person.addresses[2].street.must_equal 'Street 4/2' + + person.addresses[2].city.must_be_instance_of City + person.addresses[2].city.name.must_equal 'Berlin' + end + + it 'works for #concat' do + person.addresses.concat [ + { street: 'Street 4/2', city: { name: 'Berlin' } } + ] + + person.addresses.must_be_kind_of Array + person.addresses[2].must_be_instance_of Address + + person.addresses[2].street.must_be_instance_of String + person.addresses[2].street.must_equal 'Street 4/2' + + person.addresses[2].city.must_be_instance_of City + person.addresses[2].city.name.must_equal 'Berlin' + end + + it 'works for #replace' do + person.addresses.replace [ + { street: 'Street 4/2', city: { name: 'Berlin' } } + ] + + person.addresses.must_be_kind_of Array + person.addresses[0].must_be_instance_of Address + + person.addresses[0].street.must_be_instance_of String + person.addresses[0].street.must_equal 'Street 4/2' + + person.addresses[0].city.must_be_instance_of City + person.addresses[0].city.name.must_equal 'Berlin' + end + end + describe '#attributes' do it 'returns attributes hash' do hash = person.attributes