Skip to content

Serializable and validatable value objects for ActiveRecord

License

Notifications You must be signed in to change notification settings

GoldenOwlAsia/value_objects

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

Serializable and validatable value objects for ActiveRecord

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 100.0%