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.
- checksums.yaml +4 -4
- data/.rubocop.yml +23 -0
- data/.ruby-version +1 -0
- data/Appraisals +21 -0
- data/CHANGELOG.md +18 -1
- data/README.md +178 -126
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/no_railties_no_sidekiq.gemfile +16 -0
- data/gemfiles/no_railties_no_sidekiq.gemfile.lock +127 -0
- data/gemfiles/railties_6.gemfile +14 -0
- data/gemfiles/railties_6.gemfile.lock +253 -0
- data/gemfiles/railties_6_no_sidekiq.gemfile +17 -0
- data/gemfiles/railties_6_no_sidekiq.gemfile.lock +158 -0
- data/gemfiles/railties_6_sidekiq.gemfile +18 -0
- data/gemfiles/railties_6_sidekiq.gemfile.lock +167 -0
- data/gemfiles/railties_7_no_sidekiq.gemfile +17 -0
- data/gemfiles/railties_7_no_sidekiq.gemfile.lock +157 -0
- data/gemfiles/railties_7_sidekiq.gemfile +18 -0
- data/gemfiles/railties_7_sidekiq.gemfile.lock +166 -0
- 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 +11 -7
- data/lib/interactify/mismatching_promise_error.rb +17 -0
- data/lib/interactify/null_job.rb +11 -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 +96 -9
- metadata +36 -23
- 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: 319dd56313d82aa987c8eaec40691e43c58e8b72513969e2a6a7a83de17ae311
|
4
|
+
data.tar.gz: 3f5e4ad0464157910bca7bfd1300fa8e23ab825068c84a4c5c0ef5ad0cdb29d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
4
|
-
|
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
|
-
|
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'
|
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
|
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
|
-
|
231
|
+
# context conditional
|
232
|
+
self.if(:some_key_on_context, DoThingA, DoThingB),
|
186
233
|
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
###
|
214
|
-
Sometimes
|
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
|
-
|
218
|
-
|
219
|
-
|
289
|
+
organize \
|
290
|
+
LoadOrder.promising(:order),
|
291
|
+
TakePayment.promising(:payment_transaction)
|
292
|
+
```
|
220
293
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
337
|
+
expect(described_class).to expect_inputs(:order_id)
|
338
|
+
expect(described_class).to promise_outputs(:order)
|
339
|
+
```
|
243
340
|
|
244
|
-
|
245
|
-
|
246
|
-
end
|
247
|
-
end
|
341
|
+
### Sidekiq Jobs
|
342
|
+
Sometimes you want to asyncify an interactor.
|
248
343
|
|
249
|
-
|
250
|
-
|
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
|
-
|
253
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
402
|
+
5. Is the new DSL/syntax easier to understand than plain old Ruby objects (POROs)?
|
352
403
|
|
353
|
-
|
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
|
-
|
406
|
+
6. Doesn't this approach become verbose and complex in large applications?
|
356
407
|
|
357
|
-
|
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
|
-
|
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,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: "../"
|