functional_interactor 0.0.1

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: 42e88c718a1683448f282cae14099b67a7816885
4
+ data.tar.gz: c2a9c1a1be617e58001d1d3caba40d45225a27c1
5
+ SHA512:
6
+ metadata.gz: 3287a14707157e7bfe623f4ecc2771b96f64f4ec4288cc2296661b4c268e69b040660af4218c1b10fe42273898282b5a65937360d0faf2674c965f512b759a59
7
+ data.tar.gz: d2ab9f05f340a6a0eb8dfab94f8871c83f43cea8db09e1e6fcf89b89e1530ccd0cd4a5df136dba325c2ea84e41d6ac50ed35b01fcb37b5ee9f549aa16492b9d9
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --order random
3
+ --require spec_helper
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
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem "codeclimate-test-reporter", require: false
7
+ gem "rspec", "~> 3.1"
8
+ end
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