contract_value_object 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.yml +6 -0
- data/.gitignore +5 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +47 -0
- data/README.md +125 -0
- data/Rakefile +6 -0
- data/bin/console +49 -0
- data/contract_value_object.gemspec +29 -0
- data/lib/contract_value_object.rb +124 -0
- data/lib/contract_value_object/awesome_print.rb +22 -0
- data/lib/contract_value_object/definition_error.rb +19 -0
- data/lib/contract_value_object/error_formatter.rb +27 -0
- data/lib/contract_value_object/version.rb +3 -0
- metadata +147 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 77de384cc319e76a374dd34b585910711398f1d8
|
4
|
+
data.tar.gz: 90d884f8cf416c86835d6a4a0cf699053c027c6e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fb3c2c2247c8c8a80a399f867cf3227f19e3adb1f9b65d6b7ac00c26f876abc612e428764590e2fcd7e75c3627fa769f9c404412d893e71d87da0f68827ec3b4
|
7
|
+
data.tar.gz: 4c55b6edbc61e982c69c097c1f0b72a28e9008e45f8be7fb2823254cfa68c6f0343c2c0b5e915ebe0fa49386bca551ada5ea9d0a39cfa746ec4ec72dc0c9572f
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
contract_value_object
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.3.4
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# 0.1.0
|
2
|
+
- Remove Gusto-internal to prepare for open sourcing
|
3
|
+
- Add more tests to increase coverage and make usage more clear
|
4
|
+
|
5
|
+
# 0.0.4
|
6
|
+
- Add better implementation for `awesome_print` based upon the gem's guidelines (metaprogramming!). See their description for more information: `https://github.com/awesome-print/awesome_print/blob/4564fd74721562cbef2443f7d97109bf9192343d/lib/awesome_print/formatter.rb#L33`
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at kirill@zenpayroll.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Dockerfile
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
contract_value_object (0.1.0)
|
5
|
+
contracts
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
awesome_print (1.8.0)
|
11
|
+
contracts (0.16.0)
|
12
|
+
diff-lcs (1.3)
|
13
|
+
docile (1.3.1)
|
14
|
+
json (2.2.0)
|
15
|
+
rake (10.5.0)
|
16
|
+
rspec (3.8.0)
|
17
|
+
rspec-core (~> 3.8.0)
|
18
|
+
rspec-expectations (~> 3.8.0)
|
19
|
+
rspec-mocks (~> 3.8.0)
|
20
|
+
rspec-core (3.8.0)
|
21
|
+
rspec-support (~> 3.8.0)
|
22
|
+
rspec-expectations (3.8.3)
|
23
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
24
|
+
rspec-support (~> 3.8.0)
|
25
|
+
rspec-mocks (3.8.0)
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
27
|
+
rspec-support (~> 3.8.0)
|
28
|
+
rspec-support (3.8.0)
|
29
|
+
simplecov (0.16.1)
|
30
|
+
docile (~> 1.1)
|
31
|
+
json (>= 1.8, < 3)
|
32
|
+
simplecov-html (~> 0.10.0)
|
33
|
+
simplecov-html (0.10.2)
|
34
|
+
|
35
|
+
PLATFORMS
|
36
|
+
ruby
|
37
|
+
|
38
|
+
DEPENDENCIES
|
39
|
+
awesome_print
|
40
|
+
bundler (~> 1.15.4)
|
41
|
+
contract_value_object!
|
42
|
+
rake (~> 10.0)
|
43
|
+
rspec (~> 3.0)
|
44
|
+
simplecov
|
45
|
+
|
46
|
+
BUNDLED WITH
|
47
|
+
1.15.4
|
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# Contract Value Object 
|
2
|
+
|
3
|
+
Contract value object are designed for two purposes:
|
4
|
+
1. To create objects that act like values (in the same way that two ruby `Date` objects are the same if their year, month, and date are the same).
|
5
|
+
2. To guarantee that objects can only be created with correct values. That means that no invalid value types should be able to exist.
|
6
|
+
|
7
|
+
The inspiration for the value object part is taken from [Martin Fowler's discussion of the matter](https://martinfowler.com/bliki/ValueObject.html).
|
8
|
+
It's implemented on top of the [Contracts gem](http://egonschiele.github.io/contracts.ruby/) that allows us to define the accepted values for each attribute.
|
9
|
+
|
10
|
+
In other words it's much like this GIF:
|
11
|
+
|
12
|
+
<img width="400" src="https://media3.giphy.com/media/BHeCjdyGJck6c/source.gif">
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'contract_value_object'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install contract_value_object
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
There are two main methods that are exposed when constructing a Contract Value Object:
|
33
|
+
- `attributes` - allows you to define all of the attributes that you expect to be passed in
|
34
|
+
- input: hash, where the keys are the names of the attributes and the keys are the contract that must be met
|
35
|
+
- `defaults` - allows you to define the default values that will be set if a value will not be passed in. All of these should explicitly `Optional` contracts in the attributes.
|
36
|
+
- input: hash, where the keys are the attributes that should be default and the keys are the default values
|
37
|
+
|
38
|
+
Additionally, Contract value object expects that you can redefine the setters for the arguments.
|
39
|
+
These setters will still check their contracts. They do, however, give us two abilities:
|
40
|
+
1. Perform more complicated validations
|
41
|
+
2. Allow transformations of the original data.
|
42
|
+
|
43
|
+
Example usage:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
class Truck < ContractValueObject
|
47
|
+
class Wheel < ContractValueObject
|
48
|
+
attributes(
|
49
|
+
size: Optional[Enum[:xs, :s, :m, :l, :xl]],
|
50
|
+
)
|
51
|
+
|
52
|
+
defaults(
|
53
|
+
size: :m,
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
attributes(
|
58
|
+
wheels: ArrayOf[Wheel],
|
59
|
+
color: Enum[:red, :white, :blue],
|
60
|
+
model: String,
|
61
|
+
)
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def wheels=(wheels)
|
66
|
+
# transform all the inputs into wheels
|
67
|
+
if wheels.all? { |wheel| wheel.is_a?(Symbol) }
|
68
|
+
wheels = wheels.map do |size|
|
69
|
+
Wheel.new(size: size)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# make sure that wheels behave rationally
|
74
|
+
raise ArgumentError, 'All of the wheels should be the same size' if wheels.uniq(&:size).count != 1
|
75
|
+
|
76
|
+
@wheels = wheels
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
truck = Truck.new(
|
81
|
+
wheels: [Truck::Wheel.new, Truck::Wheel.new, Truck::Wheel.new, Truck::Wheel.new],
|
82
|
+
color: :red,
|
83
|
+
model: 'Ford F150',
|
84
|
+
)
|
85
|
+
```
|
86
|
+
|
87
|
+
One final thing to note, is that contract value objects will present all of the errors to the user at once.
|
88
|
+
Suppose we construct an invalid truck:
|
89
|
+
```ruby
|
90
|
+
Truck.new(
|
91
|
+
wheels: [:s, :m, :m, :l],
|
92
|
+
model: :ford
|
93
|
+
)
|
94
|
+
```
|
95
|
+
|
96
|
+
We'll get back a full error that says:
|
97
|
+
```
|
98
|
+
ContractValueObject::DefinitionError:
|
99
|
+
1. `color`: Missing attribute `color`.
|
100
|
+
Expected: #<Contracts::Builtin::Enum:0x007fcdff85c148 @vals=[:red, :white, :blue]>
|
101
|
+
2. `wheels`: All of the wheels should be the same size
|
102
|
+
3. `model`: Attribute `model` does not conform to its contract.
|
103
|
+
Expected: String
|
104
|
+
Actual: Symbol (:ford)
|
105
|
+
```
|
106
|
+
|
107
|
+
## Development
|
108
|
+
|
109
|
+
Run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
110
|
+
|
111
|
+
To install this gem onto your local environment, run `bundle exec rake install`.
|
112
|
+
To release a new version:
|
113
|
+
1. `rake release`
|
114
|
+
|
115
|
+
## Contributing
|
116
|
+
|
117
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Gusto/contract_value_object. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
118
|
+
|
119
|
+
## License
|
120
|
+
|
121
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
122
|
+
|
123
|
+
## Code of Conduct
|
124
|
+
|
125
|
+
Everyone interacting in the Contract Value Object project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Gusto/contract_value_object/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'irb'
|
4
|
+
require 'irb/completion'
|
5
|
+
require 'awesome_print'
|
6
|
+
require 'bundler/setup'
|
7
|
+
Bundler.require
|
8
|
+
|
9
|
+
class Truck < ContractValueObject
|
10
|
+
class Wheel < ContractValueObject
|
11
|
+
attributes(
|
12
|
+
size: Optional[Enum[:xs, :s, :m, :l, :xl]],
|
13
|
+
)
|
14
|
+
|
15
|
+
defaults(
|
16
|
+
size: :m,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
attributes(
|
21
|
+
color: Enum[:red, :white, :blue],
|
22
|
+
model: String,
|
23
|
+
wheels: ArrayOf[Wheel],
|
24
|
+
)
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def wheels=(wheels)
|
29
|
+
# transform all the inputs into wheels
|
30
|
+
if wheels.all? { |wheel| wheel.is_a?(Symbol) }
|
31
|
+
wheels = wheels.map do |size|
|
32
|
+
Wheel.new(size: size)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# make sure that wheels behave rationally
|
37
|
+
raise ArgumentError, 'All of the wheels should be the same size' if wheels.uniq(&:size).count != 1
|
38
|
+
|
39
|
+
@wheels = wheels
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
TRUCK = Truck.new(
|
44
|
+
wheels: [Truck::Wheel.new, Truck::Wheel.new, Truck::Wheel.new, Truck::Wheel.new],
|
45
|
+
color: :red,
|
46
|
+
model: 'Ford F150',
|
47
|
+
)
|
48
|
+
|
49
|
+
IRB.start
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'contract_value_object/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'contract_value_object'
|
8
|
+
spec.version = ContractValueObject::VERSION
|
9
|
+
spec.authors = ['Kirill Klimuk']
|
10
|
+
spec.email = ['kklimuk@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = %q{Validate that your objects have the right inputs.}
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
16
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
end
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'contracts'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.15.4'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
27
|
+
spec.add_development_dependency 'awesome_print'
|
28
|
+
spec.add_development_dependency 'simplecov'
|
29
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'contracts'
|
2
|
+
require 'contract_value_object/definition_error'
|
3
|
+
require 'contract_value_object/error_formatter'
|
4
|
+
require 'contract_value_object/awesome_print'
|
5
|
+
|
6
|
+
class ContractValueObject
|
7
|
+
include Contracts
|
8
|
+
|
9
|
+
class << self
|
10
|
+
Contract Maybe[HashOf[Symbol, Any]] => HashOf[Symbol, Any]
|
11
|
+
def attributes(attributes = nil)
|
12
|
+
if attributes.nil?
|
13
|
+
return @attributes if instance_variable_defined?(:@attributes)
|
14
|
+
raise ArgumentError, 'Calling for attributes without having defined them.'
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor(*attributes.keys)
|
18
|
+
attr_writers = attributes.keys.map { |attribute| :"#{attribute}=" }
|
19
|
+
private *attr_writers
|
20
|
+
|
21
|
+
@attributes = attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
Contract Maybe[HashOf[Symbol, Any]] => HashOf[Symbol, Any]
|
25
|
+
def defaults(defaults = nil)
|
26
|
+
return @defaults ||= {} if defaults.nil?
|
27
|
+
|
28
|
+
unexpected_defaults = defaults.keys - attributes.keys
|
29
|
+
unless unexpected_defaults.empty?
|
30
|
+
raise ArgumentError, "Unexpected defaults are set: #{unexpected_defaults.map(&:to_s).join(', ')}"
|
31
|
+
end
|
32
|
+
|
33
|
+
non_optional = defaults.select do |attribute, _|
|
34
|
+
!attributes.fetch(attribute).is_a?(Contracts::Optional)
|
35
|
+
end
|
36
|
+
raise ArgumentError, "Unexpected non-optional defaults: #{non_optional.keys.join(', ')}" unless non_optional.empty?
|
37
|
+
|
38
|
+
@defaults = defaults
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_writer :error_presenter
|
42
|
+
|
43
|
+
Contract RespondTo[:contract_failure, :missing, :unexpected]
|
44
|
+
def error_presenter
|
45
|
+
@error_presenter ||= self::ErrorFormatter.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
Contract HashOf[Symbol, Any] => Any
|
50
|
+
def initialize(**attributes)
|
51
|
+
error_presenter = self.class.error_presenter
|
52
|
+
class_attributes = self.class.attributes
|
53
|
+
defaults = self.class.defaults
|
54
|
+
error_message_class = DefinitionError::ErrorMessage
|
55
|
+
|
56
|
+
# determine attributes that were not passed in but should have been
|
57
|
+
missing_attributes = class_attributes.keys - attributes.keys - defaults.keys
|
58
|
+
missing_attribute_errors = missing_attributes.flat_map do |attribute|
|
59
|
+
next([]) if class_attributes[attribute].is_a?(Contracts::Optional)
|
60
|
+
error_message_class.new(attribute, error_presenter.missing(attribute, class_attributes.fetch(attribute)))
|
61
|
+
end
|
62
|
+
|
63
|
+
# determine extraneous attributes that should not have been passed in
|
64
|
+
unexpected_attributes = attributes.keys - class_attributes.keys
|
65
|
+
unexpected_attribute_errors = unexpected_attributes.map do |attribute|
|
66
|
+
error_message_class.new(attribute, error_presenter.unexpected(attribute))
|
67
|
+
end
|
68
|
+
|
69
|
+
# set attributes on the object and raise if they do not obey the contract
|
70
|
+
setter_errors = defaults.merge(attributes).flat_map do |attribute, value|
|
71
|
+
next([]) if unexpected_attributes.include?(attribute)
|
72
|
+
|
73
|
+
begin
|
74
|
+
contract = class_attributes.fetch(attribute)
|
75
|
+
contract.within_opt_hash! if contract.is_a?(Contracts::Optional)
|
76
|
+
# begin support customized setter
|
77
|
+
send("#{attribute}=", value)
|
78
|
+
set_value = instance_variable_get("@#{attribute}")
|
79
|
+
# end support for customized setter
|
80
|
+
if Contract.valid?(set_value, contract)
|
81
|
+
[]
|
82
|
+
else
|
83
|
+
raise ArgumentError, error_presenter.contract_failure(attribute, set_value, contract)
|
84
|
+
end
|
85
|
+
rescue self.class::DefinitionError => error
|
86
|
+
error_message_class.new(attribute, error.message.gsub("\n", "\n\t"))
|
87
|
+
rescue StandardError => error
|
88
|
+
error_message_class.new(attribute, error.message)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
errors = missing_attribute_errors + unexpected_attribute_errors + setter_errors
|
93
|
+
return if errors.empty?
|
94
|
+
|
95
|
+
raise DefinitionError, errors
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_h
|
99
|
+
key_value_pairs = self.class.attributes.map do |attribute, _type|
|
100
|
+
[attribute, public_send(attribute)]
|
101
|
+
end
|
102
|
+
|
103
|
+
key_value_pairs.to_h
|
104
|
+
end
|
105
|
+
|
106
|
+
# treating contract value objects with the same values as the same object for hashes and sets
|
107
|
+
def hash
|
108
|
+
to_h.hash
|
109
|
+
end
|
110
|
+
|
111
|
+
def ==(other)
|
112
|
+
return super(other) unless self.class == other.class
|
113
|
+
|
114
|
+
self.class.attributes.all? do |attribute, _type|
|
115
|
+
public_send(attribute) == other.public_send(attribute)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :eql?, :==
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
"#{self.class} #{to_h.inspect}"
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
if defined?(::AwesomePrint)
|
2
|
+
module ::AwesomePrint::ContractValueObject
|
3
|
+
def self.included(base)
|
4
|
+
base.send :alias_method, :cast_without_cvo, :cast
|
5
|
+
base.send :alias_method, :cast, :cast_with_cvo
|
6
|
+
end
|
7
|
+
|
8
|
+
def cast_with_cvo(object, type)
|
9
|
+
cast = cast_without_cvo(object, type)
|
10
|
+
if (defined?(::ContractValueObject)) && (object.is_a?(::ContractValueObject))
|
11
|
+
cast = :contract_value_object
|
12
|
+
end
|
13
|
+
cast
|
14
|
+
end
|
15
|
+
|
16
|
+
def awesome_contract_value_object(object)
|
17
|
+
"#{object.class} #{awesome_hash(object.to_h)}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
AwesomePrint::Formatter.send(:include, AwesomePrint::ContractValueObject)
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class ContractValueObject
|
2
|
+
class DefinitionError < ArgumentError
|
3
|
+
include Contracts
|
4
|
+
|
5
|
+
ErrorMessage = Struct.new(:attribute, :message)
|
6
|
+
attr_reader :error_messages
|
7
|
+
|
8
|
+
Contract ArrayOf[ErrorMessage] => Any
|
9
|
+
def initialize(error_messages)
|
10
|
+
@error_messages = error_messages
|
11
|
+
message_components = error_messages.each_with_index.map do |error, index|
|
12
|
+
"#{index + 1}. `#{error.attribute}`: #{error.message}"
|
13
|
+
end
|
14
|
+
|
15
|
+
message = ['', *message_components].join("\n")
|
16
|
+
super message
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ContractValueObject
|
2
|
+
class ErrorFormatter
|
3
|
+
include Contracts
|
4
|
+
|
5
|
+
Contract Symbol, Any, Any => String
|
6
|
+
def contract_failure(attribute, value, contract)
|
7
|
+
[
|
8
|
+
"Attribute `#{attribute}` does not conform to its contract.",
|
9
|
+
"\tExpected: #{Contracts::Formatters::Expected.new(contract).contract}",
|
10
|
+
"\tActual: #{value.class.name} (#{value.inspect})",
|
11
|
+
].join("\n")
|
12
|
+
end
|
13
|
+
|
14
|
+
Contract Symbol, Any => String
|
15
|
+
def missing(attribute, contract)
|
16
|
+
[
|
17
|
+
"Missing attribute `#{attribute}`.",
|
18
|
+
"\tExpected: #{Contracts::Formatters::Expected.new(contract).contract}",
|
19
|
+
].join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
Contract Symbol => String
|
23
|
+
def unexpected(attribute)
|
24
|
+
"Unexpected attribute `#{attribute}`."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contract_value_object
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kirill Klimuk
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: contracts
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.15.4
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.15.4
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: awesome_print
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- kklimuk@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".buildkite/pipeline.yml"
|
105
|
+
- ".gitignore"
|
106
|
+
- ".ruby-gemset"
|
107
|
+
- ".ruby-version"
|
108
|
+
- ".travis.yml"
|
109
|
+
- CHANGELOG.md
|
110
|
+
- CODE_OF_CONDUCT.md
|
111
|
+
- Dockerfile
|
112
|
+
- Gemfile
|
113
|
+
- Gemfile.lock
|
114
|
+
- README.md
|
115
|
+
- Rakefile
|
116
|
+
- bin/console
|
117
|
+
- contract_value_object.gemspec
|
118
|
+
- lib/contract_value_object.rb
|
119
|
+
- lib/contract_value_object/awesome_print.rb
|
120
|
+
- lib/contract_value_object/definition_error.rb
|
121
|
+
- lib/contract_value_object/error_formatter.rb
|
122
|
+
- lib/contract_value_object/version.rb
|
123
|
+
homepage:
|
124
|
+
licenses:
|
125
|
+
- MIT
|
126
|
+
metadata: {}
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 2.6.14
|
144
|
+
signing_key:
|
145
|
+
specification_version: 4
|
146
|
+
summary: Validate that your objects have the right inputs.
|
147
|
+
test_files: []
|