functional_interactor 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +42 -0
- data/CONTRIBUTING.md +49 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +332 -0
- data/Rakefile +6 -0
- data/functional_interactor.gemspec +21 -0
- data/lib/functional_interactor.rb +38 -0
- data/lib/interactors.rb +12 -0
- data/lib/interactors/anonymous.rb +20 -0
- data/lib/interactors/sequence.rb +28 -0
- data/lib/interactors/simple.rb +35 -0
- data/spec/integration_spec.rb +1741 -0
- data/spec/interactor/context_spec.rb +167 -0
- data/spec/interactor/hooks_spec.rb +358 -0
- data/spec/interactor/organizer_spec.rb +57 -0
- data/spec/interactor_spec.rb +3 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/lint.rb +135 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 42e88c718a1683448f282cae14099b67a7816885
|
4
|
+
data.tar.gz: c2a9c1a1be617e58001d1d3caba40d45225a27c1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3287a14707157e7bfe623f4ecc2771b96f64f4ec4288cc2296661b4c268e69b040660af4218c1b10fe42273898282b5a65937360d0faf2674c965f512b759a59
|
7
|
+
data.tar.gz: d2ab9f05f340a6a0eb8dfab94f8871c83f43cea8db09e1e6fcf89b89e1530ccd0cd4a5df136dba325c2ea84e41d6ac50ed35b01fcb37b5ee9f549aa16492b9d9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
before_install:
|
2
|
+
- gem update bundler rake
|
3
|
+
branches:
|
4
|
+
only:
|
5
|
+
- master
|
6
|
+
- v3
|
7
|
+
env:
|
8
|
+
global:
|
9
|
+
- secure: | # CODECLIMATE_REPO_TOKEN
|
10
|
+
BIemhM273wHZMpuULDMYGPsxYdfw+NMw7IQbOD6gy5r+dha07y9ssTYYE5Gn
|
11
|
+
t1ptAb09lhQ4gexXTr83i6angMrnHgQ1ZX2wfeoZ0FvWDHQht9YkXyiNH+R6
|
12
|
+
odHUeDIYAlUiqLX9nAkklL89Rc22BrHMGGNyuA8Uc5sktW5P/FE=
|
13
|
+
language: ruby
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: ruby-head
|
17
|
+
rvm:
|
18
|
+
- 1.9.3
|
19
|
+
- "2.0"
|
20
|
+
- "2.1"
|
21
|
+
- ruby-head
|
22
|
+
script: bundle exec rspec
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
## 3.1.0 / 2014-10-13
|
2
|
+
|
3
|
+
* [FEATURE] Add around hooks
|
4
|
+
|
5
|
+
## 3.0.1 / 2014-09-09
|
6
|
+
|
7
|
+
* [ENHANCEMENT] Add TomDoc code documentation
|
8
|
+
|
9
|
+
## 3.0.0 / 2014-09-07
|
10
|
+
|
11
|
+
* [FEATURE] Remove "magical" access to the context through the interactor
|
12
|
+
* [FEATURE] Manage context values via setters/getters rather than hash access
|
13
|
+
* [FEATURE] Change the primary interactor API method from "perform" to "call"
|
14
|
+
* [FEATURE] Return the mutated context rather than the interactor instance
|
15
|
+
* [FEATURE] Replace interactor setup with before and after hooks
|
16
|
+
* [FEATURE] Abort execution immediately upon interactor failure
|
17
|
+
* [ENHANCEMENT] Build a suite of realistic integration tests
|
18
|
+
* [ENHANCEMENT] Move rollback responsibility into the context
|
19
|
+
|
20
|
+
## 2.1.1 / 2014-09-30
|
21
|
+
|
22
|
+
* [FEATURE] Halt performance if the interactor fails prior
|
23
|
+
* [ENHANCEMENT] Add support for Ruby 2.1
|
24
|
+
|
25
|
+
## 2.1.0 / 2013-09-05
|
26
|
+
|
27
|
+
* [FEATURE] Roll back when an interactor within an organizer raises an error
|
28
|
+
* [BUGFIX] Ensure that context-deferred methods respect string keys
|
29
|
+
* [FEATURE] Respect context initialization from an indifferent access hash
|
30
|
+
|
31
|
+
## 2.0.1 / 2013-08-28
|
32
|
+
|
33
|
+
* [BUGFIX] Allow YAML (de)serialization by fixing interactor allocation
|
34
|
+
|
35
|
+
## 2.0.0 / 2013-08-19
|
36
|
+
|
37
|
+
* [BUGFIX] Fix rollback behavior within nested organizers
|
38
|
+
* [BUGFIX] Skip rollback for the failed interactor
|
39
|
+
|
40
|
+
## 1.0.0 / 2013-08-17
|
41
|
+
|
42
|
+
* Initial release!
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributing to Interactor
|
2
|
+
|
3
|
+
Interactor is open source and contributions from the community are encouraged!
|
4
|
+
No contribution is too small.
|
5
|
+
|
6
|
+
Please consider:
|
7
|
+
|
8
|
+
* adding a feature
|
9
|
+
* squashing a bug
|
10
|
+
* writing [documentation](http://tomdoc.org)
|
11
|
+
* reporting an issue
|
12
|
+
* fixing a typo
|
13
|
+
* correcting [style](https://github.com/styleguide/ruby)
|
14
|
+
|
15
|
+
## How do I contribute?
|
16
|
+
|
17
|
+
For the best chance of having your changes merged, please:
|
18
|
+
|
19
|
+
1. [Fork](https://github.com/collectiveidea/interactor/fork) the project.
|
20
|
+
2. [Write](http://en.wikipedia.org/wiki/Test-driven_development) a failing test.
|
21
|
+
3. [Commit](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) changes that fix the tests.
|
22
|
+
4. [Submit](https://github.com/collectiveidea/interactor/pulls) a pull request with *at least* one animated GIF.
|
23
|
+
5. Be patient.
|
24
|
+
|
25
|
+
If your proposed changes only affect documentation, include the following on a
|
26
|
+
new line in each of your commit messages:
|
27
|
+
|
28
|
+
```
|
29
|
+
[ci skip]
|
30
|
+
```
|
31
|
+
|
32
|
+
This will signal [Travis](https://travis-ci.org) that running the test suite is
|
33
|
+
not necessary for these changes.
|
34
|
+
|
35
|
+
## Bug Reports
|
36
|
+
|
37
|
+
If you are experiencing unexpected behavior and, after having read Interactor's
|
38
|
+
documentation, are convinced this behavior is a bug, please:
|
39
|
+
|
40
|
+
1. [Search](https://github.com/collectiveidea/interactor/issues) existing issues.
|
41
|
+
2. Collect enough information to reproduce the issue:
|
42
|
+
* Interactor version
|
43
|
+
* Ruby version
|
44
|
+
* Rails version (if applicable)
|
45
|
+
* Specific setup conditions
|
46
|
+
* Description of expected behavior
|
47
|
+
* Description of actual behavior
|
48
|
+
3. [Submit](https://github.com/collectiveidea/interactor/issues/new) an issue.
|
49
|
+
4. Be patient.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Collective Idea
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
# Functional Interactor
|
2
|
+
|
3
|
+
Based around https://github.com/collectiveidea/interactor, reimagined to use Kase with composability operators.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
Add Interactor to your Gemfile and `bundle install`.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "functional_interactor", "~> 0.0.1"
|
11
|
+
```
|
12
|
+
|
13
|
+
This implementation is meant to be used with the [Kase](https://github.com/lasseebert/kase) gem. This
|
14
|
+
is also an experiment -- some of the ideas such as generic interactors are somethign we (Legal.io engineering)
|
15
|
+
is trying due to the sheer complexity of our code base. The examples we have in the advanced usage section
|
16
|
+
can use a lot of improvement.
|
17
|
+
|
18
|
+
## What is an Interactor?
|
19
|
+
|
20
|
+
An interactor is a simple, single-purpose object.
|
21
|
+
|
22
|
+
Interactors are used to encapsulate your application's
|
23
|
+
[business logic](http://en.wikipedia.org/wiki/Business_logic). Each interactor
|
24
|
+
represents one thing that your application *does*.
|
25
|
+
|
26
|
+
### Call and Return Protocol
|
27
|
+
|
28
|
+
An Interactor must respond to the method `call`, and takes a single object.
|
29
|
+
|
30
|
+
An Interactor following this protocol will accept a single object which encapsulates
|
31
|
+
the state or context. By convention, we use a Hash-like object so that interactors
|
32
|
+
can be composed into higher-order interactions.
|
33
|
+
|
34
|
+
#### Success
|
35
|
+
|
36
|
+
When the action succeeds, return
|
37
|
+
```ruby
|
38
|
+
[:ok, context]
|
39
|
+
```
|
40
|
+
|
41
|
+
The return value of `context` can be anything, though it is suggested that you
|
42
|
+
stick with a Hash-like object so that interactors can be chained together.
|
43
|
+
|
44
|
+
#### Failure
|
45
|
+
|
46
|
+
When the action fails, return an array where the first element is the symbol
|
47
|
+
`:error`. Examples:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
[:error, "This failed"]
|
51
|
+
[:error, :invalid, [{'field' => 'must be present'}]]
|
52
|
+
[:error, :stripe_error, StripeException.new]
|
53
|
+
```
|
54
|
+
|
55
|
+
You are typically going to use Kase to handle errors:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Kase.kase PushUserToElasticSearch.call(user) do
|
59
|
+
on(:ok) do |ctx|
|
60
|
+
# Do something
|
61
|
+
end
|
62
|
+
|
63
|
+
on(:error, :network) do |reason|
|
64
|
+
NotifyHuman.log "failed to push user ##{user.id} to ElasticSearch"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Context
|
70
|
+
|
71
|
+
An interactor is given a *context*. The context contains everything the
|
72
|
+
interactor needs to do its work.
|
73
|
+
|
74
|
+
When an interactor does its single purpose, it affects its given context.
|
75
|
+
|
76
|
+
Context are assumed to be a Hash like object.
|
77
|
+
|
78
|
+
#### Adding to the Context
|
79
|
+
|
80
|
+
As an interactor runs it can add information to the context.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
context[:user] = user
|
84
|
+
```
|
85
|
+
|
86
|
+
### Hooks
|
87
|
+
|
88
|
+
This implementation has no hooks.
|
89
|
+
|
90
|
+
### An Example Interactor
|
91
|
+
|
92
|
+
Your application could use an interactor to authenticate a user.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class AuthenticateUser
|
96
|
+
include FunctionalInteractor
|
97
|
+
|
98
|
+
def call(context = {})
|
99
|
+
user = User.authenticate(context[:email], context[:password])
|
100
|
+
|
101
|
+
return [:error, :not_authenticated] unless user
|
102
|
+
|
103
|
+
context[:user] = user
|
104
|
+
context[:token] = user.secret_token
|
105
|
+
|
106
|
+
# Return a new context so we are not modifying the original
|
107
|
+
[:ok, { user: user, token: user.secret_token }]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
To define an interactor, simply create a class that includes the `Interactor`
|
113
|
+
module and give it a `call` instance method. The interactor can access its
|
114
|
+
`context` from within `call`.
|
115
|
+
|
116
|
+
## Interactors in the Controller
|
117
|
+
|
118
|
+
Most of the time, your application will use its interactors from its
|
119
|
+
controllers. The following controller:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
class SessionsController < ApplicationController
|
123
|
+
def create
|
124
|
+
if user = User.authenticate(session_params[:email], session_params[:password])
|
125
|
+
session[:user_token] = user.secret_token
|
126
|
+
redirect_to user
|
127
|
+
else
|
128
|
+
flash.now[:message] = "Please try again."
|
129
|
+
render :new
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def session_params
|
136
|
+
params.require(:session).permit(:email, :password)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
can be refactored to:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class SessionsController < ApplicationController
|
145
|
+
def create
|
146
|
+
Kase.kase AuthenticateUser.call(session_params) do
|
147
|
+
on(:ok) do |result|
|
148
|
+
session[:user_token] = result[:token]
|
149
|
+
redirect_to root_path
|
150
|
+
end
|
151
|
+
|
152
|
+
on(:error, :not_authenticated) do
|
153
|
+
flash.now[:message] = t(result.message)
|
154
|
+
render :new
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def session_params
|
162
|
+
params.require(:session).permit(:email, :password)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
The `.call` class method simply instantiates a new `AuthenticatedUser` interactor
|
168
|
+
and passes the context to it. This allows us to create generic interactors
|
169
|
+
that can be inlined and composed together. This is discussed in the following section.
|
170
|
+
|
171
|
+
## Advanced Usage
|
172
|
+
|
173
|
+
### Sequences
|
174
|
+
|
175
|
+
`creativeideas/interactor` has an `Organizer` class. We have a similar code called
|
176
|
+
`Interactors::Sequence`.
|
177
|
+
|
178
|
+
Let's define a second interactor:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
class NotifyLogin
|
182
|
+
include FunctionalInteractor
|
183
|
+
|
184
|
+
def call(context = {})
|
185
|
+
NotificationsMailer.login(user: context[:user]).deliver
|
186
|
+
[:ok, context]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
We can then chain them together like so:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
interactions = Interactors::Sequence.new
|
195
|
+
interactions.compose(AuthenticatedUser)
|
196
|
+
interactions.compose(NotifyLogin)
|
197
|
+
|
198
|
+
Kase.kase interactions.call(session_params) do
|
199
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
200
|
+
on(:error) { |context| puts "Failed to login" }
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
Here, the `Interactors::Sequence` object holds a sequence of
|
205
|
+
interactions. It will call them one by one, starting from the top. If
|
206
|
+
at any point, it returns something with `[:error, ...]` then the chain
|
207
|
+
will stop. We can then use `Kase` to handle the error.
|
208
|
+
|
209
|
+
### `#compose` and `|`
|
210
|
+
|
211
|
+
We do not actually have to create an `Interactors::Sequence` object. The
|
212
|
+
`#compose` method will create an `Interactors::Sequence` for you. You can
|
213
|
+
chain them together like so:
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
interactions = AuthenticatedUser.compose(NotifyLogin)
|
217
|
+
|
218
|
+
Kase.kase interactions.call(session_params) do
|
219
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
220
|
+
on(:error) { |context| puts "Failed to login" }
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
We also aliased `|` so you can use that instead:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
interactions = AuthenticatedUser | NotifyLogin
|
228
|
+
|
229
|
+
Kase.kase interactions.call(session_params) do
|
230
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
231
|
+
on(:error) { |context| puts "Failed to login" }
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
### Generic Interactors
|
236
|
+
|
237
|
+
Sometimes we want to dynamically create an interactor. We can change the
|
238
|
+
notification interactor to:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
interactions = AuthenticatedUser \
|
242
|
+
| Interactors::Anonymous.new do
|
243
|
+
NotificationsMailer.login(user: context[:user]).deliver
|
244
|
+
[:ok, context]
|
245
|
+
end
|
246
|
+
|
247
|
+
Kase.kase interactions.call(session_params) do
|
248
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
249
|
+
on(:error) { |context| puts "Failed to login" }
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
There is a helper, `Interactors.new` that can simplify that:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
interactions = AuthenticatedUser \
|
257
|
+
| Interactors.new do
|
258
|
+
NotificationsMailer.login(user: context[:user]).deliver
|
259
|
+
[:ok, context]
|
260
|
+
end
|
261
|
+
|
262
|
+
Kase.kase interactions.call(session_params) do
|
263
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
264
|
+
on(:error) { |context| puts "Failed to login" }
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
Since we don't care about handling errors, we can `Interactors::Simple` instead:
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
interactions = AuthenticatedUser \
|
272
|
+
| Interactors::Simple.new { NotificationsMailer.login(user: context[:user]).deliver }
|
273
|
+
|
274
|
+
Kase.kase interactions.call(session_params) do
|
275
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
276
|
+
on(:error) { |context| puts "Failed to login" }
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
This might seem like a lot for just a simple mailer. The real value comes from when
|
281
|
+
there is a long chain of interactions:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
interactions = AuthenticatedUser \
|
285
|
+
| FraudDetector \
|
286
|
+
| Interactors::Simple.new { NotificationsMailer.login(user: context[:user]).deliver } \
|
287
|
+
| ActivityLogger.new(:user_logs_in, controller: self) \
|
288
|
+
| Interactors::RPC.new(service: :presence, module: :'Elixir.Presence.RPC', func: :register)
|
289
|
+
|
290
|
+
Kase.kase interactions.call(session_params) do
|
291
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
292
|
+
on(:error) { |context| puts "Failed to login" }
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
### Custom Generic Interactors
|
297
|
+
|
298
|
+
Generic interactors work because we can override the constructor. In the case of a Rails mailer,
|
299
|
+
maybe we want to have a generic mailer:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
class Interactors::Mailer
|
303
|
+
include FunctionalInteractor
|
304
|
+
|
305
|
+
def new(mailer:, method:)
|
306
|
+
@mailer = mailer
|
307
|
+
@method = method
|
308
|
+
end
|
309
|
+
|
310
|
+
def call(context = {})
|
311
|
+
mailer.send(method, context)
|
312
|
+
[:ok, context]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
In which case, we can then use that:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
interactions = AuthenticatedUser \
|
321
|
+
| Interactors::Mailer.new(mailer: NotificationsMailer, method: :login)
|
322
|
+
|
323
|
+
Kase.kase interactions.call(session_params) do
|
324
|
+
on(:ok) { |context| puts "Yay! Logged in!" }
|
325
|
+
on(:error) { |context| puts "Failed to login" }
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
## Further Discussion
|
330
|
+
|
331
|
+
`collectiveideas/interactor` has a great section discussing on when to
|
332
|
+
use interactors: https://github.com/collectiveidea/interactor#when-to-use-an-interactor
|