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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bbf811a8d9e44f85d4e0c5b36f4ad3291cbc8f5aa52e5d2dd31ecbdd753a256
4
- data.tar.gz: 92ed7413e2ea370d388c43d9993928652d9bc23a3139d3fa083050243bc30695
3
+ metadata.gz: 918f4f6ae335bb5b8a748603af6a2e9d91dc11ad3d887803bbb2c7b2ba41730e
4
+ data.tar.gz: fe6506fe246a82d48f5a614d5c576cf74e49f38953c1d4ae98d52f1649f03aaf
5
5
  SHA512:
6
- metadata.gz: 1d9e01fc12b9b02bdc9d897b81866c7d668f94cf0fafd51e018cded3ec2892d5452848000672a9c4ac139f1c1dc3d30aa23e1c16b0dcaf66afd901c4586d8eae
7
- data.tar.gz: b33dc94b10b26cf1b4baeace46168641d88121bf05fe2a4c8c37098d3b5bc678667e0ff253f620def1798846b9b95533f943ef4c80726abe08a1a41662c338c0
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
- 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.
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
- Interactify wraps the interactor and interactor-contract gem and provides additional functionality making chaining and understanding interactor chains easier.
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's not possible to use lambdas in organizers, and sometimes we only want a lambda.
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 (Single Responsibility Principle).
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
- ### More Conditionals
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
- 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)
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
- # no need to manually create a job class or handle the perform/call impedance mismatch
250
- SomeInteractor::Async.call(*args)
376
+ No need to manually create a job class or handle the perform/call impedance mismatch
251
377
 
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.
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
- 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
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
- 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.
407
+ 2. Is this compatible with interactor/interactor-contracts?
323
408
 
324
- Install the gem and add to the application's Gemfile by executing:
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
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
411
+ 3. Why not suggest enhancements to the interactor or interactor-contracts gems?
327
412
 
328
- If bundler is not being used to manage dependencies, install the gem by executing:
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
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
415
+ 4. Is this just syntactic sugar?
331
416
 
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
- ```
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
- ## Development
419
+ 5. Is the new DSL/syntax easier to understand than plain old Ruby objects (POROs)?
352
420
 
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.
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
- 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).
423
+ 6. Doesn't this approach become verbose and complex in large applications?
356
424
 
357
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/interactify.
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Interactify
2
4
  module CallWrapper
3
5
  # https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L114
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ class ContractFailure < ::Interactor::Failure
5
+ end
6
+ end
@@ -1,6 +1,9 @@
1
- require 'interactify/jobable'
2
- require 'interactify/call_wrapper'
3
- require 'interactify/organizer_call_monkey_patch'
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 Shopkeeper::Fetch
48
- # failure class: Shopkeeper::Fetch::InteractorContractFailure
49
- const_set 'InteractorContractFailure', c
47
+ # example self is Whatever::SomeInteractor
48
+ # failure class: Whatever::SomeInteractor::InteractorContractFailure
49
+ const_set "InteractorContractFailure", c
50
50
  prepend CallWrapper
51
- include OrganizerCallMonkeyPatch if ancestors.include? Interactor::Organizer
51
+ include Organizer
52
52
 
53
53
  on_breach do |breaches|
54
54
  breaches = breaches.map { |b| { b.property => b.messages } }.inject(&:merge)
@@ -1,5 +1,8 @@
1
- require 'interactify/each_chain'
2
- require 'interactify/if_interactor'
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 = self.class.wrap_lambdas_in_interactors(this.each_loop_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
- namespace.const_set(iterator_klass_name, klass)
61
- namespace.const_get(iterator_klass_name)
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
- :"Each#{singular_resource_name.to_s.camelize}".to_sym
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
- namespace.const_set(if_klass_name, klass)
51
- namespace.const_get(if_klass_name)
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
- name = condition.is_a?(Proc) ? 'Proc' : condition
64
+ prefix = condition.is_a?(Proc) ? "Proc" : condition
65
+ prefix = "If#{prefix.to_s.camelize}"
60
66
 
61
- "If#{name.to_s.camelize}".to_sym
67
+ UniqueKlassName.for(namespace, prefix)
62
68
  end
63
69
  end
64
70
  end