Skip to content

Commit ada0d7d

Browse files
authored
move into Dsl/Async/Contracts namespaces (#15)
* move into Dsl/Async/Contracts namespaces * fix constant name
1 parent 80a149b commit ada0d7d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+773
-739
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ In order to detect these wiring issues, stick a spec in your test suite like thi
312312
```ruby
313313
RSpec.describe 'InteractorWiring' do
314314
it 'validates the interactors in the whole app', :aggregate_failures do
315-
errors = Interactify.validate_app(ignore: [/Priam/])
315+
errors = Interactify.validate_app(ignore: [/SomeClassName/, AnotherClass, 'SomeClassNameString'])
316316

317317
expect(errors).to eq ''
318318
end

lib/interactify.rb

+5-39
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
require "active_support/all"
66

77
require "interactify/version"
8-
require "interactify/contract_helpers"
8+
require "interactify/contracts/helpers"
9+
require "interactify/contracts/promising"
910
require "interactify/dsl"
1011
require "interactify/wiring"
11-
require "interactify/promising"
12+
require "interactify/configuration"
1213

1314
module Interactify
1415
def self.railties_missing?
@@ -102,7 +103,7 @@ def configuration
102103

103104
base.include Interactor::Organizer
104105
base.include Interactor::Contracts
105-
base.include Interactify::ContractHelpers
106+
base.include Interactify::Contracts::Helpers
106107

107108
# defines two classes on the receiver class
108109
# the first is the job class
@@ -123,45 +124,10 @@ def configuration
123124
# that calls the interactor ExampleInteractor with (foo: 'bar')
124125
#
125126
# ExampleInteractor::Async.call(foo: 'bar')
126-
include Interactify::Jobable
127+
include Interactify::Async::Jobable
127128
interactor_job
128129
end
129130

130-
class_methods do
131-
def promising(*args)
132-
Promising.validate(self, *args)
133-
end
134-
135-
def promised_keys
136-
_interactify_extract_keys(contract.promises)
137-
end
138-
139-
def expected_keys
140-
_interactify_extract_keys(contract.expectations)
141-
end
142-
143-
private
144-
145-
# this is the most brittle part of the code, relying on
146-
# interactor-contracts internals
147-
# so extracted it to here so change is isolated
148-
def _interactify_extract_keys(clauses)
149-
clauses.instance_eval { @terms }.json&.rules&.keys
150-
end
151-
end
152-
153-
class Configuration
154-
attr_writer :root
155-
156-
def root
157-
@root ||= fallback
158-
end
159-
160-
def fallback
161-
Rails.root / "app" if Interactify.railties?
162-
end
163-
end
164-
165131
def called_klass_list
166132
context._called.map(&:class)
167133
end

lib/interactify/async/job_klass.rb

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
module Interactify
4+
module Async
5+
class JobKlass
6+
attr_reader :container_klass, :klass_suffix
7+
8+
def initialize(container_klass:, klass_suffix:)
9+
@container_klass = container_klass
10+
@klass_suffix = klass_suffix
11+
end
12+
13+
def async_job_klass
14+
klass = Class.new do
15+
include Interactor
16+
include Interactor::Contracts
17+
end
18+
19+
attach_call(klass)
20+
attach_call!(klass)
21+
22+
klass
23+
end
24+
25+
def attach_call(async_job_klass)
26+
# e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
27+
async_job_klass.send(:define_singleton_method, :call) do |context|
28+
call!(context)
29+
end
30+
end
31+
32+
def attach_call!(async_job_klass)
33+
this = self
34+
35+
# e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
36+
async_job_klass.send(:define_singleton_method, :call!) do |context|
37+
# e.g. SomeInteractor::JobWithSuffix
38+
job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
39+
40+
# e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
41+
job_klass.perform_async(this.args(context))
42+
end
43+
end
44+
45+
def args(context)
46+
args = context.to_h.stringify_keys
47+
48+
return args unless container_klass.respond_to?(:expected_keys)
49+
50+
restrict_to_optional_or_keys_from_contract(args)
51+
end
52+
53+
def restrict_to_optional_or_keys_from_contract(args)
54+
keys = container_klass.expected_keys.map(&:to_s)
55+
56+
optional = Array(container_klass.optional_attrs).map(&:to_s)
57+
keys += optional
58+
59+
args.slice(*keys)
60+
end
61+
end
62+
end
63+
end

lib/interactify/async/job_maker.rb

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "interactify/async/job_klass"
4+
require "interactify/async/null_job"
5+
6+
module Interactify
7+
module Async
8+
class JobMaker
9+
attr_reader :opts, :method_name, :container_klass, :klass_suffix
10+
11+
def initialize(container_klass:, opts:, klass_suffix:, method_name: :call!)
12+
@container_klass = container_klass
13+
@opts = opts
14+
@method_name = method_name
15+
@klass_suffix = klass_suffix
16+
end
17+
18+
concerning :JobClass do
19+
def job_klass
20+
@job_klass ||= define_job_klass
21+
end
22+
23+
private
24+
25+
def define_job_klass
26+
return NullJob if Interactify.sidekiq_missing?
27+
28+
this = self
29+
30+
invalid_keys = this.opts.symbolize_keys.keys - %i[queue retry dead backtrace pool tags]
31+
32+
raise ArgumentError, "Invalid keys: #{invalid_keys}" if invalid_keys.any?
33+
34+
build_job_klass(opts).tap do |klass|
35+
klass.const_set(:JOBABLE_OPTS, opts)
36+
klass.const_set(:JOBABLE_METHOD_NAME, method_name)
37+
end
38+
end
39+
40+
def build_job_klass(opts)
41+
Class.new do
42+
include Sidekiq::Job
43+
44+
sidekiq_options(opts)
45+
46+
def perform(...)
47+
self.class.module_parent.send(self.class::JOBABLE_METHOD_NAME, ...)
48+
end
49+
end
50+
end
51+
end
52+
53+
def async_job_klass
54+
JobKlass.new(container_klass:, klass_suffix:).async_job_klass
55+
end
56+
end
57+
end
58+
end

lib/interactify/async/jobable.rb

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
require "interactify/async/job_maker"
4+
5+
module Interactify
6+
module Async
7+
module Jobable
8+
extend ActiveSupport::Concern
9+
10+
# e.g. if Klass < Base
11+
# and Base has a Base::Job class
12+
#
13+
# then let's make sure to define Klass::Job separately
14+
included do |base|
15+
next if Interactify.sidekiq_missing?
16+
17+
def base.inherited(klass)
18+
super_klass = klass.superclass
19+
super_job = super_klass::Job # really spiffing
20+
21+
opts = super_job::JOBABLE_OPTS
22+
jobable_method_name = super_job::JOBABLE_METHOD_NAME
23+
24+
to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
25+
26+
klass.send(to_call, opts:, method_name: jobable_method_name)
27+
super(klass)
28+
end
29+
end
30+
31+
class_methods do
32+
# create a Job class and an Async class
33+
# see job_calling for details on the Job class
34+
#
35+
# the Async class is a wrapper around the Job class
36+
# that allows it to be used in an interactor chain
37+
#
38+
# E.g.
39+
#
40+
# class ExampleInteractor
41+
# include Interactify
42+
# expect :foo
43+
#
44+
# include Jobable
45+
# interactor_job
46+
# end
47+
#
48+
# doing the following will immediately enqueue a job
49+
# that calls the interactor ExampleInteractor with (foo: 'bar')
50+
# ExampleInteractor::Async.call(foo: 'bar')
51+
#
52+
# it will also ensure to pluck only the expects from the context
53+
# so that you can have other non primitive values in the context
54+
# but the job will only have the expects passed to it
55+
#
56+
# obviously you will need to be aware that later interactors
57+
# in an interactor chain cannot depend on the result of the async
58+
# interactor
59+
def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
60+
job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
61+
# with WhateverInteractor::Job you can perform the interactor as a job
62+
# from sidekiq
63+
# e.g. WhateverInteractor::Job.perform_async(...)
64+
const_set("Job#{klass_suffix}", job_maker.job_klass)
65+
66+
# with WhateverInteractor::Async you can call WhateverInteractor::Job
67+
# in an organizer oro on its oen using normal interactor call call! semantics
68+
# e.g. WhateverInteractor::Async.call(...)
69+
# WhateverInteractor::Async.call!(...)
70+
const_set("Async#{klass_suffix}", job_maker.async_job_klass)
71+
end
72+
73+
# if this was defined in ExampleClass this creates the following class
74+
# ExampleClass::Job
75+
# this class ia added as a convenience so you can easily turn a
76+
# class method into a job
77+
#
78+
# Example:
79+
#
80+
# class ExampleClass
81+
# include Jobable
82+
# job_calling method_name: :some_method
83+
# end
84+
#
85+
# # the following class is created that you can use to enqueue a job
86+
# in the sidekiq yaml file
87+
# ExampleClass::Job.some_method
88+
def job_calling(method_name:, opts: {}, klass_suffix: "")
89+
job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
90+
91+
const_set("Job#{klass_suffix}", job_maker.job_klass)
92+
end
93+
end
94+
end
95+
end
96+
end

lib/interactify/async/null_job.rb

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module Interactify
4+
module Async
5+
class NullJob
6+
def method_missing(...)
7+
self
8+
end
9+
10+
def self.method_missing(...)
11+
self
12+
end
13+
14+
def respond_to_missing?(...)
15+
true
16+
end
17+
18+
def self.respond_to_missing?(...)
19+
true
20+
end
21+
end
22+
end
23+
end

lib/interactify/async_job_klass.rb

-61
This file was deleted.

0 commit comments

Comments
 (0)