interactify 0.1.0.pre.alpha.1 → 0.2.0.pre.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +23 -0
- data/CHANGELOG.md +4 -0
- data/README.md +154 -42
- 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 +9 -9
- data/lib/interactify/dsl.rb +4 -2
- data/lib/interactify/each_chain.rb +2 -0
- data/lib/interactify/if_interactor.rb +3 -1
- 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/job_maker.rb +20 -69
- data/lib/interactify/jobable.rb +9 -7
- data/lib/interactify/mismatching_promise_error.rb +17 -0
- data/lib/interactify/organizer_call_monkey_patch.rb +7 -2
- data/lib/interactify/promising.rb +34 -0
- data/lib/interactify/rspec/matchers.rb +9 -11
- data/lib/interactify/version.rb +1 -1
- data/lib/interactify.rb +39 -9
- metadata +11 -2
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
class InteractorWiring
|
5
|
+
class ErrorContext
|
6
|
+
def previously_defined_keys
|
7
|
+
@previously_defined_keys ||= Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def append_previously_defined_keys(keys)
|
11
|
+
keys.each do |key|
|
12
|
+
previously_defined_keys << key
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def missing_keys
|
17
|
+
@missing_keys ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_promised_keys(promised_keys)
|
21
|
+
promised_keys.each do |key|
|
22
|
+
previously_defined_keys << key
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def infer_missing_keys(callable)
|
27
|
+
new_keys = callable.expected_keys
|
28
|
+
not_in_previous_keys = new_keys.reject { |key| previously_defined_keys.include?(key) }
|
29
|
+
|
30
|
+
add_missing_keys(callable, not_in_previous_keys)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_missing_keys(callable, not_in_previous_keys)
|
34
|
+
return if not_in_previous_keys.empty?
|
35
|
+
|
36
|
+
missing_keys[callable] ||= Set.new
|
37
|
+
missing_keys[callable] += not_in_previous_keys
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactify
|
4
|
+
class InteractorWiring
|
5
|
+
class Files
|
6
|
+
attr_reader :root
|
7
|
+
|
8
|
+
def initialize(root:)
|
9
|
+
@root = root
|
10
|
+
end
|
11
|
+
|
12
|
+
def organizer_files
|
13
|
+
possible_files.select { |_, contents| organizer_file?(contents) }.map(&:first).sort
|
14
|
+
end
|
15
|
+
|
16
|
+
def interactor_files
|
17
|
+
possible_files.select { |_, contents| interactor_file?(contents) }.map(&:first).sort
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def organizer_file?(code)
|
23
|
+
organizer?(code) && (interactified?(code) || vanilla_organizer?(code))
|
24
|
+
end
|
25
|
+
|
26
|
+
def interactor_file?(code)
|
27
|
+
!organizer?(code) && (interactified?(code) || vanilla_interactor?(code))
|
28
|
+
end
|
29
|
+
|
30
|
+
def vanilla_organizer?(code)
|
31
|
+
code[/include Interactor::Organizer/]
|
32
|
+
end
|
33
|
+
|
34
|
+
def vanilla_interactor?(code)
|
35
|
+
code[/include Interactor$/]
|
36
|
+
end
|
37
|
+
|
38
|
+
def interactified?(code)
|
39
|
+
code["include Interactify"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def organizer?(code)
|
43
|
+
code[/^\s+organize/]
|
44
|
+
end
|
45
|
+
|
46
|
+
def possible_files
|
47
|
+
@possible_files ||= Dir.glob("#{root}/**/*.rb").map { |f| [f, File.read(f)] }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,305 +1,90 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/all"
|
4
|
+
|
5
|
+
require "interactify/interactor_wiring/callable_representation"
|
6
|
+
require "interactify/interactor_wiring/constants"
|
7
|
+
require "interactify/interactor_wiring/files"
|
2
8
|
|
3
9
|
module Interactify
|
4
10
|
class InteractorWiring
|
5
|
-
attr_reader :root, :
|
11
|
+
attr_reader :root, :ignore
|
6
12
|
|
7
|
-
def initialize(root: Rails.root,
|
8
|
-
@root = root.to_s.gsub(%r{/$},
|
9
|
-
@namespace = namespace
|
13
|
+
def initialize(root: Rails.root, ignore: [])
|
14
|
+
@root = root.to_s.gsub(%r{/$}, "")
|
10
15
|
@ignore = ignore
|
11
16
|
end
|
12
17
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
next if ignore_klass?(ignore, organizer.klass)
|
17
|
-
|
18
|
-
errors = organizer.validate_callable
|
19
|
-
all_errors[organizer] = errors
|
20
|
-
end
|
18
|
+
def validate_app
|
19
|
+
errors = organizers.each_with_object({}) do |organizer, all_errors|
|
20
|
+
next if ignore_klass?(ignore, organizer.klass)
|
21
21
|
|
22
|
-
|
22
|
+
errors = organizer.validate_callable
|
23
|
+
all_errors[organizer] = errors
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
|
-
formatted_errors = []
|
27
|
-
|
28
|
-
all_errors.each do |organizer, error_context|
|
29
|
-
next if ignore_klass?(ignore, organizer.klass)
|
30
|
-
|
31
|
-
error_context.missing_keys.each do |interactor, missing|
|
32
|
-
next if ignore_klass?(ignore, interactor.klass)
|
33
|
-
|
34
|
-
formatted_errors << <<~ERROR
|
35
|
-
Missing keys: #{missing.to_a.map(&:to_sym).map(&:inspect).join(', ')}
|
36
|
-
in: #{interactor.klass}
|
37
|
-
for: #{organizer.klass}
|
38
|
-
ERROR
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
formatted_errors.join("\n\n")
|
43
|
-
end
|
44
|
-
|
45
|
-
def ignore_klass?(ignore, klass)
|
46
|
-
case ignore
|
47
|
-
when Array
|
48
|
-
ignore.any? { ignore_klass?(_1, klass) }
|
49
|
-
when Regexp
|
50
|
-
klass.to_s =~ ignore
|
51
|
-
when String
|
52
|
-
klass.to_s[ignore]
|
53
|
-
when Proc
|
54
|
-
ignore.call(klass)
|
55
|
-
when Class
|
56
|
-
klass <= ignore
|
57
|
-
end
|
58
|
-
end
|
26
|
+
format_errors(errors)
|
59
27
|
end
|
60
28
|
|
61
|
-
|
62
|
-
|
29
|
+
def format_errors(all_errors)
|
30
|
+
formatted_errors = []
|
63
31
|
|
64
|
-
|
65
|
-
|
66
|
-
def initialize(filename:, klass:, wiring:, organizer: nil)
|
67
|
-
@filename = filename
|
68
|
-
@klass = klass
|
69
|
-
@wiring = wiring
|
70
|
-
end
|
71
|
-
|
72
|
-
def validate_callable(error_context: ErrorContext.new)
|
73
|
-
if organizer?
|
74
|
-
assign_previously_defined(error_context: error_context)
|
75
|
-
validate_children(error_context: error_context)
|
76
|
-
end
|
77
|
-
|
78
|
-
validate_self(error_context: error_context)
|
79
|
-
end
|
80
|
-
|
81
|
-
def promised_keys
|
82
|
-
Array(klass.contract.promises.instance_eval { @terms }.json&.rules&.keys)
|
83
|
-
end
|
84
|
-
|
85
|
-
def expected_keys
|
86
|
-
Array(klass.contract.expectations.instance_eval { @terms }.json&.rules&.keys)
|
32
|
+
each_error(all_errors) do |missing, interactor, organizer|
|
33
|
+
format_error(missing, interactor, organizer, formatted_errors)
|
87
34
|
end
|
88
35
|
|
89
|
-
|
90
|
-
expected_keys.concat(promised_keys)
|
91
|
-
end
|
92
|
-
|
93
|
-
def inspect
|
94
|
-
"#<#{self.class.name}#{object_id} @filename=#{filename}, @klass=#{klass.name}>"
|
95
|
-
end
|
96
|
-
|
97
|
-
def organizer?
|
98
|
-
klass.respond_to?(:organized)
|
99
|
-
end
|
100
|
-
|
101
|
-
def assign_previously_defined(error_context:)
|
102
|
-
return unless contract?
|
103
|
-
|
104
|
-
error_context.append_previously_defined_keys(all_keys)
|
105
|
-
end
|
106
|
-
|
107
|
-
def validate_children(error_context:)
|
108
|
-
klass.organized.each do |interactor|
|
109
|
-
nested_callable = interactor_lookup[interactor]
|
110
|
-
next if nested_callable.nil?
|
111
|
-
|
112
|
-
error_context = nested_callable.validate_callable(error_context: error_context)
|
113
|
-
end
|
114
|
-
|
115
|
-
error_context
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
def contract?
|
121
|
-
klass.ancestors.include? Interactor::Contracts
|
122
|
-
end
|
123
|
-
|
124
|
-
def validate_self(error_context:)
|
125
|
-
return error_context unless contract?
|
126
|
-
|
127
|
-
error_context.infer_missing_keys(self)
|
128
|
-
error_context.add_promised_keys(promised_keys)
|
129
|
-
error_context
|
130
|
-
end
|
36
|
+
formatted_errors.join("\n\n")
|
131
37
|
end
|
132
38
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
end
|
137
|
-
|
138
|
-
def append_previously_defined_keys(keys)
|
139
|
-
keys.each do |key|
|
140
|
-
previously_defined_keys << key
|
141
|
-
end
|
142
|
-
end
|
39
|
+
def each_error(all_errors)
|
40
|
+
all_errors.each do |organizer, error_context|
|
41
|
+
next if ignore_klass?(ignore, organizer.klass)
|
143
42
|
|
144
|
-
|
145
|
-
|
146
|
-
end
|
43
|
+
error_context.missing_keys.each do |interactor, missing|
|
44
|
+
next if ignore_klass?(ignore, interactor.klass)
|
147
45
|
|
148
|
-
|
149
|
-
promised_keys.each do |key|
|
150
|
-
previously_defined_keys << key
|
46
|
+
yield missing, interactor, organizer
|
151
47
|
end
|
152
48
|
end
|
153
|
-
|
154
|
-
def infer_missing_keys(callable)
|
155
|
-
new_keys = callable.expected_keys
|
156
|
-
not_in_previous_keys = new_keys.reject { |key| previously_defined_keys.include?(key) }
|
157
|
-
|
158
|
-
add_missing_keys(callable, not_in_previous_keys)
|
159
|
-
end
|
160
|
-
|
161
|
-
def add_missing_keys(callable, not_in_previous_keys)
|
162
|
-
return if not_in_previous_keys.empty?
|
163
|
-
|
164
|
-
missing_keys[callable] ||= Set.new
|
165
|
-
missing_keys[callable] += not_in_previous_keys
|
166
|
-
end
|
167
49
|
end
|
168
50
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end.compact.select(&:organizer?)
|
176
|
-
end
|
177
|
-
|
178
|
-
def interactors
|
179
|
-
@interactors ||= interactor_files.flat_map do |f|
|
180
|
-
callables_in_file(f)
|
181
|
-
end.compact.reject(&:organizer?)
|
182
|
-
end
|
183
|
-
|
184
|
-
def callables_in_file(f)
|
185
|
-
@callables_in_file ||= {}
|
186
|
-
|
187
|
-
@callables_in_file[f] ||= _callables_in_file(f)
|
188
|
-
end
|
189
|
-
|
190
|
-
def _callables_in_file(f)
|
191
|
-
constant = constant_for(f)
|
192
|
-
return if constant == Interactify
|
193
|
-
|
194
|
-
internal_klasses = internal_constants_for(constant)
|
195
|
-
|
196
|
-
([constant] + internal_klasses).map { |k| new_callable(f, k, self) }
|
197
|
-
end
|
198
|
-
|
199
|
-
def internal_constants_for(constant)
|
200
|
-
constant
|
201
|
-
.constants
|
202
|
-
.map { |sym| constant_from_symbol(constant, sym) }
|
203
|
-
.select { |pk| interactor_klass?(pk) }
|
204
|
-
end
|
205
|
-
|
206
|
-
def constant_from_symbol(constant, symbol)
|
207
|
-
constant.module_eval do
|
208
|
-
symbol.to_s.constantize
|
209
|
-
rescue StandardError
|
210
|
-
"#{constant.name}::#{symbol}".constantize rescue nil
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def interactor_klass?(object)
|
215
|
-
return unless object.is_a? Class
|
216
|
-
|
217
|
-
object.ancestors.include?(Interactor) || object.ancestors.include?(Interactor::Organizer)
|
218
|
-
end
|
219
|
-
|
220
|
-
def new_callable(filename, klass, wiring)
|
221
|
-
CallableRepresentation.new(filename: filename, klass: klass, wiring: wiring)
|
222
|
-
end
|
223
|
-
|
224
|
-
def interactor_lookup
|
225
|
-
@interactor_lookup ||= (interactors + organizers).index_by(&:klass)
|
226
|
-
end
|
227
|
-
|
228
|
-
private
|
229
|
-
|
230
|
-
def constant_for(filename)
|
231
|
-
require filename
|
232
|
-
|
233
|
-
underscored_klass_name = underscored_klass_name_without_outer_namespace(filename)
|
234
|
-
underscored_klass_name = trim_rails_folder underscored_klass_name
|
235
|
-
|
236
|
-
klass_name = underscored_klass_name.classify
|
237
|
-
|
238
|
-
should_pluralize = filename[underscored_klass_name.pluralize]
|
239
|
-
klass_name = klass_name.pluralize if should_pluralize
|
240
|
-
|
241
|
-
outer_namespace_constant.const_get(klass_name)
|
242
|
-
end
|
243
|
-
|
244
|
-
# Example:
|
245
|
-
# trim_rails_folder("interactors/namespace/sub_namespace/class_name.rb")
|
246
|
-
# => "namespace/sub_namespace/class_name.rb"
|
247
|
-
def trim_rails_folder(filename)
|
248
|
-
rails_folders = Dir.glob(Interactify.root / '*').map { |f| File.basename(f) }
|
249
|
-
|
250
|
-
rails_folders.each do |folder|
|
251
|
-
regexable_folder = Regexp.quote("#{folder}/")
|
252
|
-
regex = /^#{regexable_folder}/
|
253
|
-
|
254
|
-
return filename.gsub(regex, '') if filename.match?(regex)
|
255
|
-
end
|
256
|
-
|
257
|
-
filename
|
258
|
-
end
|
259
|
-
|
260
|
-
# Example:
|
261
|
-
# "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
|
262
|
-
# "/namespace/sub_namespace/class_name.rb"
|
263
|
-
# ['', 'namespace', 'sub_namespace', 'class_name.rb']
|
264
|
-
# ['namespace', 'sub_namespace', 'class_name.rb']
|
265
|
-
# remove outernamespace (SpecSupport)
|
266
|
-
def underscored_klass_name_without_outer_namespace(filename)
|
267
|
-
filename.to_s # "/home/code/something/app/interactors/namespace/sub_namespace/class_name.rb"
|
268
|
-
.gsub(root.to_s, '') # "/namespace/sub_namespace/class_name.rb"
|
269
|
-
.gsub('/concerns', '') # concerns directory is ignored by Zeitwerk
|
270
|
-
.split('/') # "['', 'namespace', 'sub_namespace', 'class_name.rb']
|
271
|
-
.compact_blank # "['namespace', 'sub_namespace', 'class_name.rb']
|
272
|
-
.reject.with_index { |segment, i| i.zero? && segment == namespace }
|
273
|
-
.join('/') # 'namespace/sub_namespace/class_name.rb'
|
274
|
-
.gsub(/\.rb\z/, '') # 'namespace/sub_namespace/class_name'
|
275
|
-
end
|
276
|
-
|
277
|
-
def outer_namespace_constant
|
278
|
-
@outer_namespace_constant ||= Object.const_get(namespace)
|
279
|
-
end
|
51
|
+
def format_error(missing, interactor, organizer, formatted_errors)
|
52
|
+
formatted_errors << <<~ERROR
|
53
|
+
Missing keys: #{missing.to_a.map(&:to_sym).map(&:inspect).join(', ')}
|
54
|
+
expected in: #{interactor.klass}
|
55
|
+
called by: #{organizer.klass}
|
56
|
+
ERROR
|
280
57
|
end
|
281
58
|
|
282
|
-
|
283
|
-
|
284
|
-
|
59
|
+
def ignore_klass?(ignore, klass)
|
60
|
+
case ignore
|
61
|
+
when Array
|
62
|
+
ignore.any? { ignore_klass?(_1, klass) }
|
63
|
+
when Regexp
|
64
|
+
klass.to_s =~ ignore
|
65
|
+
when String
|
66
|
+
klass.to_s[ignore]
|
67
|
+
when Proc
|
68
|
+
ignore.call(klass)
|
69
|
+
when Class
|
70
|
+
klass <= ignore
|
285
71
|
end
|
72
|
+
end
|
286
73
|
|
287
|
-
|
288
|
-
possible_files.select { |_, contents| interactor_file?(contents) }.map(&:first).sort
|
289
|
-
end
|
74
|
+
delegate :organizers, :interactors, :interactor_lookup, to: :constants
|
290
75
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
76
|
+
def constants
|
77
|
+
@constants ||= Constants.new(
|
78
|
+
root:,
|
79
|
+
organizer_files:,
|
80
|
+
interactor_files:
|
81
|
+
)
|
82
|
+
end
|
295
83
|
|
296
|
-
|
297
|
-
file_contents['include Interactify'] || file_contents[/include Interactor$/]
|
298
|
-
end
|
84
|
+
delegate :organizer_files, :interactor_files, to: :files
|
299
85
|
|
300
|
-
|
301
|
-
|
302
|
-
end
|
86
|
+
def files
|
87
|
+
@files ||= Files.new(root:)
|
303
88
|
end
|
304
89
|
end
|
305
90
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
require "sidekiq/job"
|
5
|
+
|
6
|
+
require "interactify/async_job_klass"
|
3
7
|
|
4
8
|
module Interactify
|
5
9
|
class JobMaker
|
@@ -13,93 +17,40 @@ module Interactify
|
|
13
17
|
end
|
14
18
|
|
15
19
|
concerning :JobClass do
|
16
|
-
def
|
17
|
-
@
|
20
|
+
def job_klass
|
21
|
+
@job_klass ||= define_job_klass
|
18
22
|
end
|
19
23
|
|
20
24
|
private
|
21
25
|
|
22
|
-
def
|
26
|
+
def define_job_klass
|
23
27
|
this = self
|
24
28
|
|
25
29
|
invalid_keys = this.opts.symbolize_keys.keys - %i[queue retry dead backtrace pool tags]
|
26
30
|
|
27
31
|
raise ArgumentError, "Invalid keys: #{invalid_keys}" if invalid_keys.any?
|
28
32
|
|
29
|
-
|
33
|
+
build_job_klass(opts).tap do |klass|
|
34
|
+
klass.const_set(:JOBABLE_OPTS, opts)
|
35
|
+
klass.const_set(:JOBABLE_METHOD_NAME, method_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_job_klass(opts)
|
40
|
+
Class.new do
|
30
41
|
include Sidekiq::Job
|
31
42
|
|
32
|
-
sidekiq_options(
|
43
|
+
sidekiq_options(opts)
|
33
44
|
|
34
45
|
def perform(...)
|
35
46
|
self.class.module_parent.send(self.class::JOBABLE_METHOD_NAME, ...)
|
36
47
|
end
|
37
48
|
end
|
38
|
-
|
39
|
-
job_class.const_set(:JOBABLE_OPTS, opts)
|
40
|
-
job_class.const_set(:JOBABLE_METHOD_NAME, method_name)
|
41
|
-
job_class
|
42
49
|
end
|
43
50
|
end
|
44
51
|
|
45
|
-
|
46
|
-
|
47
|
-
klass = Class.new do
|
48
|
-
include Interactor
|
49
|
-
include Interactor::Contracts
|
50
|
-
end
|
51
|
-
|
52
|
-
attach_call(klass)
|
53
|
-
attach_call!(klass)
|
54
|
-
|
55
|
-
klass
|
56
|
-
end
|
57
|
-
|
58
|
-
def args(context)
|
59
|
-
args = context.to_h.stringify_keys
|
60
|
-
|
61
|
-
return args unless container_klass.respond_to?(:contract)
|
62
|
-
|
63
|
-
restrict_to_optional_or_keys_from_contract(args)
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
def attach_call(async_job_class)
|
69
|
-
# e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
|
70
|
-
async_job_class.send(:define_singleton_method, :call) do |context|
|
71
|
-
call!(context)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def attach_call!(async_job_class)
|
76
|
-
this = self
|
77
|
-
|
78
|
-
# e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
|
79
|
-
async_job_class.send(:define_singleton_method, :call!) do |context|
|
80
|
-
# e.g. SomeInteractor::JobWithSuffix
|
81
|
-
job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
|
82
|
-
|
83
|
-
# e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
|
84
|
-
job_klass.perform_async(this.args(context))
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def restrict_to_optional_or_keys_from_contract(args)
|
89
|
-
keys = container_klass
|
90
|
-
.contract
|
91
|
-
.expectations
|
92
|
-
.instance_eval { @terms }
|
93
|
-
.schema
|
94
|
-
.key_map
|
95
|
-
.to_dot_notation
|
96
|
-
.map(&:to_s)
|
97
|
-
|
98
|
-
optional = Array(container_klass.optional_attrs).map(&:to_s)
|
99
|
-
keys += optional
|
100
|
-
|
101
|
-
args.slice(*keys)
|
102
|
-
end
|
52
|
+
def async_job_klass
|
53
|
+
AsyncJobKlass.new(container_klass:, klass_suffix:).async_job_klass
|
103
54
|
end
|
104
55
|
end
|
105
56
|
end
|
data/lib/interactify/jobable.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/job_maker"
|
2
4
|
|
3
5
|
module Interactify
|
4
6
|
module Jobable
|
@@ -18,7 +20,7 @@ module Interactify
|
|
18
20
|
|
19
21
|
to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
|
20
22
|
|
21
|
-
klass.send(to_call, opts
|
23
|
+
klass.send(to_call, opts:, method_name: jobable_method_name)
|
22
24
|
super(klass)
|
23
25
|
end
|
24
26
|
end
|
@@ -51,18 +53,18 @@ module Interactify
|
|
51
53
|
# obviously you will need to be aware that later interactors
|
52
54
|
# in an interactor chain cannot depend on the result of the async
|
53
55
|
# interactor
|
54
|
-
def interactor_job(method_name: :call!, opts: {}, klass_suffix:
|
56
|
+
def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
|
55
57
|
job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
|
56
58
|
# with WhateverInteractor::Job you can perform the interactor as a job
|
57
59
|
# from sidekiq
|
58
60
|
# e.g. WhateverInteractor::Job.perform_async(...)
|
59
|
-
const_set("Job#{klass_suffix}", job_maker.
|
61
|
+
const_set("Job#{klass_suffix}", job_maker.job_klass)
|
60
62
|
|
61
63
|
# with WhateverInteractor::Async you can call WhateverInteractor::Job
|
62
64
|
# in an organizer oro on its oen using normal interactor call call! semantics
|
63
65
|
# e.g. WhateverInteractor::Async.call(...)
|
64
66
|
# WhateverInteractor::Async.call!(...)
|
65
|
-
const_set("Async#{klass_suffix}", job_maker.
|
67
|
+
const_set("Async#{klass_suffix}", job_maker.async_job_klass)
|
66
68
|
end
|
67
69
|
|
68
70
|
# if this was defined in ExampleClass this creates the following class
|
@@ -80,10 +82,10 @@ module Interactify
|
|
80
82
|
# # the following class is created that you can use to enqueue a job
|
81
83
|
# in the sidekiq yaml file
|
82
84
|
# ExampleClass::Job.some_method
|
83
|
-
def job_calling(method_name:, opts: {}, klass_suffix:
|
85
|
+
def job_calling(method_name:, opts: {}, klass_suffix: "")
|
84
86
|
job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
|
85
87
|
|
86
|
-
const_set("Job#{klass_suffix}", job_maker.
|
88
|
+
const_set("Job#{klass_suffix}", job_maker.job_klass)
|
87
89
|
end
|
88
90
|
end
|
89
91
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "interactify/contract_failure"
|
4
|
+
|
5
|
+
module Interactify
|
6
|
+
class MismatchingPromiseError < ContractFailure
|
7
|
+
def initialize(interactor, promising, promised_keys)
|
8
|
+
super <<~MESSAGE.chomp
|
9
|
+
#{interactor} does not promise:
|
10
|
+
#{promising.inspect}
|
11
|
+
|
12
|
+
Actual promises are:
|
13
|
+
#{promised_keys.inspect}
|
14
|
+
MESSAGE
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Interactify
|
2
4
|
module OrganizerCallMonkeyPatch
|
3
5
|
extend ActiveSupport::Concern
|
@@ -30,8 +32,11 @@ module Interactify
|
|
30
32
|
def call
|
31
33
|
self.class.organized.each do |interactor|
|
32
34
|
instance = interactor.new(context)
|
33
|
-
|
34
|
-
|
35
|
+
|
36
|
+
instance.instance_variable_set(
|
37
|
+
:@_interactor_called_by_non_bang_method,
|
38
|
+
@_interactor_called_by_non_bang_method
|
39
|
+
)
|
35
40
|
|
36
41
|
instance.tap(&:run!)
|
37
42
|
end
|