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 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: