interactify 0.1.0.pre.alpha.1 → 0.2.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: 4c2dffe1928c3b1653ea12057fd660eacbef1d8fbd946d1bdbddef00494a8c8b
4
+ data.tar.gz: 7078fe7a6b217febd47f4598eb2e88fdcfb8affcd434dba239ee86a39a072176
5
5
  SHA512:
6
- metadata.gz: 1d9e01fc12b9b02bdc9d897b81866c7d668f94cf0fafd51e018cded3ec2892d5452848000672a9c4ac139f1c1dc3d30aa23e1c16b0dcaf66afd901c4586d8eae
7
- data.tar.gz: b33dc94b10b26cf1b4baeace46168641d88121bf05fe2a4c8c37098d3b5bc678667e0ff253f620def1798846b9b95533f943ef4c80726abe08a1a41662c338c0
6
+ metadata.gz: 9e3eb106e5b896965c6b1fbfea135c62f4eb7f95779041c121bcb2040fd8568c54e94b4f77a3e1bcca6d922d8a8bdfcacda354d3a6396e19a49fbfce78f39af1
7
+ data.tar.gz: 3f998d9f35105a47becc0555ed80dff264fad2e5fe58f93ec0c731b8cab4281e457da9e3fc00d0e4709e1b62b4e2416a8a75b66f56723b610531bca8fd458ea3
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
@@ -3,3 +3,7 @@
3
3
  ## [0.1.0] - 2023-12-16
4
4
 
5
5
  - Initial release
6
+
7
+ ## [0.2.0] - 2023-12-27
8
+
9
+ - Added support for Interactify.promising syntax in organizers
data/README.md CHANGED
@@ -1,11 +1,50 @@
1
1
  # Interactify
2
2
 
3
- Interactors are a great way to encapsulate business logic in a Rails application.
3
+ [Interactors](https://github.com/collectiveidea/interactor) are a great way to encapsulate business logic in a Rails application.
4
4
  However, sometimes in complex interactor chains, the complex debugging happens at one level up from your easy to read and test interactors.
5
5
 
6
- Interactify wraps the interactor and interactor-contract gem and provides additional functionality making chaining and understanding interactor chains easier.
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
+
13
+ ## Installation
14
+
15
+ ```Gemfile
16
+ gem 'interactify'
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Initializer
22
+ ```ruby
23
+ # in config/initializers/interactify.rb
24
+ Interactify.configure do |config|
25
+ # default
26
+ # config.root = Rails.root / 'app'
27
+ end
28
+
29
+ Interactify.on_contract_breach do |context, attrs|
30
+ # maybe add context to Sentry or Honeybadger etc here
31
+ end
32
+
33
+ Interactify.before_raise do |exception|
34
+ # maybe add context to Sentry or Honeybadger etc here
35
+ end
36
+ ```
37
+
38
+ ### Using the RSpec matchers
39
+ ```ruby
40
+ # e.g. in spec/supoort/interactify.rb
41
+ require 'interactify/rspec/matchers'
42
+ ```
7
43
 
8
44
  ### Syntactic Sugar
45
+ - Everything is an Organizer/Interactor and supports interactor-contracts.
46
+ - Concise syntax for most common scenarios with `expects` and `promises`. Verifying the presence of the keys/values.
47
+ - Automatic delegation of expected and promised keys to the context.
9
48
 
10
49
  ```ruby
11
50
  # before
@@ -16,6 +55,8 @@ class LoadOrder
16
55
 
17
56
  expects do
18
57
  required(:id).filled
58
+ required(:something_else).filled
59
+ required(:a_boolean_flag)
19
60
  end
20
61
 
21
62
  promises do
@@ -35,7 +76,8 @@ end
35
76
  class LoadOrder
36
77
  include Interactify
37
78
 
38
- expect :id
79
+ expect :id, :something_else
80
+ expect :a_boolean_flag, filled: false
39
81
  promise :order
40
82
 
41
83
  def call
@@ -63,7 +105,7 @@ organize \
63
105
 
64
106
  Sometimes we want an interactor for each item in a collection.
65
107
  But it gets unwieldy.
66
- It was complex procedural code and is now broken into neat SRP classes (Single Responsibility Principle).
108
+ It was complex procedural code and is now broken into neat [SRP classes](https://en.wikipedia.org/wiki/Single_responsibility_principle).
67
109
  But there is still boilerplate and jumping around between files to follow the orchestration.
68
110
  It's easy to get lost in the orchestration code that occurs across say 7 or 8 files.
69
111
 
@@ -208,6 +250,90 @@ class SomeOrganizer
208
250
  EitherWayDoThis
209
251
  end
210
252
 
253
+ ```
254
+ ### Contract validation failures
255
+ 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.
256
+ If the context is large it's often hard to spot what the actual problem is or where it occurred.
257
+
258
+ #### before
259
+ ```
260
+ Interactor::Failure
261
+
262
+ #<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)
263
+ , tasks=[]>
264
+ ```
265
+
266
+ #### after with call
267
+ ```
268
+ #<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)
269
+ , tasks=[], contract_failures={:tasks=>["tasks must be filled"]}>
270
+ ```
271
+
272
+ #### after with call!
273
+ ```
274
+ #<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)
275
+ , tasks=[], contract_failures={:tasks=>["tasks must be filled"]}>
276
+ ```
277
+
278
+ ### Promising
279
+ You can annotate your interactors in the organize arguments with their promises.
280
+ This then acts as executable documentation that is validated at load time and enforced to stay in sync with the interactor.
281
+
282
+ A writer of an organizer may quite reasonably expect `LoadOrder` to promise `:order`, but for the reader, it's not always as immediately obvious
283
+ which interactor in the chain is responsible for provides which key.
284
+
285
+ ```ruby
286
+ organize \
287
+ LoadOrder.promising(:order),
288
+ TakePayment.promising(:payment_transaction)
289
+ ```
290
+
291
+ This will be validated at load time against the interactors promises.
292
+ An example of a failure would be:
293
+
294
+ ```
295
+ SomeOrganizer::DoStep1 does not promise:
296
+ step_1
297
+
298
+ Actual promises are:
299
+ step1
300
+ ```
301
+
302
+
303
+ ### Interactor wiring specs
304
+ Sometimes you have an interactor chain that fails because something is expected deeper down the chain and not provided further up the chain.
305
+ 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.
306
+
307
+ But we can do better than that if we always `promise` something that is later `expect`ed.
308
+
309
+ In order to detect these wiring issues, stick a spec in your test suite like this:
310
+
311
+ ```ruby
312
+ RSpec.describe 'InteractorWiring' do
313
+ it 'validates the interactors in the whole app', :aggregate_failures do
314
+ errors = Interactify.validate_app(ignore: [/Priam/])
315
+
316
+ expect(errors).to eq ''
317
+ end
318
+ end
319
+ ```
320
+
321
+ ```
322
+ Missing keys: :order_id
323
+ in: AssignOrderToUser
324
+ for: PlaceOrder
325
+ ```
326
+
327
+ This allows you to quickly see exactly where you missed assigning something to the context.
328
+ Combine with lambda debugging `->(ctx) { byebug if ctx.order_id.nil?},` in your chains to drop into the exact
329
+ location in the chain to find where to make the change.
330
+
331
+ ### RSpec matchers
332
+ 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.
333
+
334
+ ```ruby
335
+ expect(described_class).to expect_inputs(:order_id)
336
+ expect(described_class).to promise_outputs(:order)
211
337
  ```
212
338
 
213
339
  ### Sidekiq Jobs
@@ -230,10 +356,11 @@ clsas SomeInteractorJob
230
356
  SomeInteractor.call(*args)
231
357
  end
232
358
  end
359
+ ```
233
360
 
234
- SomeInteractor.call(*args)
235
- code is changed to
236
- SomeInteractorJob.perform_async(*args)
361
+ ```diff
362
+ -SomeInteractor.call(*args)
363
+ +SomeInteractorJob.perform_async(*args)
237
364
  ```
238
365
 
239
366
  ```ruby
@@ -245,14 +372,22 @@ class SomeInteractor
245
372
  # ...
246
373
  end
247
374
  end
375
+ ```
248
376
 
249
- # no need to manually create a job class or handle the perform/call impedance mismatch
250
- SomeInteractor::Async.call(*args)
377
+ No need to manually create a job class or handle the perform/call impedance mismatch
251
378
 
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.
379
+ ```diff
380
+ -SomeInteractor.call!(*args)
381
+ +SomeInteractor::Async.call!(*args)
254
382
  ```
255
383
 
384
+ This also makes it easy to add cron jobs to run interactors. As any interactor can be asyncified.
385
+ By using it's internal Async class.
386
+
387
+ N.B. as your class is now executing asynchronously you can no longer rely on its promises later on in the chain.
388
+
389
+
390
+
256
391
  ## FAQs
257
392
  - This is ugly isn't it?
258
393
 
@@ -305,7 +440,14 @@ When I've used service objects, I've found them to be more complex to test and c
305
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.
306
441
 
307
442
  ### 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.
443
+ We want to add support for explicitly specifying promises in organizers.
444
+
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
+
309
451
  A writer of an organizer may expect LoadOrder to promise :order, but for the reader, it's not quite as explicit.
310
452
  The expected syntax will be
311
453
 
@@ -317,36 +459,6 @@ organize \
317
459
 
318
460
  This will be validated at test time against the interactors promises.
319
461
 
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
- ```
350
462
 
351
463
  ## Development
352
464
 
@@ -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_call_monkey_patch"
6
+ require "interactify/contract_failure"
4
7
 
5
8
  module Interactify
6
9
  module ContractHelpers
@@ -39,14 +42,11 @@ 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
51
  include OrganizerCallMonkeyPatch if ancestors.include? Interactor::Organizer
52
52
 
@@ -1,5 +1,7 @@
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"
3
5
 
4
6
  module Interactify
5
7
  module Dsl
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Interactify
2
4
  class EachChain
3
5
  attr_reader :each_loop_klasses, :plural_resource_name, :evaluating_receiver
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Interactify
2
4
  class IfInteractor
3
5
  attr_reader :condition, :success_interactor, :failure_interactor, :evaluating_receiver
@@ -56,7 +58,7 @@ module Interactify
56
58
  end
57
59
 
58
60
  def if_klass_name
59
- name = condition.is_a?(Proc) ? 'Proc' : condition
61
+ name = condition.is_a?(Proc) ? "Proc" : condition
60
62
 
61
63
  "If#{name.to_s.camelize}".to_sym
62
64
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactify/interactor_wiring/error_context"
4
+
5
+ module Interactify
6
+ class InteractorWiring
7
+ class CallableRepresentation
8
+ attr_reader :filename, :klass, :wiring
9
+
10
+ delegate :interactor_lookup, to: :wiring
11
+
12
+ def initialize(filename:, klass:, wiring:)
13
+ @filename = filename
14
+ @klass = klass
15
+ @wiring = wiring
16
+ end
17
+
18
+ def validate_callable(error_context: ErrorContext.new)
19
+ if organizer?
20
+ assign_previously_defined(error_context:)
21
+ validate_children(error_context:)
22
+ end
23
+
24
+ validate_self(error_context:)
25
+ end
26
+
27
+ def expected_keys
28
+ klass.respond_to?(:expected_keys) ? Array(klass.expected_keys) : []
29
+ end
30
+
31
+ def promised_keys
32
+ klass.respond_to?(:promised_keys) ? Array(klass.promised_keys) : []
33
+ end
34
+
35
+ def all_keys
36
+ expected_keys.concat(promised_keys)
37
+ end
38
+
39
+ def inspect
40
+ "#<#{self.class.name}#{object_id} @filename=#{filename}, @klass=#{klass.name}>"
41
+ end
42
+
43
+ def organizer?
44
+ klass.respond_to?(:organized) && klass.organized.any?
45
+ end
46
+
47
+ def assign_previously_defined(error_context:)
48
+ return unless contract?
49
+
50
+ error_context.append_previously_defined_keys(all_keys)
51
+ end
52
+
53
+ def validate_children(error_context:)
54
+ klass.organized.each do |interactor|
55
+ interactor_as_callable = interactor_lookup[interactor]
56
+ next if interactor_as_callable.nil?
57
+
58
+ error_context = interactor_as_callable.validate_callable(error_context:)
59
+ end
60
+
61
+ error_context
62
+ end
63
+
64
+ private
65
+
66
+ def contract?
67
+ klass.ancestors.include? Interactor::Contracts
68
+ end
69
+
70
+ def validate_self(error_context:)
71
+ return error_context unless contract?
72
+
73
+ error_context.infer_missing_keys(self)
74
+ error_context.add_promised_keys(promised_keys)
75
+ error_context
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactify
4
+ class InteractorWiring
5
+ class Constants
6
+ attr_reader :root, :organizer_files, :interactor_files
7
+
8
+ def initialize(root:, organizer_files:, interactor_files:)
9
+ @root = root.is_a?(Pathname) ? root : Pathname.new(root)
10
+ @organizer_files = organizer_files
11
+ @interactor_files = interactor_files
12
+ end
13
+
14
+ def organizers
15
+ @organizers ||= organizer_files.flat_map do |f|
16
+ callables_in_file(f)
17
+ end.compact.select(&:organizer?)
18
+ end
19
+
20
+ def interactors
21
+ @interactors ||= interactor_files.flat_map do |f|
22
+ callables_in_file(f)
23
+ end.compact.reject(&:organizer?)
24
+ end
25
+
26
+ def interactor_lookup
27
+ @interactor_lookup ||= (interactors + organizers).index_by(&:klass)
28
+ end
29
+
30
+ private
31
+
32
+ def callables_in_file(filename)
33
+ @callables_in_file ||= {}
34
+
35
+ @callables_in_file[filename] ||= _callables_in_file(filename)
36
+ end
37
+
38
+ def _callables_in_file(filename)
39
+ constant = constant_for(filename)
40
+ return if constant == Interactify
41
+
42
+ internal_klasses = internal_constants_for(constant)
43
+
44
+ ([constant] + internal_klasses).map do |k|
45
+ new_callable(filename, k, self)
46
+ end
47
+ end
48
+
49
+ def internal_constants_for(constant)
50
+ constant
51
+ .constants
52
+ .map { |sym| constant_from_symbol(constant, sym) }
53
+ .select { |pk| interactor_klass?(pk) }
54
+ end
55
+
56
+ def constant_from_symbol(constant, symbol)
57
+ constant.module_eval do
58
+ symbol.to_s.constantize
59
+ rescue StandardError
60
+ begin
61
+ "#{constant.name}::#{symbol}".constantize
62
+ rescue StandardError
63
+ nil
64
+ end
65
+ end
66
+ end
67
+
68
+ def interactor_klass?(object)
69
+ return unless object.is_a?(Class) && object.ancestors.include?(Interactor)
70
+ return if object.is_a?(Sidekiq::Job)
71
+
72
+ true
73
+ end
74
+
75
+ def new_callable(filename, klass, wiring)
76
+ CallableRepresentation.new(filename:, klass:, wiring:)
77
+ end
78
+
79
+ def constant_for(filename)
80
+ require filename
81
+
82
+ underscored_klass_name = underscored_klass_name(filename)
83
+ underscored_klass_name = trim_rails_design_pattern_folder underscored_klass_name
84
+
85
+ klass_name = underscored_klass_name.classify
86
+
87
+ should_pluralize = filename[underscored_klass_name.pluralize]
88
+ klass_name = klass_name.pluralize if should_pluralize
89
+
90
+ Object.const_get(klass_name)
91
+ end
92
+
93
+ # Example:
94
+ # trim_rails_folder("app/interactors/namespace/sub_namespace/class_name.rb")
95
+ # => "namespace/sub_namespace/class_name.rb"
96
+ def trim_rails_design_pattern_folder(filename)
97
+ rails_folders.each do |folder|
98
+ regexable_folder = Regexp.quote("#{folder}/")
99
+ regex = /^#{regexable_folder}/
100
+
101
+ return filename.gsub(regex, "") if filename.match?(regex)
102
+ end
103
+
104
+ filename
105
+ end
106
+
107
+ def rails_folders = Dir.glob(root / "*").map { Pathname.new _1 }.select(&:directory?).map { |f| File.basename(f) }
108
+
109
+ # Example:
110
+ # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
111
+ # "/namespace/sub_namespace/class_name.rb"
112
+ # ['', 'namespace', 'sub_namespace', 'class_name.rb']
113
+ # ['namespace', 'sub_namespace', 'class_name.rb']
114
+ def underscored_klass_name(filename)
115
+ filename.to_s # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
116
+ .gsub(root.to_s, "") # "/namespace/sub_namespace/class_name.rb"
117
+ .gsub("/concerns", "") # concerns directory is ignored by Zeitwerk
118
+ .split("/") # "['', 'namespace', 'sub_namespace', 'class_name.rb']
119
+ .compact_blank # "['namespace', 'sub_namespace', 'class_name.rb']
120
+ .join("/") # 'namespace/sub_namespace/class_name.rb'
121
+ .gsub(/\.rb\z/, "") # 'namespace/sub_namespace/class_name'
122
+ end
123
+ end
124
+ end
125
+ end