0.01
No commit activity in last 3 years
No release in over 3 years
Serializable and validatable value objects for ActiveRecord
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.11
~> 10.0
~> 3.0
~> 1.3

Runtime

 Project Readme

ValueObjects

Serializable and validatable value objects for ActiveRecord

Installation

Add this line to your application's Gemfile:

gem 'value_objects'

And then execute:

$ bundle

Or install it yourself as:

$ gem install value_objects

Usage

Create the value object class

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"}

Add validations

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 # => {}

Serialization with ActiveRecord

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

Validation with ActiveRecord

By 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"}

With ValueObjects::ActiveRecord

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
end

This 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
end

Or, to skip validation entirely:

class Customer < ActiveRecord::Base
  include ValueObjects::ActiveRecord
  value_object :home_address, AddressValue, no_validation: true
end

Value object collections

Serialization 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
end

Then 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.

Integrate with Cocoon

Put this into a Rails initializer (e.g. config/initializers/value_objects.rb):

ValueObjects::ActionView.integrate_with :cocoon

This 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'

Maintainers

Contributing

  • 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.

License

The gem is available as open source under the terms of the MIT License.