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