interactor-contracts 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/michaelherold/interactor-contracts.svg)][travis]
|
4
|
+
[![Code Climate](https://codeclimate.com/github/michaelherold/interactor-contracts/badges/gpa.svg)][codeclimate]
|
5
|
+
[![Inline docs](http://inch-ci.org/github/michaelherold/interactor-contracts.svg?branch=master)][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:
|