interactify 0.1.0.pre.alpha.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/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +363 -0
- data/Rakefile +8 -0
- data/lib/interactify/call_wrapper.rb +15 -0
- data/lib/interactify/contract_helpers.rb +71 -0
- data/lib/interactify/dsl.rb +67 -0
- data/lib/interactify/each_chain.rb +80 -0
- data/lib/interactify/if_interactor.rb +64 -0
- data/lib/interactify/interactor_wiring.rb +305 -0
- data/lib/interactify/job_maker.rb +105 -0
- data/lib/interactify/jobable.rb +90 -0
- data/lib/interactify/organizer_call_monkey_patch.rb +40 -0
- data/lib/interactify/rspec/matchers.rb +69 -0
- data/lib/interactify/version.rb +5 -0
- data/lib/interactify.rb +89 -0
- data/sig/interactify.rbs +4 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7bbf811a8d9e44f85d4e0c5b36f4ad3291cbc8f5aa52e5d2dd31ecbdd753a256
|
4
|
+
data.tar.gz: 92ed7413e2ea370d388c43d9993928652d9bc23a3139d3fa083050243bc30695
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d9e01fc12b9b02bdc9d897b81866c7d668f94cf0fafd51e018cded3ec2892d5452848000672a9c4ac139f1c1dc3d30aa23e1c16b0dcaf66afd901c4586d8eae
|
7
|
+
data.tar.gz: b33dc94b10b26cf1b4baeace46168641d88121bf05fe2a4c8c37098d3b5bc678667e0ff253f620def1798846b9b95533f943ef4c80726abe08a1a41662c338c0
|
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Mark Burns
|
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,363 @@
|
|
1
|
+
# Interactify
|
2
|
+
|
3
|
+
Interactors are a great way to encapsulate business logic in a Rails application.
|
4
|
+
However, sometimes in complex interactor chains, the complex debugging happens at one level up from your easy to read and test interactors.
|
5
|
+
|
6
|
+
Interactify wraps the interactor and interactor-contract gem and provides additional functionality making chaining and understanding interactor chains easier.
|
7
|
+
|
8
|
+
### Syntactic Sugar
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# before
|
12
|
+
|
13
|
+
class LoadOrder
|
14
|
+
include Interactor
|
15
|
+
include Interactor::Contracts
|
16
|
+
|
17
|
+
expects do
|
18
|
+
required(:id).filled
|
19
|
+
end
|
20
|
+
|
21
|
+
promises do
|
22
|
+
required(:order).filled
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def call
|
27
|
+
context.order = Order.find(context.id)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# after
|
35
|
+
class LoadOrder
|
36
|
+
include Interactify
|
37
|
+
|
38
|
+
expect :id
|
39
|
+
promise :order
|
40
|
+
|
41
|
+
def call
|
42
|
+
context.order = Order.find(id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
|
48
|
+
### Lambdas
|
49
|
+
|
50
|
+
With vanilla interactors, it's not possible to use lambdas in organizers, and sometimes we only want a lambda.
|
51
|
+
So we added support.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
organize LoadOrder, ->(context) { context.order = context.order.decorate }
|
55
|
+
|
56
|
+
organize \
|
57
|
+
Thing1,
|
58
|
+
->(c){ byebug if c.order.nil? },
|
59
|
+
Thing2
|
60
|
+
```
|
61
|
+
|
62
|
+
### Each/Iteration
|
63
|
+
|
64
|
+
Sometimes we want an interactor for each item in a collection.
|
65
|
+
But it gets unwieldy.
|
66
|
+
It was complex procedural code and is now broken into neat SRP classes (Single Responsibility Principle).
|
67
|
+
But there is still boilerplate and jumping around between files to follow the orchestration.
|
68
|
+
It's easy to get lost in the orchestration code that occurs across say 7 or 8 files.
|
69
|
+
|
70
|
+
So the complexity problem is just moved to the gaps between the classes and files.
|
71
|
+
We gain things like `EachOrder`, or `EachProduct` interactors.
|
72
|
+
|
73
|
+
Less obvious, still there.
|
74
|
+
|
75
|
+
By using `Interactify.each` we can keep the orchestration code in one place.
|
76
|
+
We get slightly more complex organizers, but a simpler mental model of organizer as orchestrator and SRP interactors.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# before
|
80
|
+
class OuterOrganizer
|
81
|
+
# ... boilerplate ...
|
82
|
+
organize SetupStep, LoadOrders, DoSomethingWithOrders
|
83
|
+
end
|
84
|
+
|
85
|
+
class LoadOrders
|
86
|
+
# ... boilerplate ...
|
87
|
+
def call
|
88
|
+
context.orders = context.ids.map do |id|
|
89
|
+
LoadOrder.call(id: id).order
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class LoadOrder
|
95
|
+
# ... boilerplate ...
|
96
|
+
def call
|
97
|
+
# ...
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class DoSomethingWithOrders
|
102
|
+
# ... boilerplate ...
|
103
|
+
def call
|
104
|
+
context.orders.each do |order|
|
105
|
+
DoSomethingWithOrder.call(order: order)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class DoSomethingWithOrder
|
111
|
+
# ... boilerplate ...
|
112
|
+
def call
|
113
|
+
# ...
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# after
|
121
|
+
class OuterOrganizer
|
122
|
+
# ... boilerplate ...
|
123
|
+
organize \
|
124
|
+
SetupStep,
|
125
|
+
self.each(:ids,
|
126
|
+
LoadOrder,
|
127
|
+
->(c){ byebug if c.order.nil? },
|
128
|
+
DoSomethingWithOrder
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
class LoadOrder
|
133
|
+
# ... boilerplate ...
|
134
|
+
def call
|
135
|
+
# ...
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
class DoSomethingWithOrder
|
141
|
+
# ... boilerplate ...
|
142
|
+
def call
|
143
|
+
# ...
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
### Conditionals (if/else)
|
149
|
+
|
150
|
+
Along the same lines of each/iteration. We sometimes have to 'break the chain' with interactors just to conditionally call one interactor chain path or another.
|
151
|
+
|
152
|
+
The same mental model problem applies. We have to jump around between files to follow the orchestration.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# before
|
156
|
+
class OuterThing
|
157
|
+
# ... boilerplate ...
|
158
|
+
organize SetupStep, InnerThing
|
159
|
+
end
|
160
|
+
|
161
|
+
class InnerThing
|
162
|
+
# ... boilerplate ...
|
163
|
+
def call
|
164
|
+
if context.thing == 'a'
|
165
|
+
DoThingA.call(context)
|
166
|
+
else
|
167
|
+
DoThingB.call(context)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# after
|
176
|
+
class OuterThing
|
177
|
+
# ... boilerplate ...
|
178
|
+
organize \
|
179
|
+
SetupStep,
|
180
|
+
self.if(->(c){ c.thing == 'a' }, DoThingA, DoThingB),
|
181
|
+
end
|
182
|
+
|
183
|
+
```
|
184
|
+
|
185
|
+
### More Conditionals
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
class OuterThing
|
189
|
+
# ... boilerplate ...
|
190
|
+
organize \
|
191
|
+
self.if(:key_set_on_context, DoThingA, DoThingB),
|
192
|
+
AfterBothCases
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
### Simple chains
|
197
|
+
Sometimes you want an organizer that just calls a few interactors in a row.
|
198
|
+
You may want to create these dynamically at load time, or you may just want to keep the orchestration in one place.
|
199
|
+
|
200
|
+
`self.chain` is a simple way to do this.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class SomeOrganizer
|
204
|
+
include Interactify
|
205
|
+
|
206
|
+
organize \
|
207
|
+
self.if(:key_set_on_context, self.chain(DoThingA, ThenB, ThenC), DoDifferentThingB),
|
208
|
+
EitherWayDoThis
|
209
|
+
end
|
210
|
+
|
211
|
+
```
|
212
|
+
|
213
|
+
### Sidekiq Jobs
|
214
|
+
Sometimes you want to asyncify an interactor.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# before
|
218
|
+
class SomeInteractor
|
219
|
+
include Interactify
|
220
|
+
|
221
|
+
def call
|
222
|
+
# ...
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
clsas SomeInteractorJob
|
227
|
+
include Sidekiq::Job
|
228
|
+
|
229
|
+
def perform(*args)
|
230
|
+
SomeInteractor.call(*args)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
SomeInteractor.call(*args)
|
235
|
+
code is changed to
|
236
|
+
SomeInteractorJob.perform_async(*args)
|
237
|
+
```
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
# after
|
241
|
+
class SomeInteractor
|
242
|
+
include Interactify
|
243
|
+
|
244
|
+
def call
|
245
|
+
# ...
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# no need to manually create a job class or handle the perform/call impedance mismatch
|
250
|
+
SomeInteractor::Async.call(*args)
|
251
|
+
|
252
|
+
# This also makes it easy to add cron jobs to run interactors. As any interactor can be asyncified.
|
253
|
+
# By using it's internal Async class.
|
254
|
+
```
|
255
|
+
|
256
|
+
## FAQs
|
257
|
+
- This is ugly isn't it?
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
class OuterOrganizer
|
261
|
+
# ... boilerplate ...
|
262
|
+
organize \
|
263
|
+
SetupStep,
|
264
|
+
self.each(:ids,
|
265
|
+
LoadOrder,
|
266
|
+
->(c){ byebug if c.order.nil? },
|
267
|
+
DoSomethingWithOrder
|
268
|
+
)
|
269
|
+
end
|
270
|
+
```
|
271
|
+
|
272
|
+
Yes I agree. It's early days and I'm open to syntax improvement ideas. This is really about it being conceptually less ugly than the alternative, which is to jump around between lots of files. In the existing alternative to using this gem the ugliness is not within each individual file, but within the overall hidden architecture and the hunting process of jumping around in complex interactor chains. We can't see that ugliness but we probably experience it. If you don't feel or experience that ugliness then this gem may not be the right fit for you.
|
273
|
+
|
274
|
+
|
275
|
+
- Is this interactor/interactor-contracts compatible?
|
276
|
+
Yes and we use them as dependencies. It's possible we'd drop those dependencies in the future but unlikely. I think it's highly likely we'd retain compatibility.
|
277
|
+
|
278
|
+
|
279
|
+
- Why not propose changes to the interactor or interactor-contracts gem?
|
280
|
+
Honestly, I think both are great and why we've built on top of them.
|
281
|
+
I presume they'd object to such an extensive opinionated change, and I think that would be the right decision too.
|
282
|
+
If this becomes more stable, less coupled to Rails, there's interest, and things we can provide upstream I'd be happy to propose changes to those gems.
|
283
|
+
|
284
|
+
- Isn't this all just syntactic sugar?
|
285
|
+
Yes, but it's sugar that makes the code easier to read and understand.
|
286
|
+
|
287
|
+
- Is it really easier to parse this new DSL/syntax than POROs?
|
288
|
+
That's subjective, but I think so. The benefit is you have fewer extraneous files patching over a common problem in interactors.
|
289
|
+
|
290
|
+
- But it gets really verbose and complex!
|
291
|
+
Again this is subjective, but if you've worked with apps with hundred or thousands of interactors, you'll have encountered these problems.
|
292
|
+
I think when we work with interactors we're in one of two modes.
|
293
|
+
Hunting to find the interactor we need to change, or working on the interactor we need to change.
|
294
|
+
This makes the first step much easier.
|
295
|
+
The second step has always been a great experience with interactors.
|
296
|
+
|
297
|
+
- I prefer Service Objects
|
298
|
+
If you're not heavily invested into interactors this may not be for you.
|
299
|
+
I love the chaining interactors provide.
|
300
|
+
I love the contracts.
|
301
|
+
I love the simplicity of the interface.
|
302
|
+
I love the way they can be composed.
|
303
|
+
I love the way they can be tested.
|
304
|
+
When I've used service objects, I've found them to be more complex to test and compose.
|
305
|
+
I can't see a clean way that using service objects to compose interactors could work well without losing some of the aforementioned benefits.
|
306
|
+
|
307
|
+
### TODO
|
308
|
+
We want to add support for explicitly specifying promises in organizers. The benefit here is on clarifying the contract between organizers and interactors.
|
309
|
+
A writer of an organizer may expect LoadOrder to promise :order, but for the reader, it's not quite as explicit.
|
310
|
+
The expected syntax will be
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
organize \
|
314
|
+
LoadOrder.promising(:order),
|
315
|
+
TakePayment.promising(:payment_transaction)
|
316
|
+
```
|
317
|
+
|
318
|
+
This will be validated at test time against the interactors promises.
|
319
|
+
|
320
|
+
## Installation
|
321
|
+
|
322
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
323
|
+
|
324
|
+
Install the gem and add to the application's Gemfile by executing:
|
325
|
+
|
326
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
327
|
+
|
328
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
329
|
+
|
330
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
331
|
+
|
332
|
+
## Usage
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
# e.g. in spec/supoort/interactify.rb
|
336
|
+
require 'interactify/rspec/matchers'
|
337
|
+
|
338
|
+
Interactify.configure do |config|
|
339
|
+
config.root = Rails.root '/app'
|
340
|
+
end
|
341
|
+
|
342
|
+
Interactify.on_contract_breach do |context, attrs|
|
343
|
+
# maybe add context to Sentry or Honeybadger etc here
|
344
|
+
end
|
345
|
+
|
346
|
+
Interactify.before_raise do |exception|
|
347
|
+
# maybe add context to Sentry or Honeybadger etc here
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
## Development
|
352
|
+
|
353
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
354
|
+
|
355
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
356
|
+
|
357
|
+
## Contributing
|
358
|
+
|
359
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/interactify.
|
360
|
+
|
361
|
+
## License
|
362
|
+
|
363
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Interactify
|
2
|
+
module CallWrapper
|
3
|
+
# https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L114
|
4
|
+
# Interactor#run calls Interactor#run!
|
5
|
+
# https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L49
|
6
|
+
# Interactor.call calls Interactor.run
|
7
|
+
#
|
8
|
+
# The non bang methods call the bang methods and rescue
|
9
|
+
def run
|
10
|
+
@_interactor_called_by_non_bang_method = true
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'interactify/jobable'
|
2
|
+
require 'interactify/call_wrapper'
|
3
|
+
require 'interactify/organizer_call_monkey_patch'
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
module ContractHelpers
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def expect(*attrs, filled: true)
|
11
|
+
expects do
|
12
|
+
attrs.each do |attr|
|
13
|
+
field = required(attr)
|
14
|
+
field.filled if filled
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
delegate(*attrs, to: :context)
|
19
|
+
end
|
20
|
+
|
21
|
+
def optional(*attrs)
|
22
|
+
@optional_attrs ||= []
|
23
|
+
@optional_attrs += attrs
|
24
|
+
|
25
|
+
delegate(*attrs, to: :context)
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :optional_attrs
|
29
|
+
|
30
|
+
def promise(*attrs, filled: true, should_delegate: true)
|
31
|
+
promises do
|
32
|
+
attrs.each do |attr|
|
33
|
+
field = required(attr)
|
34
|
+
field.filled if filled
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
delegate(*attrs, to: :context) if should_delegate
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ContractFailure < ::Interactor::Failure
|
43
|
+
end
|
44
|
+
|
45
|
+
included do
|
46
|
+
c = Class.new(ContractFailure)
|
47
|
+
# example self is Shopkeeper::Fetch
|
48
|
+
# failure class: Shopkeeper::Fetch::InteractorContractFailure
|
49
|
+
const_set 'InteractorContractFailure', c
|
50
|
+
prepend CallWrapper
|
51
|
+
include OrganizerCallMonkeyPatch if ancestors.include? Interactor::Organizer
|
52
|
+
|
53
|
+
on_breach do |breaches|
|
54
|
+
breaches = breaches.map { |b| { b.property => b.messages } }.inject(&:merge)
|
55
|
+
|
56
|
+
Interactify.trigger_contract_breach_hook(context, breaches)
|
57
|
+
|
58
|
+
if @_interactor_called_by_non_bang_method == true
|
59
|
+
context.fail! contract_failures: breaches
|
60
|
+
else
|
61
|
+
# e.g. raises
|
62
|
+
# SomeNamespace::SomeClass::ContractFailure, {whatever: 'is missing'}
|
63
|
+
# but also sending the context into Sentry
|
64
|
+
exception = c.new(breaches.to_json)
|
65
|
+
Interactify.trigger_before_raise_hook(exception)
|
66
|
+
raise exception
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'interactify/each_chain'
|
2
|
+
require 'interactify/if_interactor'
|
3
|
+
|
4
|
+
module Interactify
|
5
|
+
module Dsl
|
6
|
+
# creates a class in the attach_klass_to's namespace
|
7
|
+
# e.g.
|
8
|
+
#
|
9
|
+
# in Orders
|
10
|
+
# Interactify.each(self, :packages, A, B, C)
|
11
|
+
#
|
12
|
+
# will create a class called Orders::EachPackage, that
|
13
|
+
# will call the interactor chain A, B, C for each package in the context
|
14
|
+
def each(plural_resource_name, *each_loop_klasses)
|
15
|
+
EachChain.attach_klass(
|
16
|
+
self,
|
17
|
+
plural_resource_name,
|
18
|
+
*each_loop_klasses
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def if(condition, succcess_interactor, failure_interactor = nil)
|
23
|
+
IfInteractor.attach_klass(
|
24
|
+
self,
|
25
|
+
condition,
|
26
|
+
succcess_interactor,
|
27
|
+
failure_interactor
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# this method allows us to dynamically create
|
32
|
+
# an organizer from a name, and a chain of interactors
|
33
|
+
#
|
34
|
+
# e.g.
|
35
|
+
#
|
36
|
+
# Interactify.chain(:SomeClass, A, B, C, expect: [:foo, :bar])
|
37
|
+
#
|
38
|
+
# is the programmable equivalent to
|
39
|
+
#
|
40
|
+
# class SomeClass
|
41
|
+
# include Interactify
|
42
|
+
# organize(A, B, C)
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# it will attach the generate class to the currenct class and
|
46
|
+
# use the class name passed in
|
47
|
+
# rubocop:disable all
|
48
|
+
def chain(klass_name, *chained_klasses, expect: [])
|
49
|
+
expectations = expect
|
50
|
+
|
51
|
+
klass = Class.new do # class EvaluatingNamespace::SomeClass
|
52
|
+
include Interactify # include Interactify
|
53
|
+
expect(*expectations) if expectations.any? # expect :foo, :bar
|
54
|
+
|
55
|
+
define_singleton_method(:source_location) do # def self.source_location
|
56
|
+
source_location # [file, line]
|
57
|
+
end # end
|
58
|
+
|
59
|
+
organize(*chained_klasses) # organize(A, B, C)
|
60
|
+
end # end
|
61
|
+
|
62
|
+
# attach the class to the calling namespace
|
63
|
+
where_to_attach = self.binding.receiver
|
64
|
+
where_to_attach.const_set(klass_name, klass)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Interactify
|
2
|
+
class EachChain
|
3
|
+
attr_reader :each_loop_klasses, :plural_resource_name, :evaluating_receiver
|
4
|
+
|
5
|
+
def self.attach_klass(evaluating_receiver, plural_resource_name, *each_loop_klasses)
|
6
|
+
iteratable = new(each_loop_klasses, plural_resource_name, evaluating_receiver)
|
7
|
+
iteratable.attach_klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(each_loop_klasses, plural_resource_name, evaluating_receiver)
|
11
|
+
@each_loop_klasses = each_loop_klasses
|
12
|
+
@plural_resource_name = plural_resource_name
|
13
|
+
@evaluating_receiver = evaluating_receiver
|
14
|
+
end
|
15
|
+
|
16
|
+
# allows us to dynamically create an interactor chain
|
17
|
+
# that iterates over the packages and
|
18
|
+
# uses the passed in each_loop_klasses
|
19
|
+
# rubocop:disable all
|
20
|
+
def klass
|
21
|
+
this = self
|
22
|
+
|
23
|
+
Class.new do # class SomeNamespace::EachPackage
|
24
|
+
include Interactify # include Interactify
|
25
|
+
|
26
|
+
expects do # expects do
|
27
|
+
required(this.plural_resource_name) # required(:packages)
|
28
|
+
end # end
|
29
|
+
|
30
|
+
define_singleton_method(:source_location) do # def self.source_location
|
31
|
+
const_source_location this.evaluating_receiver.to_s # [file, line]
|
32
|
+
end # end
|
33
|
+
|
34
|
+
define_method(:run!) do # def run!
|
35
|
+
context.send(this.plural_resource_name).each_with_index do |resource, index|# context.packages.each_with_index do |package, index|
|
36
|
+
context[this.singular_resource_name] = resource # context.package = package
|
37
|
+
context[this.singular_resource_index_name] = index # context.package_index = index
|
38
|
+
|
39
|
+
klasses = self.class.wrap_lambdas_in_interactors(this.each_loop_klasses)
|
40
|
+
|
41
|
+
klasses.each do |interactor| # [A, B, C].each do |interactor|
|
42
|
+
interactor.call!(context) # interactor.call!(context)
|
43
|
+
end # end
|
44
|
+
end # end
|
45
|
+
|
46
|
+
context[this.singular_resource_name] = nil # context.package = nil
|
47
|
+
context[this.singular_resource_index_name] = nil # context.package_index = nil
|
48
|
+
|
49
|
+
context # context
|
50
|
+
end # end
|
51
|
+
|
52
|
+
define_method(:inspect) do
|
53
|
+
"<#{this.namespace}::#{this.iterator_klass_name} iterates_over: #{this.each_loop_klasses.inspect}>"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
# rubocop:enable all
|
58
|
+
|
59
|
+
def attach_klass
|
60
|
+
namespace.const_set(iterator_klass_name, klass)
|
61
|
+
namespace.const_get(iterator_klass_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def namespace
|
65
|
+
evaluating_receiver
|
66
|
+
end
|
67
|
+
|
68
|
+
def iterator_klass_name
|
69
|
+
:"Each#{singular_resource_name.to_s.camelize}".to_sym
|
70
|
+
end
|
71
|
+
|
72
|
+
def singular_resource_name
|
73
|
+
plural_resource_name.to_s.singularize.to_sym
|
74
|
+
end
|
75
|
+
|
76
|
+
def singular_resource_index_name
|
77
|
+
"#{singular_resource_name}_index".to_sym
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|