Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/pikachuexe/contracted_value
Library for creating contracted immutable(by default) value objects
https://github.com/pikachuexe/contracted_value
contracts gem ruby value-object
Last synced: 25 days ago
JSON representation
Library for creating contracted immutable(by default) value objects
- Host: GitHub
- URL: https://github.com/pikachuexe/contracted_value
- Owner: PikachuEXE
- License: mit
- Created: 2019-05-30T09:03:29.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2023-10-11T08:36:25.000Z (about 1 year ago)
- Last Synced: 2024-05-01T21:36:33.503Z (7 months ago)
- Topics: contracts, gem, ruby, value-object
- Language: Ruby
- Homepage:
- Size: 48.8 KB
- Stars: 6
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# ContractedValue
Library for creating contracted immutable(by default) value objects
This gem allows creation of value objects which are
- contracted (enforced by [`contracts.ruby`](https://github.com/egonSchiele/contracts.ruby))
- immutable (enforced by [`ice_nine`](https://github.com/dkubb/ice_nine))See details explanation in below sections
## Status
[![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/PikachuEXE/contracted_value/tests.yaml?branch=master&style=flat-square)](https://github.com/PikachuEXE/contracted_value/actions/workflows/tests.yaml)
[![Gem Version](http://img.shields.io/gem/v/contracted_value.svg?style=flat-square)](http://badge.fury.io/rb/contracted_value)
[![License](https://img.shields.io/github/license/PikachuEXE/contracted_value.svg?style=flat-square)](http://badge.fury.io/rb/contracted_value)[![Code Climate](https://img.shields.io/codeclimate/maintainability/PikachuEXE/contracted_value.svg?style=flat-square)](https://codeclimate.com/github/PikachuEXE/contracted_value)
[![Coverage Status](http://img.shields.io/coveralls/PikachuEXE/contracted_value.svg?style=flat-square)](https://coveralls.io/r/PikachuEXE/contracted_value)> The above badges are generated by https://shields.io/
## Installation
Add this line to your application's Gemfile:
```ruby
# `require` can be set to `true` safely without too much side effect
# (except having additional modules & classes defined which could be wasting memory).
# But there is no point requiring it unless in test
# Also maybe add it inside a "group"
gem "contracted_value", require: false
```And then execute:
```bash
$ bundle
```Or install it yourself as:
```bash
$ gem install contracted_value
```## Usage
The examples below might contain some of my habbits,
like including [`contracts.ruby`](https://github.com/egonSchiele/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```ruby
module ::Geometry
endmodule ::Geometry::LocationRange
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtinattribute(
:latitude,
contract: Numeric,
)
attribute(
:longitude,
contract: Numeric,
)attribute(
:radius_in_meter,
contract: And[Numeric, Send[:positive?]],
)attribute(
:latitude,
) # => error, declared already
end
endlocation_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`
```ruby
module ::Geometry
endmodule ::Geometry::Location
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtinattribute(
:latitude,
contract: Numeric,
)
attribute(
:longitude,
contract: Numeric,
)
end
endmodule ::Geometry::LocationRange
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtinattribute(
:latitude,
contract: Numeric,
)
attribute(
:longitude,
contract: Numeric,
)attribute(
:radius_in_meter,
contract: Maybe[And[Numeric, Send[:positive?]]],
default_value: nil,
)
end
endlocation = ::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````ruby
class Pokemon < ::ContractedValue::Value
attribute(:name)
attribute(:type)
endclass Pikachu < ::Pokemon
attribute(:name, default_value: "Pikachu")
attribute(:type, default_value: "Thunder")
end# Ya I love using pokemon as examples, problem?
pikachu = Pikachu.new(name: "PikaPika")
pikachu.name #=> "PikaPika"
pikachu.type #=> "Thunder"pokemon1 = Pokemon.new(pikachu)
pokemon1.name #=> "PikaPika"
pokemon1.type #=> "Thunder"pokemon2 = Pokemon.new(pikachu.to_h.merge(name: "Piak"))
pokemon2.name #=> "Piak"
pokemon2.type #=> "Thunder"
```### 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`](https://github.com/egonSchiele/contracts.ruby) contract)
Passing input value violating an attribute's contract would cause an error```ruby
class YetAnotherRationalNumber < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtinattribute(
:numerator,
contract: ::Integer,
)
attribute(
:denominator,
contract: And[::Integer, Not[Send[:zero?]]],
)
endYetAnotherRationalNumber.new(
numerator: 1,
denominator: 0,
) # => Error```
#### 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```ruby
module ::WhatIsThis
class Entry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtinattribute(
:something_required,
)
attribute(
:something_optional,
default_value: nil,
)
attribute(
:something_with_error,
contract: NatPos,
default_value: 0,
) # => error
end
endWhatIsThis::Entry.new(
something_required: 123,
).something_optional # => nil
```### Object Freezing
All input values are frozen using [`ice_nine`](https://github.com/dkubb/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)```ruby
class SomeDataEntry < ::ContractedValue::Value
include ::Contracts::Core
include ::Contracts::Builtinattribute(
: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
endentry = SomeDataEntry.new(
cold_hash: {a: {b: 0}},
cool_hash: {a: {b: 0}},
warm_hash: {a: {b: 0}},
)entry.cold_hash[:a].delete(:b) # => `FrozenError`
entry.cool_hash[:a].delete(:b) # => fine
entry.cool_hash.delete(:a) # => `FrozenError`entry.warm_hash.delete(:a) # => fine
entry.cached_hash # => `FrozenError`
```
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?
```ruby
class Pokemon < ::ContractedValue::Value
attribute(:name)
endclass Pikachu < ::Pokemon
attribute(:type, default_value: "Thunder")
end# Ya I love using pokemon as examples, problem?
pikachu = Pikachu.new(name: "PikaPika")
pikachu.name #=> "PikaPika"
pikachu.type #=> "Thunder"
```#### All existing attributes can be redeclared
Within the same class you cannot redefine an attribute
But in subclasses you can
```ruby
class Pokemon < ::ContractedValue::Value
attribute(:name)
endclass Pikachu < ::Pokemon
include ::Contracts::Core
include ::Contracts::Builtinattribute(
:name,
contract: And[::String, Not[Send[:empty?]]],
default_value: String.new("Pikachu"),
refrigeration_mode: :none,
)
end# Ya I love using pokemon as examples, problem?
Pikachu.new.name # => "Pikachu"
Pikachu.new.name.frozen? # => true, as mentioned above default value are always deeply frozen
Pikachu.new(name: "Pikaaaachuuu").name.frozen? # => false
```## 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.- [values](https://github.com/tcrayford/values)
- [active_attr](https://github.com/cgriego/active_attr)
- [dry-struct](https://github.com/dry-rb/dry-struct)### [values](https://github.com/tcrayford/values)
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### [active_attr](https://github.com/cgriego/active_attr)
Got similar issue as `values`### [dry-struct](https://github.com/dry-rb/dry-struct)
Seems more suitable for form objects instead of just value objects (for me)## Contributing
1. Fork it ( https://github.com/PikachuEXE/contracted_value/fork )
2. Create your branch (Preferred to be prefixed with `feature`/`fix`/other sensible prefixes)
3. Commit your changes (No version related changes will be accepted)
4. Push to the branch on your forked repo
5. Create a new Pull Request