interactify 0.2.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: 4c2dffe1928c3b1653ea12057fd660eacbef1d8fbd946d1bdbddef00494a8c8b
4
- data.tar.gz: 7078fe7a6b217febd47f4598eb2e88fdcfb8affcd434dba239ee86a39a072176
3
+ metadata.gz: 918f4f6ae335bb5b8a748603af6a2e9d91dc11ad3d887803bbb2c7b2ba41730e
4
+ data.tar.gz: fe6506fe246a82d48f5a614d5c576cf74e49f38953c1d4ae98d52f1649f03aaf
5
5
  SHA512:
6
- metadata.gz: 9e3eb106e5b896965c6b1fbfea135c62f4eb7f95779041c121bcb2040fd8568c54e94b4f77a3e1bcca6d922d8a8bdfcacda354d3a6396e19a49fbfce78f39af1
7
- data.tar.gz: 3f998d9f35105a47becc0555ed80dff264fad2e5fe58f93ec0c731b8cab4281e457da9e3fc00d0e4709e1b62b4e2416a8a75b66f56723b610531bca8fd458ea3
6
+ metadata.gz: ce377154a79b527f6eaebe3258d76aadd327521abbe86847b8c9d87b5710e1782bdb02b20b3ef3661ac6dc4ee1d0c6843fdd819ccf6dda8f701e9d95063d5a9c
7
+ data.tar.gz: 553271645575873bc2a95968c2ff7f01a556dd24527974b6a7bb958a8eeb9f8bb66504bdace2c3736e6faac82c1a438f07d27afa85c3fdc8ac5b051cac4e20c5
data/CHANGELOG.md CHANGED
@@ -1,9 +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
6
 
7
- ## [0.2.0] - 2023-12-27
7
+ ## [0.2.0-alpha.1] - 2023-12-27
8
8
 
9
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,18 +1,9 @@
1
1
  # Interactify
2
2
 
3
- [Interactors](https://github.com/collectiveidea/interactor) 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
- [interactor-contracts](https://github.com/michaelherold/interactor-contracts) does a fantastic job of making your interactor chains more reliable.
7
-
8
- Interactify wraps the interactor and interactor-contracts gem and provides additional functionality making chaining and understanding interactor chains easier.
9
-
10
- This is a bells and whistles gem and assumes you are working in a Rails project with Sidekiq.
11
- However, I'm open to the idea of making it more focused and making these more pluggable.
12
-
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.
13
4
  ## Installation
14
5
 
15
- ```Gemfile
6
+ ```ruby
16
7
  gem 'interactify'
17
8
  ```
18
9
 
@@ -39,10 +30,15 @@ end
39
30
  ```ruby
40
31
  # e.g. in spec/supoort/interactify.rb
41
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)
42
36
  ```
43
37
 
44
38
  ### Syntactic Sugar
45
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.
46
42
  - Concise syntax for most common scenarios with `expects` and `promises`. Verifying the presence of the keys/values.
47
43
  - Automatic delegation of expected and promised keys to the context.
48
44
 
@@ -63,7 +59,6 @@ class LoadOrder
63
59
  required(:order).filled
64
60
  end
65
61
 
66
-
67
62
  def call
68
63
  context.order = Order.find(context.id)
69
64
  end
@@ -89,8 +84,8 @@ end
89
84
 
90
85
  ### Lambdas
91
86
 
92
- With vanilla interactors, it's not possible to use lambdas in organizers, and sometimes we only want a lambda.
93
- 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.
94
89
 
95
90
  ```ruby
96
91
  organize LoadOrder, ->(context) { context.order = context.order.decorate }
@@ -157,7 +152,6 @@ class DoSomethingWithOrder
157
152
  end
158
153
  ```
159
154
 
160
-
161
155
  ```ruby
162
156
  # after
163
157
  class OuterOrganizer
@@ -178,7 +172,6 @@ class LoadOrder
178
172
  end
179
173
  end
180
174
 
181
-
182
175
  class DoSomethingWithOrder
183
176
  # ... boilerplate ...
184
177
  def call
@@ -187,7 +180,7 @@ class DoSomethingWithOrder
187
180
  end
188
181
  ```
189
182
 
190
- ### Conditionals (if/else)
183
+ ### Conditionals (if/else) with lambda
191
184
 
192
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.
193
186
 
@@ -212,7 +205,6 @@ class InnerThing
212
205
  end
213
206
  ```
214
207
 
215
-
216
208
  ```ruby
217
209
  # after
218
210
  class OuterThing
@@ -222,9 +214,16 @@ class OuterThing
222
214
  self.if(->(c){ c.thing == 'a' }, DoThingA, DoThingB),
223
215
  end
224
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
225
224
  ```
226
225
 
227
- ### More Conditionals
226
+ ### Conditionals with a key from the context
228
227
 
229
228
  ```ruby
230
229
  class OuterThing
@@ -348,19 +347,19 @@ class SomeInteractor
348
347
  # ...
349
348
  end
350
349
  end
351
-
352
- clsas SomeInteractorJob
353
- include Sidekiq::Job
354
-
355
- def perform(*args)
356
- SomeInteractor.call(*args)
357
- end
358
- end
359
350
  ```
360
351
 
361
352
  ```diff
362
- -SomeInteractor.call(*args)
363
- +SomeInteractorJob.perform_async(*args)
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)
364
363
  ```
365
364
 
366
365
  ```ruby
@@ -377,8 +376,8 @@ end
377
376
  No need to manually create a job class or handle the perform/call impedance mismatch
378
377
 
379
378
  ```diff
380
- -SomeInteractor.call!(*args)
381
- +SomeInteractor::Async.call!(*args)
379
+ - SomeInteractor.call!(*args)
380
+ + SomeInteractor::Async.call!(*args)
382
381
  ```
383
382
 
384
383
  This also makes it easy to add cron jobs to run interactors. As any interactor can be asyncified.
@@ -386,8 +385,6 @@ By using it's internal Async class.
386
385
 
387
386
  N.B. as your class is now executing asynchronously you can no longer rely on its promises later on in the chain.
388
387
 
389
-
390
-
391
388
  ## FAQs
392
389
  - This is ugly isn't it?
393
390
 
@@ -403,73 +400,33 @@ class OuterOrganizer
403
400
  )
404
401
  end
405
402
  ```
403
+ 1. Do you find the syntax of OuterOrganizer ugly?
406
404
 
407
- 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.
408
-
409
-
410
- - Is this interactor/interactor-contracts compatible?
411
- 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.
412
-
413
-
414
- - Why not propose changes to the interactor or interactor-contracts gem?
415
- Honestly, I think both are great and why we've built on top of them.
416
- I presume they'd object to such an extensive opinionated change, and I think that would be the right decision too.
417
- 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.
418
-
419
- - Isn't this all just syntactic sugar?
420
- Yes, but it's sugar that makes the code easier to read and understand.
421
-
422
- - Is it really easier to parse this new DSL/syntax than POROs?
423
- That's subjective, but I think so. The benefit is you have fewer extraneous files patching over a common problem in interactors.
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.
424
406
 
425
- - But it gets really verbose and complex!
426
- Again this is subjective, but if you've worked with apps with hundred or thousands of interactors, you'll have encountered these problems.
427
- I think when we work with interactors we're in one of two modes.
428
- Hunting to find the interactor we need to change, or working on the interactor we need to change.
429
- This makes the first step much easier.
430
- The second step has always been a great experience with interactors.
407
+ 2. Is this compatible with interactor/interactor-contracts?
431
408
 
432
- - I prefer Service Objects
433
- If you're not heavily invested into interactors this may not be for you.
434
- I love the chaining interactors provide.
435
- I love the contracts.
436
- I love the simplicity of the interface.
437
- I love the way they can be composed.
438
- I love the way they can be tested.
439
- When I've used service objects, I've found them to be more complex to test and compose.
440
- I can't see a clean way that using service objects to compose interactors could work well without losing some of the aforementioned benefits.
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.
441
410
 
442
- ### TODO
443
- We want to add support for explicitly specifying promises in organizers.
411
+ 3. Why not suggest enhancements to the interactor or interactor-contracts gems?
444
412
 
445
- The benefit here is on clarifying the contract between organizers and interactors.
446
-
447
- This is another variation of the "interactors themselves are great but their coordination and finding where things happen is hard in large applications".
448
-
449
- By adding promise notation to organizers, we can signal to the reader that 'here in this part of the chain is the thing you are looking for'.
450
-
451
- A writer of an organizer may expect LoadOrder to promise :order, but for the reader, it's not quite as explicit.
452
- The expected syntax will be
453
-
454
- ```ruby
455
- organize \
456
- LoadOrder.promising(:order),
457
- TakePayment.promising(:payment_transaction)
458
- ```
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.
459
414
 
460
- This will be validated at test time against the interactors promises.
415
+ 4. Is this just syntactic sugar?
461
416
 
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.
462
418
 
463
- ## Development
419
+ 5. Is the new DSL/syntax easier to understand than plain old Ruby objects (POROs)?
464
420
 
465
- 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.
466
422
 
467
- 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?
468
424
 
469
- ## 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.
470
426
 
471
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/interactify.
427
+ 7. What if I prefer using Service Objects?
472
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.
473
430
  ## License
474
431
 
475
432
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "interactify/jobable"
4
4
  require "interactify/call_wrapper"
5
- require "interactify/organizer_call_monkey_patch"
5
+ require "interactify/organizer"
6
6
  require "interactify/contract_failure"
7
7
 
8
8
  module Interactify
@@ -48,7 +48,7 @@ module Interactify
48
48
  # failure class: Whatever::SomeInteractor::InteractorContractFailure
49
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)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "interactify/each_chain"
4
4
  require "interactify/if_interactor"
5
+ require "interactify/unique_klass_name"
5
6
 
6
7
  module Interactify
7
8
  module Dsl
@@ -63,6 +64,7 @@ module Interactify
63
64
 
64
65
  # attach the class to the calling namespace
65
66
  where_to_attach = self.binding.receiver
67
+ klass_name = UniqueKlassName.for(where_to_attach, klass_name)
66
68
  where_to_attach.const_set(klass_name, klass)
67
69
  end
68
70
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "interactify/unique_klass_name"
4
+
3
5
  module Interactify
4
6
  class EachChain
5
7
  attr_reader :each_loop_klasses, :plural_resource_name, :evaluating_receiver
@@ -38,7 +40,7 @@ module Interactify
38
40
  context[this.singular_resource_name] = resource # context.package = package
39
41
  context[this.singular_resource_index_name] = index # context.package_index = index
40
42
 
41
- klasses = self.class.wrap_lambdas_in_interactors(this.each_loop_klasses)
43
+ klasses = InteractorWrapper.wrap_many(self, this.each_loop_klasses)
42
44
 
43
45
  klasses.each do |interactor| # [A, B, C].each do |interactor|
44
46
  interactor.call!(context) # interactor.call!(context)
@@ -59,8 +61,10 @@ module Interactify
59
61
  # rubocop:enable all
60
62
 
61
63
  def attach_klass
62
- namespace.const_set(iterator_klass_name, klass)
63
- namespace.const_get(iterator_klass_name)
64
+ name = iterator_klass_name
65
+
66
+ namespace.const_set(name, klass)
67
+ namespace.const_get(name)
64
68
  end
65
69
 
66
70
  def namespace
@@ -68,7 +72,9 @@ module Interactify
68
72
  end
69
73
 
70
74
  def iterator_klass_name
71
- :"Each#{singular_resource_name.to_s.camelize}".to_sym
75
+ prefix = "Each#{singular_resource_name.to_s.camelize}"
76
+
77
+ UniqueKlassName.for(namespace, prefix)
72
78
  end
73
79
 
74
80
  def singular_resource_name
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "interactify/unique_klass_name"
4
+
3
5
  module Interactify
4
6
  class IfInteractor
5
7
  attr_reader :condition, :success_interactor, :failure_interactor, :evaluating_receiver
@@ -49,8 +51,9 @@ module Interactify
49
51
  # rubocop:enable all
50
52
 
51
53
  def attach_klass
52
- namespace.const_set(if_klass_name, klass)
53
- namespace.const_get(if_klass_name)
54
+ name = if_klass_name
55
+ namespace.const_set(name, klass)
56
+ namespace.const_get(name)
54
57
  end
55
58
 
56
59
  def namespace
@@ -58,9 +61,10 @@ module Interactify
58
61
  end
59
62
 
60
63
  def if_klass_name
61
- name = condition.is_a?(Proc) ? "Proc" : condition
64
+ prefix = condition.is_a?(Proc) ? "Proc" : condition
65
+ prefix = "If#{prefix.to_s.camelize}"
62
66
 
63
- "If#{name.to_s.camelize}".to_sym
67
+ UniqueKlassName.for(namespace, prefix)
64
68
  end
65
69
  end
66
70
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/unique_klass_name"
4
+
5
+ module Interactify
6
+ class InteractorWrapper
7
+ attr_reader :organizer, :interactor
8
+
9
+ def self.wrap_many(organizer, interactors)
10
+ Array(interactors).map do |interactor|
11
+ wrap(organizer, interactor)
12
+ end
13
+ end
14
+
15
+ def self.wrap(organizer, interactor)
16
+ new(organizer, interactor).wrap
17
+ end
18
+
19
+ def initialize(organizer, interactor)
20
+ @organizer = organizer
21
+ @interactor = interactor
22
+ end
23
+
24
+ def wrap
25
+ case interactor
26
+ when Hash
27
+ wrap_conditional
28
+ when Array
29
+ wrap_chain
30
+ when Proc
31
+ wrap_proc
32
+ else
33
+ interactor
34
+ end
35
+ end
36
+
37
+ def wrap_chain
38
+ return self.class.wrap(organizer, interactor.first) if interactor.length == 1
39
+
40
+ klass_name = UniqueKlassName.for(organizer, "Chained")
41
+ organizer.chain(klass_name, *interactor.map { self.class.wrap(organizer, _1) })
42
+ end
43
+
44
+ def wrap_conditional
45
+ raise ArgumentError, "Hash must have at least :if, and :then key" unless condition && then_do
46
+
47
+ return organizer.if(condition, then_do, else_do) if else_do
48
+
49
+ organizer.if(condition, then_do)
50
+ end
51
+
52
+ def condition = interactor[:if]
53
+ def then_do = interactor[:then]
54
+ def else_do = interactor[:else]
55
+
56
+ def wrap_proc
57
+ this = self
58
+
59
+ Class.new do
60
+ include Interactify
61
+
62
+ define_singleton_method :wrapped do
63
+ this.interactor
64
+ end
65
+
66
+ define_method(:call) do
67
+ this.interactor.call(context)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,32 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "interactify/interactor_wrapper"
4
+
3
5
  module Interactify
4
- module OrganizerCallMonkeyPatch
6
+ module Organizer
5
7
  extend ActiveSupport::Concern
6
8
 
7
9
  class_methods do
8
10
  def organize(*interactors)
9
- wrapped = wrap_lambdas_in_interactors(interactors)
11
+ wrapped = InteractorWrapper.wrap_many(self, interactors)
10
12
 
11
13
  super(*wrapped)
12
14
  end
13
-
14
- def wrap_lambdas_in_interactors(interactors)
15
- Array(interactors).map do |interactor|
16
- case interactor
17
- when Proc
18
- Class.new do
19
- include Interactify
20
-
21
- define_method(:call) do
22
- interactor.call(context)
23
- end
24
- end
25
- else
26
- interactor
27
- end
28
- end
29
- end
30
15
  end
31
16
 
32
17
  def call
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ module UniqueKlassName
5
+ def self.for(namespace, prefix)
6
+ id = generate_unique_id
7
+ klass_name = :"#{prefix}#{id}"
8
+
9
+ while namespace.const_defined?(klass_name)
10
+ id = generate_unique_id
11
+ klass_name = :"#{prefix}#{id}"
12
+ end
13
+
14
+ klass_name.to_sym
15
+ end
16
+
17
+ def self.generate_unique_id
18
+ rand(10_000)
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Interactify
4
- VERSION = "0.2.0-alpha.1"
4
+ VERSION = "0.3.0-alpha.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interactify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.pre.alpha.1
4
+ version: 0.3.0.pre.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Burns
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-27 00:00:00.000000000 Z
11
+ date: 2023-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: interactor
@@ -127,12 +127,14 @@ files:
127
127
  - lib/interactify/interactor_wiring/constants.rb
128
128
  - lib/interactify/interactor_wiring/error_context.rb
129
129
  - lib/interactify/interactor_wiring/files.rb
130
+ - lib/interactify/interactor_wrapper.rb
130
131
  - lib/interactify/job_maker.rb
131
132
  - lib/interactify/jobable.rb
132
133
  - lib/interactify/mismatching_promise_error.rb
133
- - lib/interactify/organizer_call_monkey_patch.rb
134
+ - lib/interactify/organizer.rb
134
135
  - lib/interactify/promising.rb
135
136
  - lib/interactify/rspec/matchers.rb
137
+ - lib/interactify/unique_klass_name.rb
136
138
  - lib/interactify/version.rb
137
139
  - sig/interactify.rbs
138
140
  homepage: https://github.com/markburns/interactify