interactify 0.1.0.pre.alpha.1 → 0.3.0.pre.RC1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +23 -0
  3. data/.ruby-version +1 -0
  4. data/Appraisals +21 -0
  5. data/CHANGELOG.md +18 -1
  6. data/README.md +178 -126
  7. data/gemfiles/.bundle/config +2 -0
  8. data/gemfiles/no_railties_no_sidekiq.gemfile +16 -0
  9. data/gemfiles/no_railties_no_sidekiq.gemfile.lock +127 -0
  10. data/gemfiles/railties_6.gemfile +14 -0
  11. data/gemfiles/railties_6.gemfile.lock +253 -0
  12. data/gemfiles/railties_6_no_sidekiq.gemfile +17 -0
  13. data/gemfiles/railties_6_no_sidekiq.gemfile.lock +158 -0
  14. data/gemfiles/railties_6_sidekiq.gemfile +18 -0
  15. data/gemfiles/railties_6_sidekiq.gemfile.lock +167 -0
  16. data/gemfiles/railties_7_no_sidekiq.gemfile +17 -0
  17. data/gemfiles/railties_7_no_sidekiq.gemfile.lock +157 -0
  18. data/gemfiles/railties_7_sidekiq.gemfile +18 -0
  19. data/gemfiles/railties_7_sidekiq.gemfile.lock +166 -0
  20. data/lib/interactify/async_job_klass.rb +61 -0
  21. data/lib/interactify/call_wrapper.rb +2 -0
  22. data/lib/interactify/contract_failure.rb +6 -0
  23. data/lib/interactify/contract_helpers.rb +10 -10
  24. data/lib/interactify/dsl.rb +6 -2
  25. data/lib/interactify/each_chain.rb +12 -4
  26. data/lib/interactify/if_interactor.rb +10 -4
  27. data/lib/interactify/interactor_wiring/callable_representation.rb +79 -0
  28. data/lib/interactify/interactor_wiring/constants.rb +125 -0
  29. data/lib/interactify/interactor_wiring/error_context.rb +41 -0
  30. data/lib/interactify/interactor_wiring/files.rb +51 -0
  31. data/lib/interactify/interactor_wiring.rb +57 -272
  32. data/lib/interactify/interactor_wrapper.rb +72 -0
  33. data/lib/interactify/job_maker.rb +20 -69
  34. data/lib/interactify/jobable.rb +11 -7
  35. data/lib/interactify/mismatching_promise_error.rb +17 -0
  36. data/lib/interactify/null_job.rb +11 -0
  37. data/lib/interactify/organizer.rb +30 -0
  38. data/lib/interactify/promising.rb +34 -0
  39. data/lib/interactify/rspec/matchers.rb +9 -11
  40. data/lib/interactify/unique_klass_name.rb +21 -0
  41. data/lib/interactify/version.rb +1 -1
  42. data/lib/interactify.rb +96 -9
  43. metadata +36 -23
  44. 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: 7bbf811a8d9e44f85d4e0c5b36f4ad3291cbc8f5aa52e5d2dd31ecbdd753a256
4
- data.tar.gz: 92ed7413e2ea370d388c43d9993928652d9bc23a3139d3fa083050243bc30695
3
+ metadata.gz: 319dd56313d82aa987c8eaec40691e43c58e8b72513969e2a6a7a83de17ae311
4
+ data.tar.gz: 3f5e4ad0464157910bca7bfd1300fa8e23ab825068c84a4c5c0ef5ad0cdb29d6
5
5
  SHA512:
6
- metadata.gz: 1d9e01fc12b9b02bdc9d897b81866c7d668f94cf0fafd51e018cded3ec2892d5452848000672a9c4ac139f1c1dc3d30aa23e1c16b0dcaf66afd901c4586d8eae
7
- data.tar.gz: b33dc94b10b26cf1b4baeace46168641d88121bf05fe2a4c8c37098d3b5bc678667e0ff253f620def1798846b9b95533f943ef4c80726abe08a1a41662c338c0
6
+ metadata.gz: fea0120a4456f108b41ccd48c994e365762bad8ff6424eba9d7ecd0c09c6858805e0456df0655eec86042ed7dc9fa589ffb8f3bdb49a14e79a7e05e30b6737ae
7
+ data.tar.gz: 64345da2e8b036ad65c639bddb9c0f90f93b1fdbc1415a5cb29f26596fa9169c6a9b0309d9d0ecbb515706251ff473f4ac98f7b41be7d89c089ce3b8537ade54
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/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.4
data/Appraisals ADDED
@@ -0,0 +1,21 @@
1
+ appraise "railties-7-sidekiq" do
2
+ gem "railties", "7"
3
+ gem "sidekiq", "7"
4
+ end
5
+
6
+ appraise "railties-7-no-sidekiq" do
7
+ gem "railties", "7"
8
+ end
9
+
10
+ appraise "railties-6-sidekiq" do
11
+ gem "railties", "6"
12
+ gem "sidekiq", "7"
13
+ end
14
+
15
+ appraise "railties-6-no-sidekiq" do
16
+ gem "railties", "6"
17
+ end
18
+
19
+ appraise "no-railties-no-sidekiq" do
20
+ # nothing extra
21
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
14
+
15
+ ## [0.3.0-alpha.2] - 2023-12-29
16
+
17
+ - Remove deep_matching development dependency
18
+
19
+ ## [0.3.0-RC1] - 2023-12-29
20
+
21
+ - Fixed to work with and make optional dependencies for sidekiq and railties. Confirmed as working with ruby >= 3.1.4
22
+
data/README.md CHANGED
@@ -1,11 +1,59 @@
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
+ [![Gem Version](https://badge.fury.io/rb/interactify.svg)](https://badge.fury.io/rb/interactify)
4
+ ![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=markburns/interactify)
5
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+ ![Ruby 3.3.0](https://img.shields.io/badge/ruby-3.3.0-green.svg)
7
+ ![Ruby 3.2.2](https://img.shields.io/badge/ruby-3.2.2-green.svg)
8
+ ![Ruby 3.1.4](https://img.shields.io/badge/ruby-3.1.4-green.svg)
9
+ [![Code Climate](https://codeclimate.com/github/markburns/interactify/badges/gpa.svg)](https://codeclimate.com/github/markburns/interactify)
10
+
11
+ Interactify enhances Rails applications by simplifying complex interactor chains.
12
+ 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.
13
+ We depend on activesupport, and optionally on railties and sidekiq. So it's a good fit for Rails projects using Sidekiq, offering advanced features for chain management and debugging.
14
+ Interactify is about making interactor usage in Rails more efficient and less error-prone, reducing the overhead of traditional interactor orchestration.
5
15
 
6
- Interactify wraps the interactor and interactor-contract gem and provides additional functionality making chaining and understanding interactor chains easier.
16
+ ## Installation
17
+
18
+ ```ruby
19
+ gem 'interactify'
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Initializer
25
+
26
+ ```ruby
27
+ # in config/initializers/interactify.rb
28
+ Interactify.configure do |config|
29
+ # default
30
+ # config.root = Rails.root / 'app'
31
+ end
32
+
33
+ Interactify.on_contract_breach do |context, attrs|
34
+ # maybe add context to Sentry or Honeybadger etc here
35
+ end
36
+
37
+ Interactify.before_raise do |exception|
38
+ # maybe add context to Sentry or Honeybadger etc here
39
+ end
40
+ ```
41
+
42
+ ### Using the RSpec matchers
43
+ ```ruby
44
+ # e.g. in spec/supoort/interactify.rb
45
+ require 'interactify/rspec/matchers'
46
+
47
+ expect(described_class).to expect_inputs(:foo, :bar, :baz)
48
+ expect(described_class).to promise_outputs(:fee, :fi, :fo, :fum)
49
+ ```
7
50
 
8
51
  ### Syntactic Sugar
52
+ - Everything is an Organizer/Interactor and supports interactor-contracts.
53
+ - They only becomes considered an organizer once `organize` is called.
54
+ - They could technically be both (if you want?) but you have to remember to call `super` within `call` to trigger the organized interactors.
55
+ - Concise syntax for most common scenarios with `expects` and `promises`. Verifying the presence of the keys/values.
56
+ - Automatic delegation of expected and promised keys to the context.
9
57
 
10
58
  ```ruby
11
59
  # before
@@ -16,13 +64,14 @@ class LoadOrder
16
64
 
17
65
  expects do
18
66
  required(:id).filled
67
+ required(:something_else).filled
68
+ required(:a_boolean_flag)
19
69
  end
20
70
 
21
71
  promises do
22
72
  required(:order).filled
23
73
  end
24
74
 
25
-
26
75
  def call
27
76
  context.order = Order.find(context.id)
28
77
  end
@@ -35,7 +84,8 @@ end
35
84
  class LoadOrder
36
85
  include Interactify
37
86
 
38
- expect :id
87
+ expect :id, :something_else
88
+ expect :a_boolean_flag, filled: false
39
89
  promise :order
40
90
 
41
91
  def call
@@ -47,8 +97,8 @@ end
47
97
 
48
98
  ### Lambdas
49
99
 
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.
100
+ With vanilla interactors, it wasn't possible to use lambdas in organizers.
101
+ But sometimes we only want a lambda. So we added support.
52
102
 
53
103
  ```ruby
54
104
  organize LoadOrder, ->(context) { context.order = context.order.decorate }
@@ -63,7 +113,7 @@ organize \
63
113
 
64
114
  Sometimes we want an interactor for each item in a collection.
65
115
  But it gets unwieldy.
66
- It was complex procedural code and is now broken into neat SRP classes (Single Responsibility Principle).
116
+ It was complex procedural code and is now broken into neat [SRP classes](https://en.wikipedia.org/wiki/Single_responsibility_principle).
67
117
  But there is still boilerplate and jumping around between files to follow the orchestration.
68
118
  It's easy to get lost in the orchestration code that occurs across say 7 or 8 files.
69
119
 
@@ -115,7 +165,6 @@ class DoSomethingWithOrder
115
165
  end
116
166
  ```
117
167
 
118
-
119
168
  ```ruby
120
169
  # after
121
170
  class OuterOrganizer
@@ -136,7 +185,6 @@ class LoadOrder
136
185
  end
137
186
  end
138
187
 
139
-
140
188
  class DoSomethingWithOrder
141
189
  # ... boilerplate ...
142
190
  def call
@@ -145,7 +193,7 @@ class DoSomethingWithOrder
145
193
  end
146
194
  ```
147
195
 
148
- ### Conditionals (if/else)
196
+ ### Conditionals (if/else) with lambda
149
197
 
150
198
  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
199
 
@@ -170,26 +218,22 @@ class InnerThing
170
218
  end
171
219
  ```
172
220
 
173
-
174
221
  ```ruby
175
222
  # after
176
223
  class OuterThing
177
224
  # ... boilerplate ...
178
225
  organize \
179
226
  SetupStep,
180
- self.if(->(c){ c.thing == 'a' }, DoThingA, DoThingB),
181
- end
182
227
 
183
- ```
228
+ # lambda conditional
229
+ self.if(->(c){ c.thing == 'a' }, DoThingA, DoThingB),
184
230
 
185
- ### More Conditionals
231
+ # context conditional
232
+ self.if(:some_key_on_context, DoThingA, DoThingB),
186
233
 
187
- ```ruby
188
- class OuterThing
189
- # ... boilerplate ...
190
- organize \
191
- self.if(:key_set_on_context, DoThingA, DoThingB),
192
- AfterBothCases
234
+ # alternative hash syntax
235
+ {if: :key_set_on_context, then: DoThingA, else: DoThingB},
236
+ AfterDoThis
193
237
  end
194
238
  ```
195
239
 
@@ -210,49 +254,120 @@ end
210
254
 
211
255
  ```
212
256
 
213
- ### Sidekiq Jobs
214
- Sometimes you want to asyncify an interactor.
257
+ ### Contract validation failures
258
+ 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.
259
+ If the context is large it's often hard to spot what the actual problem is or where it occurred.
260
+
261
+ #### before
262
+ ```
263
+ Interactor::Failure
264
+
265
+ #<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)
266
+ , tasks=[]>
267
+ ```
268
+
269
+ #### after with call
270
+ ```
271
+ #<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)
272
+ , tasks=[], contract_failures={:tasks=>["tasks must be filled"]}>
273
+ ```
274
+
275
+ #### after with call!
276
+ ```
277
+ #<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)
278
+ , tasks=[], contract_failures={:tasks=>["tasks must be filled"]}>
279
+ ```
280
+
281
+ ### Promising
282
+ You can annotate your interactors in the organize arguments with their promises.
283
+ This then acts as executable documentation that is validated at load time and enforced to stay in sync with the interactor.
284
+
285
+ A writer of an organizer may quite reasonably expect `LoadOrder` to promise `:order`, but for the reader, it's not always as immediately obvious
286
+ which interactor in the chain is responsible for provides which key.
215
287
 
216
288
  ```ruby
217
- # before
218
- class SomeInteractor
219
- include Interactify
289
+ organize \
290
+ LoadOrder.promising(:order),
291
+ TakePayment.promising(:payment_transaction)
292
+ ```
220
293
 
221
- def call
222
- # ...
223
- end
224
- end
294
+ This will be validated at load time against the interactors promises.
295
+ An example of a failure would be:
296
+
297
+ ```
298
+ SomeOrganizer::DoStep1 does not promise:
299
+ step_1
300
+
301
+ Actual promises are:
302
+ step1
303
+ ```
304
+
305
+ ### Interactor wiring specs
306
+ Sometimes you have an interactor chain that fails because something is expected deeper down the chain and not provided further up the chain.
307
+ 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.
308
+
309
+ But we can do better than that if we always `promise` something that is later `expect`ed.
310
+
311
+ In order to detect these wiring issues, stick a spec in your test suite like this:
225
312
 
226
- clsas SomeInteractorJob
227
- include Sidekiq::Job
313
+ ```ruby
314
+ RSpec.describe 'InteractorWiring' do
315
+ it 'validates the interactors in the whole app', :aggregate_failures do
316
+ errors = Interactify.validate_app(ignore: [/Priam/])
228
317
 
229
- def perform(*args)
230
- SomeInteractor.call(*args)
318
+ expect(errors).to eq ''
231
319
  end
232
320
  end
321
+ ```
233
322
 
234
- SomeInteractor.call(*args)
235
- code is changed to
236
- SomeInteractorJob.perform_async(*args)
237
323
  ```
324
+ Missing keys: :order_id
325
+ in: AssignOrderToUser
326
+ for: PlaceOrder
327
+ ```
328
+
329
+ This allows you to quickly see exactly where you missed assigning something to the context.
330
+ Combine with lambda debugging `->(ctx) { byebug if ctx.order_id.nil?},` in your chains to drop into the exact
331
+ location in the chain to find where to make the change.
332
+
333
+ ### RSpec matchers
334
+ 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.
238
335
 
239
336
  ```ruby
240
- # after
241
- class SomeInteractor
242
- include Interactify
337
+ expect(described_class).to expect_inputs(:order_id)
338
+ expect(described_class).to promise_outputs(:order)
339
+ ```
243
340
 
244
- def call
245
- # ...
246
- end
247
- end
341
+ ### Sidekiq Jobs
342
+ Sometimes you want to asyncify an interactor.
248
343
 
249
- # no need to manually create a job class or handle the perform/call impedance mismatch
250
- SomeInteractor::Async.call(*args)
344
+ #### before
345
+ ```diff
346
+ - SomeInteractor.call(*args)
347
+ + class SomeInteractorJob
348
+ + include Sidekiq::Job
349
+ +
350
+ + def perform(*args)
351
+ + SomeInteractor.call(*args)
352
+ + end
353
+ + end
354
+ +
355
+ + SomeInteractorJob.perform_async(*args)
356
+ ```
251
357
 
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.
358
+ #### after
359
+ ```diff
360
+ - SomeInteractor.call!(*args)
361
+ + SomeInteractor::Async.call!(*args)
254
362
  ```
255
363
 
364
+ No need to manually create a job class or handle the perform/call impedance mismatch
365
+
366
+ This also makes it easy to add cron jobs to run interactors. As any interactor can be asyncified.
367
+ By using it's internal Async class.
368
+
369
+ N.B. as your class is now executing asynchronously you can no longer rely on its promises later on in the chain.
370
+
256
371
  ## FAQs
257
372
  - This is ugly isn't it?
258
373
 
@@ -268,96 +383,33 @@ class OuterOrganizer
268
383
  )
269
384
  end
270
385
  ```
386
+ 1. Do you find the syntax of OuterOrganizer ugly?
271
387
 
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.
388
+ 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.
277
389
 
390
+ 2. Is this compatible with interactor/interactor-contracts?
278
391
 
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.
392
+ 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.
283
393
 
284
- - Isn't this all just syntactic sugar?
285
- Yes, but it's sugar that makes the code easier to read and understand.
394
+ 3. Why not suggest enhancements to the interactor or interactor-contracts gems?
286
395
 
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.
396
+ 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.
289
397
 
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.
398
+ 4. Is this just syntactic sugar?
296
399
 
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
- ```
400
+ 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
401
 
351
- ## Development
402
+ 5. Is the new DSL/syntax easier to understand than plain old Ruby objects (POROs)?
352
403
 
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.
404
+ This is subjective, but we believe it is. It reduces the need for numerous files addressing common interactor issues, thereby streamlining the workflow.
354
405
 
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).
406
+ 6. Doesn't this approach become verbose and complex in large applications?
356
407
 
357
- ## Contributing
408
+ 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
409
 
359
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/interactify.
410
+ 7. What if I prefer using Service Objects?
360
411
 
412
+ 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
413
  ## License
362
414
 
363
415
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+
7
+ group :development do
8
+ gem "bundler", "~> 2.0"
9
+ end
10
+
11
+ group :test do
12
+ gem "simplecov", require: false
13
+ gem "rspec", "~> 3.0"
14
+ end
15
+
16
+ gemspec path: "../"
@@ -0,0 +1,127 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ interactify (0.3.0.pre.alpha.2)
5
+ activesupport (>= 6.0.0)
6
+ interactor
7
+ interactor-contracts
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activesupport (7.1.2)
13
+ base64
14
+ bigdecimal
15
+ concurrent-ruby (~> 1.0, >= 1.0.2)
16
+ connection_pool (>= 2.2.5)
17
+ drb
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ mutex_m
21
+ tzinfo (~> 2.0)
22
+ appraisal (2.5.0)
23
+ bundler
24
+ rake
25
+ thor (>= 0.14.0)
26
+ base64 (0.2.0)
27
+ bigdecimal (3.1.5)
28
+ concurrent-ruby (1.2.2)
29
+ connection_pool (2.4.1)
30
+ debug (1.9.1)
31
+ irb (~> 1.10)
32
+ reline (>= 0.3.8)
33
+ diff-lcs (1.5.0)
34
+ docile (1.4.0)
35
+ drb (2.2.0)
36
+ ruby2_keywords
37
+ dry-configurable (1.0.1)
38
+ dry-core (~> 1.0, < 2)
39
+ zeitwerk (~> 2.6)
40
+ dry-core (1.0.0)
41
+ concurrent-ruby (~> 1.0)
42
+ zeitwerk (~> 2.6)
43
+ dry-inflector (1.0.0)
44
+ dry-initializer (3.1.1)
45
+ dry-logic (1.5.0)
46
+ concurrent-ruby (~> 1.0)
47
+ dry-core (~> 1.0, < 2)
48
+ zeitwerk (~> 2.6)
49
+ dry-schema (1.13.3)
50
+ concurrent-ruby (~> 1.0)
51
+ dry-configurable (~> 1.0, >= 1.0.1)
52
+ dry-core (~> 1.0, < 2)
53
+ dry-initializer (~> 3.0)
54
+ dry-logic (>= 1.4, < 2)
55
+ dry-types (>= 1.7, < 2)
56
+ zeitwerk (~> 2.6)
57
+ dry-types (1.7.1)
58
+ concurrent-ruby (~> 1.0)
59
+ dry-core (~> 1.0)
60
+ dry-inflector (~> 1.0)
61
+ dry-logic (~> 1.4)
62
+ zeitwerk (~> 2.6)
63
+ dry-validation (1.10.0)
64
+ concurrent-ruby (~> 1.0)
65
+ dry-core (~> 1.0, < 2)
66
+ dry-initializer (~> 3.0)
67
+ dry-schema (>= 1.12, < 2)
68
+ zeitwerk (~> 2.6)
69
+ i18n (1.14.1)
70
+ concurrent-ruby (~> 1.0)
71
+ interactor (3.1.2)
72
+ interactor-contracts (0.3.0)
73
+ dry-validation (~> 1.0)
74
+ interactor (~> 3)
75
+ io-console (0.7.1)
76
+ irb (1.11.0)
77
+ rdoc
78
+ reline (>= 0.3.8)
79
+ minitest (5.20.0)
80
+ mutex_m (0.2.0)
81
+ psych (5.1.2)
82
+ stringio
83
+ rake (13.1.0)
84
+ rdoc (6.6.2)
85
+ psych (>= 4.0.0)
86
+ reline (0.4.1)
87
+ io-console (~> 0.5)
88
+ rspec (3.12.0)
89
+ rspec-core (~> 3.12.0)
90
+ rspec-expectations (~> 3.12.0)
91
+ rspec-mocks (~> 3.12.0)
92
+ rspec-core (3.12.2)
93
+ rspec-support (~> 3.12.0)
94
+ rspec-expectations (3.12.3)
95
+ diff-lcs (>= 1.2.0, < 2.0)
96
+ rspec-support (~> 3.12.0)
97
+ rspec-mocks (3.12.6)
98
+ diff-lcs (>= 1.2.0, < 2.0)
99
+ rspec-support (~> 3.12.0)
100
+ rspec-support (3.12.1)
101
+ ruby2_keywords (0.0.5)
102
+ simplecov (0.22.0)
103
+ docile (~> 1.1)
104
+ simplecov-html (~> 0.11)
105
+ simplecov_json_formatter (~> 0.1)
106
+ simplecov-html (0.12.3)
107
+ simplecov_json_formatter (0.1.4)
108
+ stringio (3.1.0)
109
+ thor (1.3.0)
110
+ tzinfo (2.0.6)
111
+ concurrent-ruby (~> 1.0)
112
+ zeitwerk (2.6.12)
113
+
114
+ PLATFORMS
115
+ ruby
116
+
117
+ DEPENDENCIES
118
+ appraisal
119
+ bundler (~> 2.0)
120
+ debug
121
+ interactify!
122
+ rake (~> 13.0)
123
+ rspec (~> 3.0)
124
+ simplecov
125
+
126
+ BUNDLED WITH
127
+ 2.4.22
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "simplecov", require: false
6
+ gem "rake", "~> 13.0"
7
+ gem "railties", "6"
8
+ gem "sidekiq", "7"
9
+
10
+ group :test do
11
+ gem "rspec", "~> 3.0"
12
+ end
13
+
14
+ gemspec path: "../"