ContractedValue
Library for creating contracted immutable(by default) value objects
This gem allows creation of value objects which are
See details explanation in below sections
Status



The above badges are generated by https://shields.io/
Installation
Add this line to your application's Gemfile:
gem "contracted_value", require: false
And then execute:
$ bundle
Or install it yourself as:
$ gem install contracted_value
Usage
The examples below might contain some of my habbits,
like including contracts.ruby
modules in class
You don't have to do it
Attribute Declaration
You can declare with or without contract/default value
But an attribute cannot be declared twice
module ::Geometry
end
module ::Geometry::LocationRange
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:latitude,
contract: Numeric,
)
attribute(
:longitude,
contract: Numeric,
)
attribute(
:radius_in_meter,
contract: And[Numeric, Send[:positive?]],
)
attribute(
:latitude,
)
end
end
location_range = ::Geometry::LocationRange::Entry.new(
latitude: 22.2,
longitude: 114.4,
radius_in_meter: 1234,
)
Attribute Assignment
Only Hash
and ContractedValue::Value
can be passed to .new
module ::Geometry
end
module ::Geometry::Location
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:latitude,
contract: Numeric,
)
attribute(
:longitude,
contract: Numeric,
)
end
end
module ::Geometry::LocationRange
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:latitude,
contract: Numeric,
)
attribute(
:longitude,
contract: Numeric,
)
attribute(
:radius_in_meter,
contract: Maybe[And[Numeric, Send[:positive?]]],
default_value: nil,
)
end
end
location = ::Geometry::Location::Entry.new(
latitude: 22.2,
longitude: 114.4,
)
location_range = ::Geometry::LocationRange::Entry.new(location)
Passing objects of different ContractedValue::Value
subclasses to .new
Possible due to the implementation calling #to_h
for ContractedValue::Value
objects
But in case the attribute names are different, or adding new attributes/updating existing attributes is needed
You will need to call #to_h
to get a Hash
and do whatever modification needed before passing into .new
class Pokemon < ::ContractedValue::Value
attribute(:name)
attribute(:type)
end
class Pikachu < ::Pokemon
attribute(:name, default_value: "Pikachu")
attribute(:type, default_value: "Thunder")
end
pikachu = Pikachu.new(name: "PikaPika")
pikachu.name
pikachu.type
pokemon1 = Pokemon.new(pikachu)
pokemon1.name
pokemon1.type
pokemon2 = Pokemon.new(pikachu.to_h.merge(name: "Piak"))
pokemon2.name
pokemon2.type
Input Validation
Input values are validated on object creation (instead of on attribute value access) with 2 validations:
- Value contract
- Value presence
Value contract
An attribute can be declared without any contract, and any input value would be pass the validation
But you can pass a contract via contract
option (must be a contracts.ruby
contract)
Passing input value violating an attribute's contract would cause an error
class YetAnotherRationalNumber < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:numerator,
contract: ::Integer,
)
attribute(
:denominator,
contract: And[::Integer, Not[Send[:zero?]]],
)
end
YetAnotherRationalNumber.new(
numerator: 1,
denominator: 0,
)
Value presence
An attribute declared should be provided a value on object creation, even the input value is nil
Otherwise an error is raised
You can pass default value via option default_value
The default value will need to confront to the contract passed in contract
option too
module ::WhatIsThis
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:something_required,
)
attribute(
:something_optional,
default_value: nil,
)
attribute(
:something_with_error,
contract: NatPos,
default_value: 0,
)
end
end
WhatIsThis::Entry.new(
something_required: 123,
).something_optional
Object Freezing
All input values are frozen using ice_nine
by default
But some objects won't work properly when deeply frozen (rails obviously)
So you can specify how input value should be frozen (or not frozen) with option refrigeration_mode
Possible values are:
:deep
(default)
:shallow
:none
However the value object itself is always frozen
Any lazy method caching with use of instance var would cause FrozenError
(Many Rails classes use lazy caching heavily so most rails object can't be frozen to work properly)
class SomeDataEntry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:cold_hash,
contract: ::Hash,
)
attribute(
:cool_hash,
contract: ::Hash,
refrigeration_mode: :shallow,
)
attribute(
:warm_hash,
contract: ::Hash,
refrigeration_mode: :none,
)
def cached_hash
@cached_hash ||= {}
end
end
entry = SomeDataEntry.new(
cold_hash: {a: {b: 0}},
cool_hash: {a: {b: 0}},
warm_hash: {a: {b: 0}},
)
entry.cold_hash[:a].delete(:b)
entry.cool_hash[:a].delete(:b)
entry.cool_hash.delete(:a)
entry.warm_hash.delete(:a)
entry.cached_hash
Beware that the value passed to default_value
option when declaring an attribute is always deeply frozen
This is to avoid any in-place change which changes the default value of any value object class attribute
Value Object Class Inheritance
You can create a value object class inheriting an existing value class instead of ::ContractedValue::Value
All existing attributes can be used
No need to explain right?
class Pokemon < ::ContractedValue::Value
attribute(:name)
end
class Pikachu < ::Pokemon
attribute(:type, default_value: "Thunder")
end
pikachu = Pikachu.new(name: "PikaPika")
pikachu.name
pikachu.type
All existing attributes can be redeclared
Within the same class you cannot redefine an attribute
But in subclasses you can
class Pokemon < ::ContractedValue::Value
attribute(:name)
end
class Pikachu < ::Pokemon
include ::Contracts::Core
include ::Contracts::Builtin
attribute(
:name,
contract: And[::String, Not[Send[:empty?]]],
default_value: String.new("Pikachu"),
refrigeration_mode: :none,
)
end
Pikachu.new.name
Pikachu.new.name.frozen?
Pikachu.new(name: "Pikaaaachuuu").name.frozen?
Related gems
Here is a list of gems which I found and I have tried some of them.
But eventually I am unsatisfied so I build this gem.
I used to use this a bit
But I keep having to write the attribute names in Values.new
,
then the same attribute names again with attr_reader
+ contract (since I want to use contract)
Also the input validation happens on attribute value access instead of on object creation
Got similar issue as values
Seems more suitable for form objects instead of just value objects (for me)
Contributing
- Fork it ( https://github.com/PikachuEXE/contracted_value/fork )
- Create your branch (Preferred to be prefixed with
feature
/fix
/other sensible prefixes)
- Commit your changes (No version related changes will be accepted)
- Push to the branch on your forked repo
- Create a new Pull Request