interactor-contracts 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/CHANGELOG.md +25 -0
- data/CONTRIBUTING.md +55 -0
- data/LICENSE.md +21 -0
- data/README.md +179 -0
- data/interactor-contracts.gemspec +25 -0
- data/lib/interactor/contracts/breach.rb +60 -0
- data/lib/interactor/contracts/breach_set.rb +46 -0
- data/lib/interactor/contracts/contract.rb +116 -0
- data/lib/interactor/contracts/dsl.rb +150 -0
- data/lib/interactor/contracts/errors.rb +10 -0
- data/lib/interactor/contracts/outcome.rb +80 -0
- data/lib/interactor/contracts/terms.rb +51 -0
- data/lib/interactor/contracts/version.rb +5 -0
- data/lib/interactor/contracts.rb +47 -0
- data/lib/interactor-contracts.rb +2 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f4b6a4b9a1473818e3af68c266630d617bf119a6
|
4
|
+
data.tar.gz: 21a9105f05f823a824545aa231669c7215b6af34
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab017960b801b150397d026f4754eb231e3128c96e7119f1a3764d3bcc3cdf70cf2640ea630558b4433761f15be64c6a3200a3bcf95f36bc8db3f564be5c8082
|
7
|
+
data.tar.gz: 76c7ba6f0390f18ffebac61dfb14863d9f0920255c930dce9a4dbb3e22f498b9690ae115453a2075d3baf14345a1f6881b3b41072520e6778980fc30d7ec795b
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning 2.0.0][semver]. Any violations of this scheme are considered to be bugs.
|
4
|
+
|
5
|
+
[semver]: http://semver.org/spec/v2.0.0.html
|
6
|
+
|
7
|
+
## [0.1.0] - 2017-02-25
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
* [#2](https://github.com/michaelherold/interactor-contracts/pull/2): Support for dry-validations ~> 0.10 - [@andrewaguiar](https://github.com/andrewaguiar).
|
12
|
+
* [#7](https://github.com/michaelherold/interactor-contracts/pull/7): More ergonomic error handling via `BreachSet#to_hash` - [@michaelherold](https://github.com/michaelherold).
|
13
|
+
|
14
|
+
### Fixed
|
15
|
+
|
16
|
+
* [#5](https://github.com/michaelherold/interactor-contracts/pull/5): Refactored code base to prepare for the release of v0.1.0 - [@michaelherold](https://github.com/michaelherold).
|
17
|
+
|
18
|
+
### Miscellaneous
|
19
|
+
|
20
|
+
* [#3](https://github.com/michaelherold/interactor-contracts/pull/3): Updated all dependencies to fix build process - [@michaelherold](https://github.com/michaelherold).
|
21
|
+
* [#4](https://github.com/michaelherold/interactor-contracts/pull/4): Added Danger as a collaboration tool - [@michaelherold](https://github.com/michaelherold).
|
22
|
+
* [#6](https://github.com/michaelherold/interactor-contracts/pull/6): Updated the README - [@michaelherold](https://github.com/michaelherold).
|
23
|
+
|
24
|
+
|
25
|
+
[0.1.0]: https://github.com/michaelherold/interactor-contracts/tree/v0.1.0
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
In the spirit of [free software], **everyone** is encouraged to help improve this project. Here are some ways *you* can contribute:
|
4
|
+
|
5
|
+
* Use alpha, beta, and pre-release versions.
|
6
|
+
* Report bugs.
|
7
|
+
* Suggest new features.
|
8
|
+
* Write or edit documentation.
|
9
|
+
* Write specifications.
|
10
|
+
* Write code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace).
|
11
|
+
* Refactor code.
|
12
|
+
* Fix [issues].
|
13
|
+
* Review patches.
|
14
|
+
|
15
|
+
[free software]: http://www.fsf.org/licensing/essays/free-sw.html
|
16
|
+
[issues]: https://github.com/michaelherold/interactor-contracts/issues
|
17
|
+
|
18
|
+
## Submitting an Issue
|
19
|
+
|
20
|
+
We use the [GitHub issue tracker][issues] to track bugs and features. Before submitting a bug report or feature request, check to make sure it hasn't already been submitted.
|
21
|
+
|
22
|
+
When submitting a bug report, please include a [Gist] that includes a stack trace and any details that may be necessary to reproduce the bug, including your gem version, Ruby version, and operating system.
|
23
|
+
|
24
|
+
Ideally, a bug report should include a pull request with failing specs.
|
25
|
+
|
26
|
+
[Gist]: https://gist.github.com
|
27
|
+
|
28
|
+
## Submitting a Pull Request
|
29
|
+
|
30
|
+
1. [Fork the repository].
|
31
|
+
2. [Create a topic branch].
|
32
|
+
3. Add specs for your unimplemented feature or bug fix.
|
33
|
+
4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
|
34
|
+
5. Implement your feature or bug fix.
|
35
|
+
6. Run `bundle exec rake`. If your specs or any of the linters fail, return to step 5.
|
36
|
+
7. Open `coverage/index.html`. If your changes are not completely covered by your tests, return to step 3.
|
37
|
+
8. Add documentation for your feature or bug fix.
|
38
|
+
9. Run `bundle exec inch`. If your changes are below a B in documentation, go back to step 8.
|
39
|
+
10. Commit and push your changes.
|
40
|
+
11. [Submit a pull request].
|
41
|
+
|
42
|
+
[Create a topic branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/
|
43
|
+
[Fork the repository]: http://learn.github.com/p/branching.html
|
44
|
+
[Submit a pull request]: https://help.github.com/articles/creating-a-pull-request/
|
45
|
+
|
46
|
+
## Tools to Help You Succeed
|
47
|
+
|
48
|
+
After checking out the repository, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
49
|
+
|
50
|
+
When writing code, you can use the helper application [Guard] to automatically run tests and coverage tools whenever you modify and save a file. This helps to eliminate the tedium of running tests manually and reduces the chance that you will accidentally forget to run the tests. To use Guard, run `bundle exec guard`.
|
51
|
+
|
52
|
+
Before committing code, run `bundle exec rake` to check that the code conforms to the style guidelines of the project, that all of the tests are green (if you're writing a feature; if you're only submitting a failing test, then it does not have to pass!), and that the changes are sufficiently documented.
|
53
|
+
|
54
|
+
[Guard]: http://guardgem.org
|
55
|
+
[rubygems]: https://rubygems.org
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Michael Herold
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# Interactor::Contracts
|
2
|
+
|
3
|
+
[][travis]
|
4
|
+
[][codeclimate]
|
5
|
+
[][inch]
|
6
|
+
|
7
|
+
[codeclimate]: https://codeclimate.com/github/michaelherold/interactor-contracts
|
8
|
+
[inch]: http://inch-ci.org/github/michaelherold/interactor-contracts
|
9
|
+
[travis]: https://travis-ci.org/michaelherold/interactor-contracts
|
10
|
+
|
11
|
+
Interactor::Contracts is an extension to the [interactor] gem that gives you
|
12
|
+
the ability to specify the expectations (expected inputs) and assurances
|
13
|
+
(expected outputs) of your interactors.
|
14
|
+
|
15
|
+
[interactor]: https://github.com/collectiveidea/interactor
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'interactor-contracts'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install interactor-contracts
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
Let's extend the sample `AuthenticateUser` from the Interactor examples with a
|
36
|
+
contract that specifies its expectations and assurances.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
class AuthenticateUser
|
40
|
+
include Interactor
|
41
|
+
include Interactor::Contracts
|
42
|
+
|
43
|
+
expects do
|
44
|
+
required(:email).filled
|
45
|
+
required(:password).filled
|
46
|
+
end
|
47
|
+
|
48
|
+
assures do
|
49
|
+
required(:user).filled
|
50
|
+
required(:token).filled
|
51
|
+
end
|
52
|
+
|
53
|
+
on_breach do |breaches|
|
54
|
+
context.fail!(breaches)
|
55
|
+
end
|
56
|
+
|
57
|
+
def call
|
58
|
+
if user = User.authenticate(context.email, context.password)
|
59
|
+
context.user = user
|
60
|
+
context.token = user.secret_token
|
61
|
+
else
|
62
|
+
context.fail!(:message => "authenticate_user.failure")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
The `expects` block defines the expectations: the expected attributes of the
|
69
|
+
context prior to the interactor running, along with any predicates that further
|
70
|
+
constrain the input.
|
71
|
+
|
72
|
+
The `assures` block defines the assurances: the expected attributes of the
|
73
|
+
context after the interactor runs and successfully completes, along with any
|
74
|
+
predicates the further constrain the output.
|
75
|
+
|
76
|
+
Because interactors can have transitive dependencies through the use of
|
77
|
+
organizers, any other inputs or outputs are ignored from the perspective of
|
78
|
+
the contract and are passed along to the outgoing (successful) context.
|
79
|
+
|
80
|
+
Both `expects` and `assures` wrap [dry-validation], so you can use any
|
81
|
+
predicates defined in it to describe the expected inputs and outputs of your
|
82
|
+
interactor.
|
83
|
+
|
84
|
+
To hook into a failed expectation or assurance, you can use the `on_breach`
|
85
|
+
method to defined a breach handler. It should take a 1-arity block that expects
|
86
|
+
an array of `Breach` objects. These objects have a `property` attribute that
|
87
|
+
will give you the key that's in breach of its contract. Breaches also have a
|
88
|
+
`messages` attribute that gives the reasons that property is in breach.
|
89
|
+
|
90
|
+
By default, when an `on_breach` consequence is not specified, the contract will
|
91
|
+
`#fail!` the `Interactor::Context` with the keys that are in breach and arrays
|
92
|
+
of messages about what the breaches are.
|
93
|
+
|
94
|
+
For example, the above interactor acts as follows:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
result = AuthenticateUser.call({})
|
98
|
+
#=> #<Interactor::Context email=["email is missing"], password=["password is missing"]>
|
99
|
+
|
100
|
+
result.failure? #=> true
|
101
|
+
```
|
102
|
+
|
103
|
+
[dry-validation]: https://github.com/dryrb/dry-validation
|
104
|
+
|
105
|
+
## Development
|
106
|
+
|
107
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
108
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
109
|
+
prompt that will allow you to experiment.
|
110
|
+
|
111
|
+
When writing code, you can use the helper application [Guard][guard] to
|
112
|
+
automatically run tests and coverage tools whenever you modify and save a file.
|
113
|
+
This helps to eliminate the tedium of running tests manually and reduces the
|
114
|
+
change that you will accidentally forget to run the tests. To use Guard, run
|
115
|
+
`bundle exec guard`.
|
116
|
+
|
117
|
+
Before committing code, run `rake` to check that the code conforms to the style
|
118
|
+
guidelines of the project, that all of the tests are green (if you're writing a
|
119
|
+
feature; if you're only submitting a failing test, then it does not have to
|
120
|
+
pass!), and that the changes are sufficiently documented.
|
121
|
+
|
122
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
123
|
+
release a new version, update the version number in `version.rb`, and then run
|
124
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
125
|
+
git commits and tags, and push the `.gem` file to [rubygems.org][rubygems].
|
126
|
+
|
127
|
+
[guard]: http://guardgem.org
|
128
|
+
[rubygems]: https://rubygems.org
|
129
|
+
|
130
|
+
## Contributing
|
131
|
+
|
132
|
+
Bug reports and pull requests are welcome on GitHub at
|
133
|
+
https://github.com/michaelherold/interactor-contracts.
|
134
|
+
|
135
|
+
## Supported Ruby Versions
|
136
|
+
|
137
|
+
This library aims to support and is [tested against][travis] the following Ruby
|
138
|
+
versions:
|
139
|
+
|
140
|
+
* Ruby 2.1
|
141
|
+
* Ruby 2.2
|
142
|
+
* Ruby 2.3
|
143
|
+
* Ruby 2.4
|
144
|
+
* JRuby 9.1
|
145
|
+
|
146
|
+
If something doesn't work on one of these versions, it's a bug.
|
147
|
+
|
148
|
+
This library may inadvertently work (or seem to work) on other Ruby versions,
|
149
|
+
however support will only be provided for the versions listed above.
|
150
|
+
|
151
|
+
If you would like this library to support another Ruby version or
|
152
|
+
implementation, you may volunteer to be a maintainer. Being a maintainer
|
153
|
+
entails making sure all tests run and pass on that implementation. When
|
154
|
+
something breaks on your implementation, you will be responsible for providing
|
155
|
+
patches in a timely fashion. If critical issues for a particular implementation
|
156
|
+
exist at the time of a major release, support for that Ruby version may be
|
157
|
+
dropped.
|
158
|
+
|
159
|
+
## Versioning
|
160
|
+
|
161
|
+
This library aims to adhere to [Semantic Versioning 2.0.0][semver]. Violations
|
162
|
+
of this scheme should be reported as bugs. Specifically, if a minor or patch
|
163
|
+
version is released that breaks backward compatibility, that version should be
|
164
|
+
immediately yanked and/or a new version should be immediately released that
|
165
|
+
restores compatibility. Breaking changes to the public API will only be
|
166
|
+
introduced with new major versions. As a result of this policy, you can (and
|
167
|
+
should) specify a dependency on this gem using the [Pessimistic Version
|
168
|
+
Constraint][pessimistic] with two digits of precision. For example:
|
169
|
+
|
170
|
+
spec.add_dependency "interactor-contracts", "~> 0.1"
|
171
|
+
|
172
|
+
[pessimistic]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
|
173
|
+
[semver]: http://semver.org/spec/v2.0.0.html
|
174
|
+
|
175
|
+
## License
|
176
|
+
|
177
|
+
The gem is available as open source under the terms of the [MIT License][license].
|
178
|
+
|
179
|
+
[license]: http://opensource.org/licenses/MIT.
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path("../lib/interactor/contracts/version", __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "interactor-contracts"
|
7
|
+
spec.version = Interactor::Contracts::VERSION
|
8
|
+
spec.authors = ["Michael Herold"]
|
9
|
+
spec.email = ["michael.j.herold@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Add contacts to your interactors"
|
12
|
+
spec.description = "Add contacts to your interactors"
|
13
|
+
spec.homepage = "https://github.com/michaelherold/interactor-contracts"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = %w(CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md)
|
17
|
+
spec.files += %w(interactor-contracts.gemspec)
|
18
|
+
spec.files += Dir["lib/**/*.rb"]
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "dry-validation", "~> 0.10"
|
22
|
+
spec.add_dependency "interactor", "~> 3"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Interactor
|
2
|
+
module Contracts
|
3
|
+
# A wrapper for breached contract terms that encapsulates the failed
|
4
|
+
# property and its messages.
|
5
|
+
class Breach
|
6
|
+
# Represents a breach of a contract with its messages
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# Interactor::Contracts::Breach.new(:name, ["name is missing"])
|
10
|
+
#
|
11
|
+
# @api semipublic
|
12
|
+
# @param [Symbol] property the property violated in the contract
|
13
|
+
# @param [Array<String>] messages the messages describing the breach.
|
14
|
+
def initialize(property, messages)
|
15
|
+
@property = property
|
16
|
+
@messages = messages
|
17
|
+
end
|
18
|
+
|
19
|
+
# The messages describing the breach
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# breach = Interactor::Contracts::Breach.new(:name, ["name is missing"])
|
23
|
+
# breach.messages #=> ["name is missing"]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
# @return [Array<String>] the messages describing the breach
|
27
|
+
attr_reader :messages
|
28
|
+
|
29
|
+
# The property violated in the contract
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# breach = Interactor::Contracts::Breach.new(:name, ["name is missing"])
|
33
|
+
# breach.property #=> :name
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
# @return [Symbol] the property violated in the contract
|
37
|
+
attr_reader :property
|
38
|
+
|
39
|
+
# Allows the Breach to be splatted out as arguments to a block
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
# @return [Array<Symbol, Array<String>>]
|
43
|
+
def to_ary
|
44
|
+
[property, messages]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Converts the Breach to an equivalent Hash
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# breach = Interactor::Contracts::Breach.new(:name, ["name is missing"])
|
51
|
+
# breach.to_h #=> {:name => ["name is missing"]}
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
# @return [Hash]
|
55
|
+
def to_h
|
56
|
+
{property => messages}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
module Interactor
|
4
|
+
module Contracts
|
5
|
+
# A simple wrapper around set of breaches of contract constraints
|
6
|
+
class BreachSet < SimpleDelegator
|
7
|
+
# Converts the breach set into a Hash for use with context failing
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class AuthenticateUser
|
11
|
+
# include Interactor
|
12
|
+
# include Interactor::Contracts
|
13
|
+
#
|
14
|
+
# expects do
|
15
|
+
# required(:email).filled
|
16
|
+
# required(:password).filled
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# assures do
|
20
|
+
# required(:user).filled
|
21
|
+
# required(:token).filled
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# on_breach do |breaches|
|
25
|
+
# context.fail!(breaches)
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# result = AuthenticateUser.call({})
|
30
|
+
# #=> #<Interactor::Context email=["email is missing"],
|
31
|
+
# password=["password is missing"]>
|
32
|
+
#
|
33
|
+
# result.failure? #=> true
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
# @return [Hash] a hash with property keys and message values
|
37
|
+
def to_hash
|
38
|
+
reduce({}) do |result, (property, messages)|
|
39
|
+
result[property] = Array(result[property]) | messages
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
alias_method :to_h, :to_hash
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "interactor/contracts/terms"
|
2
|
+
|
3
|
+
module Interactor
|
4
|
+
module Contracts
|
5
|
+
# Contains the assurances, expectations, and consequences of an
|
6
|
+
# interactor's contract.
|
7
|
+
class Contract
|
8
|
+
# Instantiates a new Contract with the given contraints
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# Interactor::Contracts::Contract.new
|
12
|
+
#
|
13
|
+
# @api semipublic
|
14
|
+
# @param [Terms] assurances the Contract's assurances
|
15
|
+
# @param [Terms] expectations the Contract's expectations
|
16
|
+
# @param [Array<#call>] consequences the Contract's consequences
|
17
|
+
# rubocop:disable Metrics/LineLength
|
18
|
+
def initialize(assurances: Terms.new, expectations: Terms.new, consequences: [])
|
19
|
+
@assurances = assurances
|
20
|
+
@consequences = consequences
|
21
|
+
@expectations = expectations
|
22
|
+
end
|
23
|
+
|
24
|
+
# The assurances the Contract will fulfill
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# contract = Interactor::Contracts::Contract.new
|
28
|
+
# contract.assurances #=> <#Interactor::Contracts::Terms>
|
29
|
+
#
|
30
|
+
# @api semipublic
|
31
|
+
# @return [Terms] the terms for the assurances
|
32
|
+
attr_reader :assurances
|
33
|
+
|
34
|
+
# The expectations for arguments passed into the Interactor
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# contract = Interactor::Contracts::Contract.new
|
38
|
+
# contract.expectations #=> <#Interactor::Contracts::Terms>
|
39
|
+
#
|
40
|
+
# @api semipublic
|
41
|
+
# @return [Terms] the terms for the expectations
|
42
|
+
attr_reader :expectations
|
43
|
+
|
44
|
+
# Adds an assurance to the Contract's set of assurances
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# contract = Interactor::Contracts::Contract.new
|
48
|
+
# contract.add_assurance do
|
49
|
+
# required(:name).filled
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @api semipublic
|
53
|
+
# @param [Block] term the assurance as a block of arity 0
|
54
|
+
# @return [void]
|
55
|
+
def add_assurance(&term)
|
56
|
+
assurances.add(&term)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Adds a consequence to the Contract's set of consequences
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# contract = Interactor::Contracts::Contract.new
|
63
|
+
# contract.add_expectation do |breaches|
|
64
|
+
# context[:message] = breaches.first.messages.first
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# @api semipublic
|
68
|
+
# @param [#call] consequence the consequence as a callable with arity 1
|
69
|
+
# @return [void]
|
70
|
+
def add_consequence(consequence)
|
71
|
+
@consequences << consequence
|
72
|
+
end
|
73
|
+
|
74
|
+
# Adds an expectation to the Contract's set of expectations
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# contract = Interactor::Contracts::Contract.new
|
78
|
+
# contract.add_expectation do
|
79
|
+
# required(:name).filled
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# @api semipublic
|
83
|
+
# @param [Block] term the expectation as a block of arity 0
|
84
|
+
# @return [void]
|
85
|
+
def add_expectation(&term)
|
86
|
+
expectations.add(&term)
|
87
|
+
end
|
88
|
+
|
89
|
+
# The consequences for the Contract
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# contract = Interactor::Contracts::Contract.new
|
93
|
+
# contract.consequences #=> [<#Proc>]
|
94
|
+
#
|
95
|
+
# @api semipublic
|
96
|
+
# @return [Array<#call>] the consequences for the Contract
|
97
|
+
def consequences
|
98
|
+
if @consequences.empty?
|
99
|
+
Array(default_consequence)
|
100
|
+
else
|
101
|
+
@consequences
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# The default consequence of a breached contract
|
108
|
+
#
|
109
|
+
# @api private
|
110
|
+
# @return [#call] the default consequence
|
111
|
+
def default_consequence
|
112
|
+
->(breaches) { context.fail!(breaches) }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "interactor/contracts/contract"
|
2
|
+
|
3
|
+
module Interactor
|
4
|
+
module Contracts
|
5
|
+
# Defines the class-level DSL that enables Interactor contracts.
|
6
|
+
module DSL
|
7
|
+
# Defines the assurances of an Interactor and creates an after hook
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# class CreatePerson
|
11
|
+
# include Interactor
|
12
|
+
# include Interactor::Contracts
|
13
|
+
#
|
14
|
+
# assures do
|
15
|
+
# required(:person).filled
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def call
|
19
|
+
# context.person = Person.new
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
# @param [Block] block the block defining the assurances
|
25
|
+
# @return [void]
|
26
|
+
def assures(&block)
|
27
|
+
contract.add_assurance(&block)
|
28
|
+
define_assurances_hook
|
29
|
+
end
|
30
|
+
|
31
|
+
# The Contract to enforce on calls to the Interactor
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# class CreatePerson
|
35
|
+
# include Interactor
|
36
|
+
# include Interactor::Contracts
|
37
|
+
#
|
38
|
+
# assures do
|
39
|
+
# required(:person).filled
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# contracts #=> <#Interactor::Contracts::Contract>
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @api semipublic
|
46
|
+
# @return [Contract]
|
47
|
+
def contract
|
48
|
+
@contract ||= Contract.new
|
49
|
+
end
|
50
|
+
|
51
|
+
# Defines the expectations of an Interactor and creates a before hook
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# class CreatePerson
|
55
|
+
# include Interactor
|
56
|
+
# include Interactor::Contracts
|
57
|
+
#
|
58
|
+
# expects do
|
59
|
+
# required(:name).filled
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# def call
|
63
|
+
# context.person = Person.create!(:name => context.name)
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# CreatePerson.call(:first_name => "Billy").success? #=> false
|
68
|
+
# CreatePerson.call(:name => "Billy").success? #=> true
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
# @param [Block] block the block defining the expectations
|
72
|
+
# @return [void]
|
73
|
+
def expects(&block)
|
74
|
+
contract.add_expectation(&block)
|
75
|
+
define_expectations_hook
|
76
|
+
end
|
77
|
+
|
78
|
+
# Defines a consequence that is called when a contract is breached
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# class CreatePerson
|
82
|
+
# include Interactor
|
83
|
+
# include Interactor::Contracts
|
84
|
+
#
|
85
|
+
# expects do
|
86
|
+
# required(:name).filled
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# on_breach do |breaches|
|
90
|
+
# context.fail!(:message => "invalid_#{breaches.first.property}")
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# def call
|
94
|
+
# context.person = Person.create!(:name => context.name)
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# CreatePerson.call(:first_name => "Billy").message #=> "invalid_name"
|
99
|
+
#
|
100
|
+
# @api public
|
101
|
+
# @param [Block] block the consequence as a block of arity 1.
|
102
|
+
# @return [void]
|
103
|
+
def on_breach(&block)
|
104
|
+
contract.add_consequence(block)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Flags whether the assurances hook has been defined
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
# @return [TrueClass, FalseClass] true if the hook is defined
|
113
|
+
attr_reader :defined_assurances_hook
|
114
|
+
alias_method :defined_assurances_hook?, :defined_assurances_hook
|
115
|
+
|
116
|
+
# Flags whether the expectations hook has been defined
|
117
|
+
#
|
118
|
+
# @api private
|
119
|
+
# @return [TrueClass, FalseClass] true if the hook is defined
|
120
|
+
attr_reader :defined_expectations_hook
|
121
|
+
alias_method :defined_expectations_hook?, :defined_expectations_hook
|
122
|
+
|
123
|
+
# Defines an after hook that validates the Interactor's output
|
124
|
+
#
|
125
|
+
# @api private
|
126
|
+
# @raise [Interactor::Failure] if the input fails to meet its contract.
|
127
|
+
# @return [void]
|
128
|
+
def define_assurances_hook
|
129
|
+
return if defined_assurances_hook?
|
130
|
+
|
131
|
+
after { enforce_contracts(contract.assurances) }
|
132
|
+
|
133
|
+
@defined_assurances_hook = true
|
134
|
+
end
|
135
|
+
|
136
|
+
# Defines a before hook that validates the Interactor's input
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
# @raise [Interactor::Failure] if the input fails to meet its contract.
|
140
|
+
# @return [void]
|
141
|
+
def define_expectations_hook
|
142
|
+
return if defined_expectations_hook?
|
143
|
+
|
144
|
+
before { enforce_contracts(contract.expectations) }
|
145
|
+
|
146
|
+
@defined_expectations_hook = true
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Interactor
|
2
|
+
module Contracts
|
3
|
+
# Base error class used for all errors within the gem.
|
4
|
+
ContractsError = Class.new(StandardError)
|
5
|
+
|
6
|
+
# Raised when trying to include Interactor::Contracts into a class that is
|
7
|
+
# not an Interactor.
|
8
|
+
NotAnInteractor = Class.new(ContractsError)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "interactor/contracts/breach"
|
3
|
+
require "interactor/contracts/breach_set"
|
4
|
+
|
5
|
+
module Interactor
|
6
|
+
module Contracts
|
7
|
+
# The outcome of a Terms enforcement.
|
8
|
+
class Outcome
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# Instantiantes a new Outcome based on the results of a Terms enforcement
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
# @param [Dry::Validation::Result] result
|
15
|
+
def initialize(result)
|
16
|
+
@result = result
|
17
|
+
end
|
18
|
+
|
19
|
+
# @!method success?
|
20
|
+
# Checks whether the the terms enforcement was a success
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# terms = Interactor::Contract::Terms.new
|
24
|
+
# terms.add do
|
25
|
+
# required(:name).filled
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# outcome = terms.call(:name => "Bilbo Baggins")
|
29
|
+
# outcome.success? #=> true
|
30
|
+
#
|
31
|
+
# @api semipublic
|
32
|
+
# @return [Boolean]
|
33
|
+
def_delegator :@result, :success?
|
34
|
+
|
35
|
+
# The list of breaches of the Terms
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# terms = Interactor::Contract::Terms.new
|
39
|
+
# terms.add do
|
40
|
+
# required(:name).filled
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# outcome = terms.call(:name => "Bilbo Baggins")
|
44
|
+
# outcome.breaches #=> []
|
45
|
+
#
|
46
|
+
# @api semipublic
|
47
|
+
# @return [Array<Breach>] the breaches of the Terms' constraints
|
48
|
+
def breaches
|
49
|
+
BreachSet.new(result.messages(:full => true).map do |property, messages|
|
50
|
+
Breach.new(property, messages)
|
51
|
+
end)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks whether the the Terms enforcement was a failure
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# terms = Interactor::Contract::Terms.new
|
58
|
+
# terms.add do
|
59
|
+
# required(:name).filled
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# outcome = terms.call(:name => "Bilbo Baggins")
|
63
|
+
# outcome.failure? #=> false.
|
64
|
+
#
|
65
|
+
# @api semipublic
|
66
|
+
# @return [Boolean]
|
67
|
+
def failure?
|
68
|
+
!success?
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# The result of a Terms enforcement
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
# @return [Dry::Validation::Result]
|
77
|
+
attr_reader :result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "dry-validation"
|
2
|
+
require "interactor/contracts/outcome"
|
3
|
+
|
4
|
+
module Interactor
|
5
|
+
module Contracts
|
6
|
+
# The terms of a contract, either for assurances or expectations
|
7
|
+
class Terms
|
8
|
+
# Instantiates a new set of terms
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# terms = Interactor::Contracts::Terms.new
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
# @param [Dry::Validation::Schema] terms the terms to start with
|
15
|
+
def initialize(terms = Class.new(Dry::Validation::Schema))
|
16
|
+
@terms = terms
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add a new set of terms to the list of terms
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# terms = Interactor::Contracts::Terms.new
|
23
|
+
# terms.add do
|
24
|
+
# required(:name).filled
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
# @param [Block] term the term to add to the terms
|
29
|
+
# @return [void]
|
30
|
+
def add(&term)
|
31
|
+
@terms = Dry::Validation.Schema(@terms, {:build => false}, &term)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Validates the terms against a given context
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# terms = Interactor::Contracts::Terms.new
|
38
|
+
# terms.add do
|
39
|
+
# required(:name).filled
|
40
|
+
# end
|
41
|
+
# terms.call(:name => "Bilbo Baggins")
|
42
|
+
#
|
43
|
+
# @api public
|
44
|
+
# @param [#to_h] context the context to validate the terms against
|
45
|
+
# @return [Outcome]
|
46
|
+
def call(context)
|
47
|
+
Outcome.new(@terms.new.call(context.to_h))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "dry-validation"
|
2
|
+
require "interactor"
|
3
|
+
require "interactor/contracts/dsl"
|
4
|
+
require "interactor/contracts/errors"
|
5
|
+
|
6
|
+
module Interactor
|
7
|
+
# Create a contract for your interactor that specifies what it expects as
|
8
|
+
# inputs.
|
9
|
+
module Contracts
|
10
|
+
# Called when the module is included into another class or module
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
# @param [Class, Module] descendant the including class or module
|
14
|
+
# @return [void]
|
15
|
+
def self.included(descendant)
|
16
|
+
unless descendant.include?(Interactor)
|
17
|
+
fail NotAnInteractor, "#{descendant} does not include `Interactor'"
|
18
|
+
end
|
19
|
+
descendant.extend(DSL)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# The Contract to enforce on calls to the Interactor
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
# @return [Contract]
|
28
|
+
def contract
|
29
|
+
self.class.contract
|
30
|
+
end
|
31
|
+
|
32
|
+
# Checks for a breach of contract and applies consequences for a breach
|
33
|
+
#
|
34
|
+
# @api private
|
35
|
+
# @param [#call] contracts a callable object
|
36
|
+
# @return [void]
|
37
|
+
def enforce_contracts(contracts)
|
38
|
+
outcome = contracts.call(context)
|
39
|
+
|
40
|
+
unless outcome.success?
|
41
|
+
contract.consequences.each do |handler|
|
42
|
+
instance_exec(outcome.breaches, &handler)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: interactor-contracts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Herold
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-02-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-validation
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: interactor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.11'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.11'
|
55
|
+
description: Add contacts to your interactors
|
56
|
+
email:
|
57
|
+
- michael.j.herold@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- CHANGELOG.md
|
63
|
+
- CONTRIBUTING.md
|
64
|
+
- LICENSE.md
|
65
|
+
- README.md
|
66
|
+
- interactor-contracts.gemspec
|
67
|
+
- lib/interactor-contracts.rb
|
68
|
+
- lib/interactor/contracts.rb
|
69
|
+
- lib/interactor/contracts/breach.rb
|
70
|
+
- lib/interactor/contracts/breach_set.rb
|
71
|
+
- lib/interactor/contracts/contract.rb
|
72
|
+
- lib/interactor/contracts/dsl.rb
|
73
|
+
- lib/interactor/contracts/errors.rb
|
74
|
+
- lib/interactor/contracts/outcome.rb
|
75
|
+
- lib/interactor/contracts/terms.rb
|
76
|
+
- lib/interactor/contracts/version.rb
|
77
|
+
homepage: https://github.com/michaelherold/interactor-contracts
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.6.10
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Add contacts to your interactors
|
101
|
+
test_files: []
|
102
|
+
has_rdoc:
|