interactify 0.1.0.pre.alpha.1 → 0.3.0.pre.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +23 -0
- data/CHANGELOG.md +9 -1
- data/README.md +174 -105
- data/lib/interactify/async_job_klass.rb +61 -0
- data/lib/interactify/call_wrapper.rb +2 -0
- data/lib/interactify/contract_failure.rb +6 -0
- data/lib/interactify/contract_helpers.rb +10 -10
- data/lib/interactify/dsl.rb +6 -2
- data/lib/interactify/each_chain.rb +12 -4
- data/lib/interactify/if_interactor.rb +10 -4
- data/lib/interactify/interactor_wiring/callable_representation.rb +79 -0
- data/lib/interactify/interactor_wiring/constants.rb +125 -0
- data/lib/interactify/interactor_wiring/error_context.rb +41 -0
- data/lib/interactify/interactor_wiring/files.rb +51 -0
- data/lib/interactify/interactor_wiring.rb +57 -272
- data/lib/interactify/interactor_wrapper.rb +72 -0
- data/lib/interactify/job_maker.rb +20 -69
- data/lib/interactify/jobable.rb +9 -7
- data/lib/interactify/mismatching_promise_error.rb +17 -0
- data/lib/interactify/organizer.rb +30 -0
- data/lib/interactify/promising.rb +34 -0
- data/lib/interactify/rspec/matchers.rb +9 -11
- data/lib/interactify/unique_klass_name.rb +21 -0
- data/lib/interactify/version.rb +1 -1
- data/lib/interactify.rb +39 -9
- metadata +14 -3
- data/lib/interactify/organizer_call_monkey_patch.rb +0 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 918f4f6ae335bb5b8a748603af6a2e9d91dc11ad3d887803bbb2c7b2ba41730e
|
4
|
+
data.tar.gz: fe6506fe246a82d48f5a614d5c576cf74e49f38953c1d4ae98d52f1649f03aaf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce377154a79b527f6eaebe3258d76aadd327521abbe86847b8c9d87b5710e1782bdb02b20b3ef3661ac6dc4ee1d0c6843fdd819ccf6dda8f701e9d95063d5a9c
|
7
|
+
data.tar.gz: 553271645575873bc2a95968c2ff7f01a556dd24527974b6a7bb958a8eeb9f8bb66504bdace2c3736e6faac82c1a438f07d27afa85c3fdc8ac5b051cac4e20c5
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
Exclude:
|
4
|
+
- 'spec/fixtures/**/*'
|
5
|
+
- 'tmp/**/*'
|
6
|
+
- '.git/**/*'
|
7
|
+
- 'bin/*'
|
8
|
+
Style/ClassAndModuleChildren:
|
9
|
+
Exclude:
|
10
|
+
- 'spec/**/*'
|
11
|
+
Metrics/BlockLength:
|
12
|
+
Exclude:
|
13
|
+
- 'spec/**/*'
|
14
|
+
- '*.gemspec'
|
15
|
+
Metrics/MethodLength:
|
16
|
+
Enabled: false
|
17
|
+
Lint/ConstantDefinitionInBlock:
|
18
|
+
Exclude:
|
19
|
+
- 'spec/**/*'
|
20
|
+
Style/Documentation:
|
21
|
+
Enabled: false
|
22
|
+
Style/StringLiterals:
|
23
|
+
EnforcedStyle: double_quotes
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
## [0.1.0] - 2023-12-16
|
3
|
+
## [0.1.0-alpha.1] - 2023-12-16
|
4
4
|
|
5
5
|
- Initial release
|
6
|
+
|
7
|
+
## [0.2.0-alpha.1] - 2023-12-27
|
8
|
+
|
9
|
+
- Added support for Interactify.promising syntax in organizers
|
10
|
+
|
11
|
+
## [0.3.0-alpha.1] - 2023-12-29
|
12
|
+
|
13
|
+
- Added support for `{if: :condition, then: A, else: B}` in organizers
|
data/README.md
CHANGED
@@ -1,11 +1,46 @@
|
|
1
1
|
# Interactify
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
Interactify enhances Rails applications by simplifying complex interactor chains. This gem builds on [interactors](https://github.com/collectiveidea/interactor) and [interactor-contracts](https://github.com/michaelherold/interactor-contracts) to improve readability and maintainability of business logic. It's optimized for Rails projects using Sidekiq, offering advanced features for chain management and debugging. Interactify is about making interactor usage in Rails more efficient and less error-prone, reducing the overhead of traditional interactor orchestration.
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
gem 'interactify'
|
8
|
+
```
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
### Initializer
|
13
|
+
```ruby
|
14
|
+
# in config/initializers/interactify.rb
|
15
|
+
Interactify.configure do |config|
|
16
|
+
# default
|
17
|
+
# config.root = Rails.root / 'app'
|
18
|
+
end
|
19
|
+
|
20
|
+
Interactify.on_contract_breach do |context, attrs|
|
21
|
+
# maybe add context to Sentry or Honeybadger etc here
|
22
|
+
end
|
23
|
+
|
24
|
+
Interactify.before_raise do |exception|
|
25
|
+
# maybe add context to Sentry or Honeybadger etc here
|
26
|
+
end
|
27
|
+
```
|
5
28
|
|
6
|
-
|
29
|
+
### Using the RSpec matchers
|
30
|
+
```ruby
|
31
|
+
# e.g. in spec/supoort/interactify.rb
|
32
|
+
require 'interactify/rspec/matchers'
|
33
|
+
|
34
|
+
expect(described_class).to expect_inputs(:foo, :bar, :baz)
|
35
|
+
expect(described_class).to promise_outputs(:fee, :fi, :fo, :fum)
|
36
|
+
```
|
7
37
|
|
8
38
|
### Syntactic Sugar
|
39
|
+
- Everything is an Organizer/Interactor and supports interactor-contracts.
|
40
|
+
- They only becomes considered an organizer once `organize` is called.
|
41
|
+
- They could technically be both (if you want?) but you have to remember to call `super` within `call` to trigger the organized interactors.
|
42
|
+
- Concise syntax for most common scenarios with `expects` and `promises`. Verifying the presence of the keys/values.
|
43
|
+
- Automatic delegation of expected and promised keys to the context.
|
9
44
|
|
10
45
|
```ruby
|
11
46
|
# before
|
@@ -16,13 +51,14 @@ class LoadOrder
|
|
16
51
|
|
17
52
|
expects do
|
18
53
|
required(:id).filled
|
54
|
+
required(:something_else).filled
|
55
|
+
required(:a_boolean_flag)
|
19
56
|
end
|
20
57
|
|
21
58
|
promises do
|
22
59
|
required(:order).filled
|
23
60
|
end
|
24
61
|
|
25
|
-
|
26
62
|
def call
|
27
63
|
context.order = Order.find(context.id)
|
28
64
|
end
|
@@ -35,7 +71,8 @@ end
|
|
35
71
|
class LoadOrder
|
36
72
|
include Interactify
|
37
73
|
|
38
|
-
expect :id
|
74
|
+
expect :id, :something_else
|
75
|
+
expect :a_boolean_flag, filled: false
|
39
76
|
promise :order
|
40
77
|
|
41
78
|
def call
|
@@ -47,8 +84,8 @@ end
|
|
47
84
|
|
48
85
|
### Lambdas
|
49
86
|
|
50
|
-
With vanilla interactors, it'
|
51
|
-
So we added support.
|
87
|
+
With vanilla interactors, it wasn't possible to use lambdas in organizers.
|
88
|
+
But sometimes we only want a lambda. So we added support.
|
52
89
|
|
53
90
|
```ruby
|
54
91
|
organize LoadOrder, ->(context) { context.order = context.order.decorate }
|
@@ -63,7 +100,7 @@ organize \
|
|
63
100
|
|
64
101
|
Sometimes we want an interactor for each item in a collection.
|
65
102
|
But it gets unwieldy.
|
66
|
-
It was complex procedural code and is now broken into neat SRP classes
|
103
|
+
It was complex procedural code and is now broken into neat [SRP classes](https://en.wikipedia.org/wiki/Single_responsibility_principle).
|
67
104
|
But there is still boilerplate and jumping around between files to follow the orchestration.
|
68
105
|
It's easy to get lost in the orchestration code that occurs across say 7 or 8 files.
|
69
106
|
|
@@ -115,7 +152,6 @@ class DoSomethingWithOrder
|
|
115
152
|
end
|
116
153
|
```
|
117
154
|
|
118
|
-
|
119
155
|
```ruby
|
120
156
|
# after
|
121
157
|
class OuterOrganizer
|
@@ -136,7 +172,6 @@ class LoadOrder
|
|
136
172
|
end
|
137
173
|
end
|
138
174
|
|
139
|
-
|
140
175
|
class DoSomethingWithOrder
|
141
176
|
# ... boilerplate ...
|
142
177
|
def call
|
@@ -145,7 +180,7 @@ class DoSomethingWithOrder
|
|
145
180
|
end
|
146
181
|
```
|
147
182
|
|
148
|
-
### Conditionals (if/else)
|
183
|
+
### Conditionals (if/else) with lambda
|
149
184
|
|
150
185
|
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
186
|
|
@@ -170,7 +205,6 @@ class InnerThing
|
|
170
205
|
end
|
171
206
|
```
|
172
207
|
|
173
|
-
|
174
208
|
```ruby
|
175
209
|
# after
|
176
210
|
class OuterThing
|
@@ -180,9 +214,16 @@ class OuterThing
|
|
180
214
|
self.if(->(c){ c.thing == 'a' }, DoThingA, DoThingB),
|
181
215
|
end
|
182
216
|
|
217
|
+
# or hash syntax
|
218
|
+
class OuterThing
|
219
|
+
# ... boilerplate ...
|
220
|
+
organize \
|
221
|
+
{if: :key_set_on_context, then: DoThingA, else: DoThingB},
|
222
|
+
AfterBothCases
|
223
|
+
end
|
183
224
|
```
|
184
225
|
|
185
|
-
###
|
226
|
+
### Conditionals with a key from the context
|
186
227
|
|
187
228
|
```ruby
|
188
229
|
class OuterThing
|
@@ -208,6 +249,90 @@ class SomeOrganizer
|
|
208
249
|
EitherWayDoThis
|
209
250
|
end
|
210
251
|
|
252
|
+
```
|
253
|
+
### Contract validation failures
|
254
|
+
Sometimes contract validation fails at runtime as an exception. It's something unexpected and you'll have an `Interactor::Failure` sent to rollbar/sentry/honeybadger.
|
255
|
+
If the context is large it's often hard to spot what the actual problem is or where it occurred.
|
256
|
+
|
257
|
+
#### before
|
258
|
+
```
|
259
|
+
Interactor::Failure
|
260
|
+
|
261
|
+
#<Interactor::Context output_destination="DataExportSystem", output_format=:xml, region_code="XX", custom_flag=false, process_mode="sample", cache_identifier="GenericProcessorSample-XML-XX-0", data_key="GenericProcessorSample", data_version=0, last_process_time=2023-12-26 04:00:18.953000000 GMT +00:00, process_start_time=2023-12-26 06:45:17.915237484 UTC, updated_ids=[BSON::ObjectId('123f77a58444201ff1f0611a'), BSON::ObjectId('123f78148444201fd62a2e9b'), BSON::ObjectId('12375d8084442038712ba40e')], lock_info=#<Processing::Lock _id: 123a767d7b944674cc069064, created_at: 2023-12-26 06:45:17.992417809 UTC, updated_at: 2023-12-26 06:45:17.992417809 UTC, processor: "DataExportSystem", format: "xml", type: "sample">, expired_cache_ids=[], jobs=['jobs must be filled'] items=#<Mongoid::Criteria (Interactor::Failure)
|
262
|
+
, tasks=[]>
|
263
|
+
```
|
264
|
+
|
265
|
+
#### after with call
|
266
|
+
```
|
267
|
+
#<Interactor::Context output_destination="DataExportSystem", output_format=:xml, region_code="XX", custom_flag=false, process_mode="sample", cache_identifier="GenericProcessorSample-XML-XX-0", data_key="GenericProcessorSample", data_version=0, last_process_time=2023-12-26 04:00:18.953000000 GMT +00:00, process_start_time=2023-12-26 06:45:17.915237484 UTC, updated_ids=[BSON::ObjectId('123f77a58444201ff1f0611a'), BSON::ObjectId('123f78148444201fd62a2e9b'), BSON::ObjectId('12375d8084442038712ba40e')], lock_info=#<Processing::Lock _id: 123a767d7b944674cc069064, created_at: 2023-12-26 06:45:17.992417809 UTC, updated_at: 2023-12-26 06:45:17.992417809 UTC, processor: "DataExportSystem", format: "xml", type: "sample">, expired_cache_ids=[], tasks=['tasks must be filled'] items=#<Mongoid::Criteria (Interactor::Failure)
|
268
|
+
, tasks=[], contract_failures={:tasks=>["tasks must be filled"]}>
|
269
|
+
```
|
270
|
+
|
271
|
+
#### after with call!
|
272
|
+
```
|
273
|
+
#<SomeSpecificInteractor::ContractFailure output_destination="DataExportSystem", output_format=:xml, region_code="XX", custom_flag=false, process_mode="sample", cache_identifier="GenericProcessorSample-XML-XX-0", data_key="GenericProcessorSample", data_version=0, last_process_time=2023-12-26 04:00:18.953000000 GMT +00:00, process_start_time=2023-12-26 06:45:17.915237484 UTC, updated_ids=[BSON::ObjectId('123f77a58444201ff1f0611a'), BSON::ObjectId('123f78148444201fd62a2e9b'), BSON::ObjectId('12375d8084442038712ba40e')], lock_info=#<Processing::Lock _id: 123a767d7b944674cc069064, created_at: 2023-12-26 06:45:17.992417809 UTC, updated_at: 2023-12-26 06:45:17.992417809 UTC, processor: "DataExportSystem", format: "xml", type: "sample">, expired_cache_ids=[], tasks=['tasks must be filled'] items=#<Mongoid::Criteria (Interactor::Failure)
|
274
|
+
, tasks=[], contract_failures={:tasks=>["tasks must be filled"]}>
|
275
|
+
```
|
276
|
+
|
277
|
+
### Promising
|
278
|
+
You can annotate your interactors in the organize arguments with their promises.
|
279
|
+
This then acts as executable documentation that is validated at load time and enforced to stay in sync with the interactor.
|
280
|
+
|
281
|
+
A writer of an organizer may quite reasonably expect `LoadOrder` to promise `:order`, but for the reader, it's not always as immediately obvious
|
282
|
+
which interactor in the chain is responsible for provides which key.
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
organize \
|
286
|
+
LoadOrder.promising(:order),
|
287
|
+
TakePayment.promising(:payment_transaction)
|
288
|
+
```
|
289
|
+
|
290
|
+
This will be validated at load time against the interactors promises.
|
291
|
+
An example of a failure would be:
|
292
|
+
|
293
|
+
```
|
294
|
+
SomeOrganizer::DoStep1 does not promise:
|
295
|
+
step_1
|
296
|
+
|
297
|
+
Actual promises are:
|
298
|
+
step1
|
299
|
+
```
|
300
|
+
|
301
|
+
|
302
|
+
### Interactor wiring specs
|
303
|
+
Sometimes you have an interactor chain that fails because something is expected deeper down the chain and not provided further up the chain.
|
304
|
+
The existing way to solve this is with enough integration specs to catch them, hunting and sticking a `byebug`, `debugger` or `binding.pry` in at suspected locations and inferring where in the chain the wiring went awry.
|
305
|
+
|
306
|
+
But we can do better than that if we always `promise` something that is later `expect`ed.
|
307
|
+
|
308
|
+
In order to detect these wiring issues, stick a spec in your test suite like this:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
RSpec.describe 'InteractorWiring' do
|
312
|
+
it 'validates the interactors in the whole app', :aggregate_failures do
|
313
|
+
errors = Interactify.validate_app(ignore: [/Priam/])
|
314
|
+
|
315
|
+
expect(errors).to eq ''
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
```
|
321
|
+
Missing keys: :order_id
|
322
|
+
in: AssignOrderToUser
|
323
|
+
for: PlaceOrder
|
324
|
+
```
|
325
|
+
|
326
|
+
This allows you to quickly see exactly where you missed assigning something to the context.
|
327
|
+
Combine with lambda debugging `->(ctx) { byebug if ctx.order_id.nil?},` in your chains to drop into the exact
|
328
|
+
location in the chain to find where to make the change.
|
329
|
+
|
330
|
+
### RSpec matchers
|
331
|
+
Easily add [low value, low cost](https://noelrappin.com/blog/2017/02/high-cost-tests-and-high-value-tests/) specs for your expects and promises.
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
expect(described_class).to expect_inputs(:order_id)
|
335
|
+
expect(described_class).to promise_outputs(:order)
|
211
336
|
```
|
212
337
|
|
213
338
|
### Sidekiq Jobs
|
@@ -222,18 +347,19 @@ class SomeInteractor
|
|
222
347
|
# ...
|
223
348
|
end
|
224
349
|
end
|
350
|
+
```
|
225
351
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
SomeInteractorJob.perform_async(*args)
|
352
|
+
```diff
|
353
|
+
- SomeInteractor.call(*args)
|
354
|
+
+ class SomeInteractorJob
|
355
|
+
+ include Sidekiq::Job
|
356
|
+
+
|
357
|
+
+ def perform(*args)
|
358
|
+
+ SomeInteractor.call(*args)
|
359
|
+
+ end
|
360
|
+
+ end
|
361
|
+
+
|
362
|
+
+ SomeInteractorJob.perform_async(*args)
|
237
363
|
```
|
238
364
|
|
239
365
|
```ruby
|
@@ -245,14 +371,20 @@ class SomeInteractor
|
|
245
371
|
# ...
|
246
372
|
end
|
247
373
|
end
|
374
|
+
```
|
248
375
|
|
249
|
-
|
250
|
-
SomeInteractor::Async.call(*args)
|
376
|
+
No need to manually create a job class or handle the perform/call impedance mismatch
|
251
377
|
|
252
|
-
|
253
|
-
|
378
|
+
```diff
|
379
|
+
- SomeInteractor.call!(*args)
|
380
|
+
+ SomeInteractor::Async.call!(*args)
|
254
381
|
```
|
255
382
|
|
383
|
+
This also makes it easy to add cron jobs to run interactors. As any interactor can be asyncified.
|
384
|
+
By using it's internal Async class.
|
385
|
+
|
386
|
+
N.B. as your class is now executing asynchronously you can no longer rely on its promises later on in the chain.
|
387
|
+
|
256
388
|
## FAQs
|
257
389
|
- This is ugly isn't it?
|
258
390
|
|
@@ -268,96 +400,33 @@ class OuterOrganizer
|
|
268
400
|
)
|
269
401
|
end
|
270
402
|
```
|
403
|
+
1. Do you find the syntax of OuterOrganizer ugly?
|
271
404
|
|
272
|
-
|
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
|
405
|
+
While the syntax might seem unconventional initially, its conceptual elegance lies in streamlining complex interactor chains. Traditional methods often involve navigating through multiple files, creating a hidden and cumbersome architecture. This gem aims to alleviate that by centralizing operations, making the overall process more intuitive.
|
321
406
|
|
322
|
-
|
407
|
+
2. Is this compatible with interactor/interactor-contracts?
|
323
408
|
|
324
|
-
|
409
|
+
Yes, it's fully compatible. We currently use these as dependencies. While there's a possibility of future changes, maintaining this compatibility is a priority.
|
325
410
|
|
326
|
-
|
411
|
+
3. Why not suggest enhancements to the interactor or interactor-contracts gems?
|
327
412
|
|
328
|
-
|
413
|
+
These gems are excellent in their own right, which is why we've built upon them. Proposing such extensive changes might not align with their current philosophy. However, if our approach proves stable and garners interest, we're open to discussing potential contributions to these gems.
|
329
414
|
|
330
|
-
|
415
|
+
4. Is this just syntactic sugar?
|
331
416
|
|
332
|
-
|
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
|
-
```
|
417
|
+
It's more than that. This approach enhances readability and comprehension of the code. It simplifies the structure, making it easier to navigate and maintain.
|
350
418
|
|
351
|
-
|
419
|
+
5. Is the new DSL/syntax easier to understand than plain old Ruby objects (POROs)?
|
352
420
|
|
353
|
-
|
421
|
+
This is subjective, but we believe it is. It reduces the need for numerous files addressing common interactor issues, thereby streamlining the workflow.
|
354
422
|
|
355
|
-
|
423
|
+
6. Doesn't this approach become verbose and complex in large applications?
|
356
424
|
|
357
|
-
|
425
|
+
While it may appear so, this method shines in large-scale applications with numerous interactors. It simplifies locating and modifying the necessary interactors, which is often a cumbersome process.
|
358
426
|
|
359
|
-
|
427
|
+
7. What if I prefer using Service Objects?
|
360
428
|
|
429
|
+
That's completely valid. Service Objects have their merits, but this gem is particularly useful for those deeply engaged with interactors. It capitalizes on the chaining, contracts, simplicity, composability, and testability that interactors offer. Combining Service Objects with interactors might not retain these advantages as effectively.
|
361
430
|
## License
|
362
431
|
|
363
432
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
class AsyncJobKlass
|
5
|
+
attr_reader :container_klass, :klass_suffix
|
6
|
+
|
7
|
+
def initialize(container_klass:, klass_suffix:)
|
8
|
+
@container_klass = container_klass
|
9
|
+
@klass_suffix = klass_suffix
|
10
|
+
end
|
11
|
+
|
12
|
+
def async_job_klass
|
13
|
+
klass = Class.new do
|
14
|
+
include Interactor
|
15
|
+
include Interactor::Contracts
|
16
|
+
end
|
17
|
+
|
18
|
+
attach_call(klass)
|
19
|
+
attach_call!(klass)
|
20
|
+
|
21
|
+
klass
|
22
|
+
end
|
23
|
+
|
24
|
+
def attach_call(async_job_klass)
|
25
|
+
# e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
|
26
|
+
async_job_klass.send(:define_singleton_method, :call) do |context|
|
27
|
+
call!(context)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def attach_call!(async_job_klass)
|
32
|
+
this = self
|
33
|
+
|
34
|
+
# e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
|
35
|
+
async_job_klass.send(:define_singleton_method, :call!) do |context|
|
36
|
+
# e.g. SomeInteractor::JobWithSuffix
|
37
|
+
job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
|
38
|
+
|
39
|
+
# e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
|
40
|
+
job_klass.perform_async(this.args(context))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def args(context)
|
45
|
+
args = context.to_h.stringify_keys
|
46
|
+
|
47
|
+
return args unless container_klass.respond_to?(:expected_keys)
|
48
|
+
|
49
|
+
restrict_to_optional_or_keys_from_contract(args)
|
50
|
+
end
|
51
|
+
|
52
|
+
def restrict_to_optional_or_keys_from_contract(args)
|
53
|
+
keys = container_klass.expected_keys.map(&:to_s)
|
54
|
+
|
55
|
+
optional = Array(container_klass.optional_attrs).map(&:to_s)
|
56
|
+
keys += optional
|
57
|
+
|
58
|
+
args.slice(*keys)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/jobable"
|
4
|
+
require "interactify/call_wrapper"
|
5
|
+
require "interactify/organizer"
|
6
|
+
require "interactify/contract_failure"
|
4
7
|
|
5
8
|
module Interactify
|
6
9
|
module ContractHelpers
|
@@ -39,16 +42,13 @@ module Interactify
|
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
42
|
-
class ContractFailure < ::Interactor::Failure
|
43
|
-
end
|
44
|
-
|
45
45
|
included do
|
46
46
|
c = Class.new(ContractFailure)
|
47
|
-
# example self is
|
48
|
-
# failure class:
|
49
|
-
const_set
|
47
|
+
# example self is Whatever::SomeInteractor
|
48
|
+
# failure class: Whatever::SomeInteractor::InteractorContractFailure
|
49
|
+
const_set "InteractorContractFailure", c
|
50
50
|
prepend CallWrapper
|
51
|
-
include
|
51
|
+
include Organizer
|
52
52
|
|
53
53
|
on_breach do |breaches|
|
54
54
|
breaches = breaches.map { |b| { b.property => b.messages } }.inject(&:merge)
|
data/lib/interactify/dsl.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/each_chain"
|
4
|
+
require "interactify/if_interactor"
|
5
|
+
require "interactify/unique_klass_name"
|
3
6
|
|
4
7
|
module Interactify
|
5
8
|
module Dsl
|
@@ -61,6 +64,7 @@ module Interactify
|
|
61
64
|
|
62
65
|
# attach the class to the calling namespace
|
63
66
|
where_to_attach = self.binding.receiver
|
67
|
+
klass_name = UniqueKlassName.for(where_to_attach, klass_name)
|
64
68
|
where_to_attach.const_set(klass_name, klass)
|
65
69
|
end
|
66
70
|
end
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/unique_klass_name"
|
4
|
+
|
1
5
|
module Interactify
|
2
6
|
class EachChain
|
3
7
|
attr_reader :each_loop_klasses, :plural_resource_name, :evaluating_receiver
|
@@ -36,7 +40,7 @@ module Interactify
|
|
36
40
|
context[this.singular_resource_name] = resource # context.package = package
|
37
41
|
context[this.singular_resource_index_name] = index # context.package_index = index
|
38
42
|
|
39
|
-
klasses =
|
43
|
+
klasses = InteractorWrapper.wrap_many(self, this.each_loop_klasses)
|
40
44
|
|
41
45
|
klasses.each do |interactor| # [A, B, C].each do |interactor|
|
42
46
|
interactor.call!(context) # interactor.call!(context)
|
@@ -57,8 +61,10 @@ module Interactify
|
|
57
61
|
# rubocop:enable all
|
58
62
|
|
59
63
|
def attach_klass
|
60
|
-
|
61
|
-
|
64
|
+
name = iterator_klass_name
|
65
|
+
|
66
|
+
namespace.const_set(name, klass)
|
67
|
+
namespace.const_get(name)
|
62
68
|
end
|
63
69
|
|
64
70
|
def namespace
|
@@ -66,7 +72,9 @@ module Interactify
|
|
66
72
|
end
|
67
73
|
|
68
74
|
def iterator_klass_name
|
69
|
-
|
75
|
+
prefix = "Each#{singular_resource_name.to_s.camelize}"
|
76
|
+
|
77
|
+
UniqueKlassName.for(namespace, prefix)
|
70
78
|
end
|
71
79
|
|
72
80
|
def singular_resource_name
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/unique_klass_name"
|
4
|
+
|
1
5
|
module Interactify
|
2
6
|
class IfInteractor
|
3
7
|
attr_reader :condition, :success_interactor, :failure_interactor, :evaluating_receiver
|
@@ -47,8 +51,9 @@ module Interactify
|
|
47
51
|
# rubocop:enable all
|
48
52
|
|
49
53
|
def attach_klass
|
50
|
-
|
51
|
-
namespace.
|
54
|
+
name = if_klass_name
|
55
|
+
namespace.const_set(name, klass)
|
56
|
+
namespace.const_get(name)
|
52
57
|
end
|
53
58
|
|
54
59
|
def namespace
|
@@ -56,9 +61,10 @@ module Interactify
|
|
56
61
|
end
|
57
62
|
|
58
63
|
def if_klass_name
|
59
|
-
|
64
|
+
prefix = condition.is_a?(Proc) ? "Proc" : condition
|
65
|
+
prefix = "If#{prefix.to_s.camelize}"
|
60
66
|
|
61
|
-
|
67
|
+
UniqueKlassName.for(namespace, prefix)
|
62
68
|
end
|
63
69
|
end
|
64
70
|
end
|