interactor-contracts 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ module Interactor
2
+ module Contracts
3
+ VERSION = "0.1.0".freeze
4
+ end
5
+ 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
@@ -0,0 +1,2 @@
1
+ # rubocop:disable Style/FileName
2
+ require "interactor/contracts"
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: