Serializable and validatable value objects for ActiveRecord
Add this line to your application's Gemfile:
gem 'value_objects'And then execute:
$ bundle
Or install it yourself as:
$ gem install value_objects
The value object class inherits from ValueObjects::Base, and attributes are defined with attr_accessor:
class AddressValue < ValueObjects::Base
attr_accessor :street, :postcode, :city
end
address = AddressValue.new(street: '123 Big Street', postcode: '12345', city: 'Metropolis')
address.street # => "123 Big Street"
address.street = '321 Main St' # => "321 Main St"
address.to_hash # => {:street=>"321 Main St", :postcode=>"12345", :city=>"Metropolis"}Validations can be added using the DSL from ActiveModel::Validations:
class AddressValue < ValueObjects::Base
attr_accessor :street, :postcode, :city
validates :postcode, presence: true
end
address = AddressValue.new(street: '123 Big Street', city: 'Metropolis')
address.valid? # => false
address.errors.to_h # => {:postcode=>"can't be blank"}
address.postcode = '12345' # => "12345"
address.valid? # => true
address.errors.to_h # => {}For columns of json type, the value object class can be used as the coder for the serialize method:
class Customer < ActiveRecord::Base
serialize :home_address, AddressValue
end
customer = Customer.new
customer.home_address = AddressValue.new(street: '123 Big Street', postcode: '12345', city: 'Metropolis')
customer.save
customer.reload
customer.home_address # => #<AddressValue:0x00ba9876543210 @street="123 Big Street", @postcode="12345", @city="Metropolis">For columns of string or text type, wrap the value object class in a JsonCoder:
class Customer < ActiveRecord::Base
serialize :home_address, ValueObjects::ActiveRecord::JsonCoder.new(AddressValue)
endBy default, validating the record does not automatically validate the value object.
Use the ValueObjects::ValidValidator to make this automatic:
class Customer < ActiveRecord::Base
serialize :home_address, AddressValue
validates :home_address, 'value_objects/valid': true
validates :home_address, presence: true # other validations are allowed too
end
customer = Customer.new
customer.home_address = AddressValue.new(street: '123 Big Street', city: 'Metropolis')
customer.valid? # => false
customer.errors.to_h # => {:home_address=>"is invalid"}
customer.home_address.errors.to_h # => {:postcode=>"can't be blank"}
customer = Customer.new
customer.valid? # => false
customer.errors.to_h # => {:home_address=>"can't be blank"}For easy set up of both serialization and validation, include ValueObjects::ActiveRecord and invoke value_object:
class Customer < ActiveRecord::Base
include ValueObjects::ActiveRecord
value_object :home_address, AddressValue
validates :home_address, presence: true
endThis basically works the same way but also defines the <attribute>_attributes= method which can be used to assign the value object using a hash:
customer.home_address_attributes = { street: '321 Main St', postcode: '54321', city: 'Micropolis' }
customer.home_address # => #<AddressValue:0x00ba9876503210 @street="321 Main St", @postcode="54321", @city="Micropolis">This is functionally similar to what accepts_nested_attributes_for does for associations.
Also, value_object will use the JsonCoder automatically if it detects that the column type is string or text.
Additional options may be passed in to customize validation:
class Customer < ActiveRecord::Base
include ValueObjects::ActiveRecord
value_object :home_address, AddressValue, allow_nil: true
endOr, to skip validation entirely:
class Customer < ActiveRecord::Base
include ValueObjects::ActiveRecord
value_object :home_address, AddressValue, no_validation: true
endSerialization and validation of value object collections are also supported.
First, create a nested Collection class that inherits from ValueObjects::Base::Collection:
class AddressValue < ValueObjects::Base
attr_accessor :street, :postcode, :city
validates :postcode, presence: true
class Collection < Collection
end
endThen use the nested Collection class as the serialization coder:
class Customer < ActiveRecord::Base
include ValueObjects::ActiveRecord
value_object :addresses, AddressValue::Collection
validates :addresses, presence: true
end
customer = Customer.new(addresses: [])
customer.valid? # => false
customer.errors.to_h # => {:addresses=>"can't be blank"}
customer.addresses << AddressValue.new(street: '123 Big Street', postcode: '12345', city: 'Metropolis')
customer.valid? # => true
customer.addresses << AddressValue.new(street: '321 Main St', city: 'Micropolis')
customer.valid? # => false
customer.errors.to_h # => {:addresses=>"is invalid"}
customer.addresses[1].errors.to_h # => {:postcode=>"can't be blank"}The <attribute>_attributes= method also functions in much the same way:
customer.addresses_attributes = { '0' => { city: 'Micropolis' }, '1' => { city: 'Metropolis' } }
customer.addresses # => [#<AddressValue:0x00ba9876543210 @city="Micropolis">, #<AddressValue:0x00ba9876503210 @city="Metropolis">]Except, items with '-1' keys are considered as dummy items and ignored:
customer.addresses_attributes = { '0' => { city: 'Micropolis' }, '-1' => { city: 'Metropolis' } }
customer.addresses # => [#<AddressValue:0x00ba9876543210 @city="Micropolis">]This is useful when data is submitted via standard HTML forms encoded with the 'application/x-www-form-urlencoded' media type (which cannot represent empty collections). To work around this, a dummy item can be added to the collection with it's key set to '-1' and it will conveniently be ignored when assigned to the value object collection.
Put this into a Rails initializer (e.g. config/initializers/value_objects.rb):
ValueObjects::ActionView.integrate_with :cocoonThis will add the link_to_add_nested_value & link_to_remove_nested_value view helpers.
Use them in place of Cocoon's link_to_add_association & link_to_remove_association when working with nested value objects:
# use the attribute name (:addresses) in place of the association name
# and supply the value object class as the next argument
link_to_add_nested_value 'Add Address', f, :addresses, AddressValue
# the `f` form builder argument is not needed
link_to_remove_nested_value 'Remove Address'- Matthew Yeow (https://github.com/tbsmatt), Tinkerbox Studios (https://www.tinkerbox.com.sg/)
- Fork the repository.
- Make your feature addition or bug fix.
- Add tests for it. This is important so we don't break it in a future version unintentionally.
- Commit, but do not mess with rakefile or version. (if you want to have your own version, that is fine but bump version in a commit by itself)
- Submit a pull request. Bonus points for topic branches.
The gem is available as open source under the terms of the MIT License.