senro_usecaser 0.1.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +72 -0
- data/LICENSE +21 -0
- data/README.md +1069 -0
- data/Rakefile +12 -0
- data/Steepfile +24 -0
- data/examples/RBS_GENERATION.md +16 -0
- data/examples/namespace_demo.rb +751 -0
- data/examples/order_system.rb +1279 -0
- data/examples/sig/namespace_demo.rbs +279 -0
- data/examples/sig/order_system.rbs +685 -0
- data/lefthook.yml +31 -0
- data/lib/senro_usecaser/base.rb +660 -0
- data/lib/senro_usecaser/configuration.rb +149 -0
- data/lib/senro_usecaser/container.rb +315 -0
- data/lib/senro_usecaser/error.rb +88 -0
- data/lib/senro_usecaser/provider.rb +212 -0
- data/lib/senro_usecaser/result.rb +182 -0
- data/lib/senro_usecaser/version.rb +8 -0
- data/lib/senro_usecaser.rb +155 -0
- data/sig/generated/senro_usecaser/base.rbs +365 -0
- data/sig/generated/senro_usecaser/configuration.rbs +80 -0
- data/sig/generated/senro_usecaser/container.rbs +190 -0
- data/sig/generated/senro_usecaser/error.rbs +58 -0
- data/sig/generated/senro_usecaser/provider.rbs +153 -0
- data/sig/generated/senro_usecaser/result.rbs +109 -0
- data/sig/generated/senro_usecaser/version.rbs +6 -0
- data/sig/generated/senro_usecaser.rbs +113 -0
- data/sig/overrides.rbs +16 -0
- metadata +77 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module SenroUsecaser
|
|
6
|
+
# Represents a step in an organized pipeline
|
|
7
|
+
class Step
|
|
8
|
+
attr_reader :use_case_class, :if_condition, :unless_condition, :on_failure, :all_conditions, :any_conditions,
|
|
9
|
+
:input_mapping
|
|
10
|
+
|
|
11
|
+
# rubocop:disable Metrics/ParameterLists
|
|
12
|
+
#: (singleton(Base), ?if_condition: (Symbol | Proc)?, ?unless_condition: (Symbol | Proc)?, ?on_failure: Symbol?,
|
|
13
|
+
#: ?all_conditions: Array[(Symbol | Proc)]?, ?any_conditions: Array[(Symbol | Proc)]?,
|
|
14
|
+
#: ?input_mapping: (Symbol | Proc)?) -> void
|
|
15
|
+
def initialize(use_case_class, if_condition: nil, unless_condition: nil, on_failure: nil,
|
|
16
|
+
all_conditions: nil, any_conditions: nil, input_mapping: nil)
|
|
17
|
+
@use_case_class = use_case_class
|
|
18
|
+
@if_condition = if_condition
|
|
19
|
+
@unless_condition = unless_condition
|
|
20
|
+
@on_failure = on_failure
|
|
21
|
+
@all_conditions = all_conditions
|
|
22
|
+
@any_conditions = any_conditions
|
|
23
|
+
@input_mapping = input_mapping
|
|
24
|
+
end
|
|
25
|
+
# rubocop:enable Metrics/ParameterLists
|
|
26
|
+
|
|
27
|
+
# Checks if this step should be executed based on conditions
|
|
28
|
+
#
|
|
29
|
+
#: (untyped, untyped) -> bool
|
|
30
|
+
def should_execute?(input, use_case_instance)
|
|
31
|
+
return false if if_condition && !evaluate_condition(if_condition, input, use_case_instance)
|
|
32
|
+
return false if unless_condition && evaluate_condition(unless_condition, input, use_case_instance)
|
|
33
|
+
return false if all_conditions && !all_conditions_met?(input, use_case_instance)
|
|
34
|
+
return false if any_conditions && !any_condition_met?(input, use_case_instance)
|
|
35
|
+
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Maps the input for this step based on input_mapping configuration
|
|
40
|
+
#
|
|
41
|
+
#: (untyped, untyped) -> untyped
|
|
42
|
+
def map_input(input, use_case_instance)
|
|
43
|
+
return input unless input_mapping
|
|
44
|
+
|
|
45
|
+
case input_mapping
|
|
46
|
+
when Symbol
|
|
47
|
+
use_case_instance.send(input_mapping, input)
|
|
48
|
+
when Proc
|
|
49
|
+
input_mapping.call(input)
|
|
50
|
+
else
|
|
51
|
+
input
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
#: ((Symbol | Proc), untyped, untyped) -> bool
|
|
58
|
+
def evaluate_condition(condition, input, use_case_instance)
|
|
59
|
+
case condition
|
|
60
|
+
when Symbol
|
|
61
|
+
use_case_instance.send(condition, input)
|
|
62
|
+
when Proc
|
|
63
|
+
condition.call(input)
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError, "Invalid condition type: #{condition.class}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: (untyped, untyped) -> bool
|
|
70
|
+
def all_conditions_met?(input, use_case_instance)
|
|
71
|
+
all_conditions.all? { |cond| evaluate_condition(cond, input, use_case_instance) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#: (untyped, untyped) -> bool
|
|
75
|
+
def any_condition_met?(input, use_case_instance)
|
|
76
|
+
any_conditions.any? { |cond| evaluate_condition(cond, input, use_case_instance) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Base class for all UseCases
|
|
81
|
+
#
|
|
82
|
+
# @example Basic UseCase with keyword arguments
|
|
83
|
+
# class CreateUserUseCase < SenroUsecaser::Base
|
|
84
|
+
# def call(name:, email:)
|
|
85
|
+
# user = User.create(name: name, email: email)
|
|
86
|
+
# success(user)
|
|
87
|
+
# end
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
# result = CreateUserUseCase.call(name: "Taro", email: "taro@example.com")
|
|
91
|
+
#
|
|
92
|
+
# @example With input/output classes (recommended for pipelines)
|
|
93
|
+
# class CreateUserUseCase < SenroUsecaser::Base
|
|
94
|
+
# input CreateUserInput
|
|
95
|
+
# output CreateUserOutput
|
|
96
|
+
#
|
|
97
|
+
# def call(input)
|
|
98
|
+
# user = User.create(name: input.name, email: input.email)
|
|
99
|
+
# success(CreateUserOutput.new(user: user))
|
|
100
|
+
# end
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# @example Pipeline with input/output chaining
|
|
104
|
+
# class StepA < SenroUsecaser::Base
|
|
105
|
+
# input AInput
|
|
106
|
+
# output AOutput
|
|
107
|
+
# def call(input)
|
|
108
|
+
# success(AOutput.new(value: input.value * 2))
|
|
109
|
+
# end
|
|
110
|
+
# end
|
|
111
|
+
#
|
|
112
|
+
# class StepB < SenroUsecaser::Base
|
|
113
|
+
# input AOutput # Receives StepA's output directly
|
|
114
|
+
# output BOutput
|
|
115
|
+
# def call(input)
|
|
116
|
+
# success(BOutput.new(result: input.value + 1))
|
|
117
|
+
# end
|
|
118
|
+
# end
|
|
119
|
+
#
|
|
120
|
+
# class Pipeline < SenroUsecaser::Base
|
|
121
|
+
# organize StepA, StepB
|
|
122
|
+
# end
|
|
123
|
+
class Base
|
|
124
|
+
class << self
|
|
125
|
+
# Declares a dependency to be injected from the container
|
|
126
|
+
#
|
|
127
|
+
#: (Symbol, ?Class) -> void
|
|
128
|
+
def depends_on(name, type = nil)
|
|
129
|
+
dependencies << name unless dependencies.include?(name)
|
|
130
|
+
dependency_types[name] = type if type
|
|
131
|
+
|
|
132
|
+
define_method(name) do
|
|
133
|
+
@_dependencies[name]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Returns the list of declared dependencies
|
|
138
|
+
#
|
|
139
|
+
#: () -> Array[Symbol]
|
|
140
|
+
def dependencies
|
|
141
|
+
@dependencies ||= []
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns the dependency type mapping
|
|
145
|
+
#
|
|
146
|
+
#: () -> Hash[Symbol, Class]
|
|
147
|
+
def dependency_types
|
|
148
|
+
@dependency_types ||= {}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Sets the namespace for dependency resolution
|
|
152
|
+
#
|
|
153
|
+
#: ((Symbol | String)) -> void
|
|
154
|
+
def namespace(name)
|
|
155
|
+
@use_case_namespace = name
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Returns the declared namespace
|
|
159
|
+
#
|
|
160
|
+
#: () -> (Symbol | String)?
|
|
161
|
+
attr_reader :use_case_namespace
|
|
162
|
+
|
|
163
|
+
# Declares a sequence of UseCases to execute as a pipeline
|
|
164
|
+
#
|
|
165
|
+
# @example Basic organize
|
|
166
|
+
# organize StepA, StepB, StepC
|
|
167
|
+
#
|
|
168
|
+
# @example With block and step
|
|
169
|
+
# organize do
|
|
170
|
+
# step StepA
|
|
171
|
+
# step StepB, if: :should_run?
|
|
172
|
+
# step StepC, on_failure: :continue
|
|
173
|
+
# end
|
|
174
|
+
#
|
|
175
|
+
#: (*Class, ?on_failure: Symbol) ?{ () -> void } -> void
|
|
176
|
+
def organize(*use_case_classes, on_failure: :stop, &block)
|
|
177
|
+
@on_failure_strategy = on_failure
|
|
178
|
+
|
|
179
|
+
if block
|
|
180
|
+
@organized_steps = [] #: Array[Step]
|
|
181
|
+
@_defining_steps = true
|
|
182
|
+
instance_eval(&block) # steep:ignore BlockTypeMismatch
|
|
183
|
+
@_defining_steps = false
|
|
184
|
+
else
|
|
185
|
+
@organized_steps = use_case_classes.map { |klass| Step.new(klass) }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Defines a step in the organize block
|
|
190
|
+
#
|
|
191
|
+
# rubocop:disable Metrics/ParameterLists
|
|
192
|
+
#: (Class, ?if: (Symbol | Proc)?, ?unless: (Symbol | Proc)?, ?on_failure: Symbol?,
|
|
193
|
+
#: ?all: Array[(Symbol | Proc)]?, ?any: Array[(Symbol | Proc)]?,
|
|
194
|
+
#: ?input: (Symbol | Proc)?) -> void
|
|
195
|
+
def step(use_case_class, if: nil, unless: nil, on_failure: nil, all: nil, any: nil, input: nil)
|
|
196
|
+
raise "step can only be called inside organize block" unless @_defining_steps
|
|
197
|
+
|
|
198
|
+
@organized_steps << Step.new(
|
|
199
|
+
use_case_class,
|
|
200
|
+
if_condition: binding.local_variable_get(:if),
|
|
201
|
+
unless_condition: binding.local_variable_get(:unless),
|
|
202
|
+
on_failure: on_failure,
|
|
203
|
+
all_conditions: all,
|
|
204
|
+
any_conditions: any,
|
|
205
|
+
input_mapping: input
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
# rubocop:enable Metrics/ParameterLists
|
|
209
|
+
|
|
210
|
+
# Returns the list of organized steps
|
|
211
|
+
#
|
|
212
|
+
#: () -> Array[Step]?
|
|
213
|
+
attr_reader :organized_steps
|
|
214
|
+
|
|
215
|
+
# Returns the failure handling strategy
|
|
216
|
+
#
|
|
217
|
+
#: () -> Symbol
|
|
218
|
+
def on_failure_strategy
|
|
219
|
+
@on_failure_strategy || :stop
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Adds extension modules with hooks
|
|
223
|
+
#
|
|
224
|
+
#: (*Module) -> void
|
|
225
|
+
def extend_with(*extensions)
|
|
226
|
+
extensions.each { |ext| self.extensions << ext }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Returns the list of extensions
|
|
230
|
+
#
|
|
231
|
+
#: () -> Array[Module]
|
|
232
|
+
def extensions
|
|
233
|
+
@extensions ||= []
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Adds a before hook
|
|
237
|
+
#
|
|
238
|
+
#: () { (untyped) -> void } -> void
|
|
239
|
+
def before(&block)
|
|
240
|
+
before_hooks << block
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Returns the list of before hooks
|
|
244
|
+
#
|
|
245
|
+
#: () -> Array[Proc]
|
|
246
|
+
def before_hooks
|
|
247
|
+
@before_hooks ||= []
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Adds an after hook
|
|
251
|
+
#
|
|
252
|
+
#: () { (untyped, Result[untyped]) -> void } -> void
|
|
253
|
+
def after(&block)
|
|
254
|
+
after_hooks << block
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Returns the list of after hooks
|
|
258
|
+
#
|
|
259
|
+
#: () -> Array[Proc]
|
|
260
|
+
def after_hooks
|
|
261
|
+
@after_hooks ||= []
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Adds an around hook
|
|
265
|
+
#
|
|
266
|
+
#: () { (untyped) { () -> Result[untyped] } -> Result[untyped] } -> void
|
|
267
|
+
def around(&block)
|
|
268
|
+
around_hooks << block if block
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Returns the list of around hooks
|
|
272
|
+
#
|
|
273
|
+
#: () -> Array[Proc]
|
|
274
|
+
def around_hooks
|
|
275
|
+
@around_hooks ||= []
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Declares the expected input type for this UseCase
|
|
279
|
+
#
|
|
280
|
+
#: (Class) -> void
|
|
281
|
+
def input(type)
|
|
282
|
+
@input_class = type
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Returns the input class
|
|
286
|
+
#
|
|
287
|
+
#: () -> Class?
|
|
288
|
+
attr_reader :input_class
|
|
289
|
+
|
|
290
|
+
# Declares the expected output type for this UseCase
|
|
291
|
+
#
|
|
292
|
+
#: ((Class | Hash[Symbol, Class])) -> void
|
|
293
|
+
def output(type_or_schema)
|
|
294
|
+
@output_schema = type_or_schema
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Returns the output schema
|
|
298
|
+
#
|
|
299
|
+
#: () -> (Class | Hash[Symbol, Class])?
|
|
300
|
+
attr_reader :output_schema
|
|
301
|
+
|
|
302
|
+
# Calls the UseCase with the given input
|
|
303
|
+
#
|
|
304
|
+
#: [T] (?untyped, ?container: Container, **untyped) -> Result[T]
|
|
305
|
+
def call(input = nil, container: nil, **args)
|
|
306
|
+
new(container: container).perform(input, capture_exceptions: false, **args)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Calls the UseCase and captures any exceptions as failures
|
|
310
|
+
#
|
|
311
|
+
#: [T] (?untyped, ?container: Container, **untyped) -> Result[T]
|
|
312
|
+
def call!(input = nil, container: nil, **args)
|
|
313
|
+
new(container: container).perform(input, capture_exceptions: true, **args)
|
|
314
|
+
rescue StandardError => e
|
|
315
|
+
Result.from_exception(e)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Calls the UseCase with custom exception handling options
|
|
319
|
+
#
|
|
320
|
+
#: [T] (input: untyped, ?container: Container, ?exception_classes: Array[Class], ?code: Symbol) -> Result[T]
|
|
321
|
+
def call_with_capture(input:, container: nil, exception_classes: [StandardError], code: :exception)
|
|
322
|
+
new(container: container).perform(input)
|
|
323
|
+
rescue *exception_classes => e
|
|
324
|
+
Result.from_exception(e, code: code)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @api private
|
|
328
|
+
def inherited(subclass)
|
|
329
|
+
super
|
|
330
|
+
copy_configuration_to(subclass)
|
|
331
|
+
copy_hooks_to(subclass)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
private
|
|
335
|
+
|
|
336
|
+
def copy_configuration_to(subclass)
|
|
337
|
+
subclass.instance_variable_set(:@dependencies, dependencies.dup)
|
|
338
|
+
subclass.instance_variable_set(:@dependency_types, dependency_types.dup)
|
|
339
|
+
subclass.instance_variable_set(:@use_case_namespace, @use_case_namespace)
|
|
340
|
+
subclass.instance_variable_set(:@organized_steps, @organized_steps&.dup)
|
|
341
|
+
subclass.instance_variable_set(:@on_failure_strategy, @on_failure_strategy)
|
|
342
|
+
subclass.instance_variable_set(:@input_class, @input_class)
|
|
343
|
+
subclass.instance_variable_set(:@output_schema, @output_schema)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def copy_hooks_to(subclass)
|
|
347
|
+
subclass.instance_variable_set(:@extensions, extensions.dup)
|
|
348
|
+
subclass.instance_variable_set(:@before_hooks, before_hooks.dup)
|
|
349
|
+
subclass.instance_variable_set(:@after_hooks, after_hooks.dup)
|
|
350
|
+
subclass.instance_variable_set(:@around_hooks, around_hooks.dup)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Initializes the UseCase with dependencies resolved from the container
|
|
355
|
+
#
|
|
356
|
+
#: (?container: Container?, ?dependencies: Hash[Symbol, untyped]) -> void
|
|
357
|
+
def initialize(container: nil, dependencies: {})
|
|
358
|
+
@_container = container || SenroUsecaser.container
|
|
359
|
+
@_dependencies = {} #: Hash[Symbol, untyped]
|
|
360
|
+
|
|
361
|
+
resolve_dependencies(@_container, dependencies)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Performs the UseCase with hooks
|
|
365
|
+
#
|
|
366
|
+
#: (untyped, ?capture_exceptions: bool) -> Result[untyped]
|
|
367
|
+
def perform(input, capture_exceptions: false)
|
|
368
|
+
@_capture_exceptions = capture_exceptions
|
|
369
|
+
|
|
370
|
+
unless self.class.input_class || self.class.organized_steps
|
|
371
|
+
raise ArgumentError, "#{self.class.name} must define `input` class"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
execute_with_hooks(input) do
|
|
375
|
+
call(input)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Executes the UseCase logic
|
|
380
|
+
#
|
|
381
|
+
#: (?untyped input) -> Result[untyped]
|
|
382
|
+
def call(input = nil)
|
|
383
|
+
return execute_pipeline(input) if self.class.organized_steps
|
|
384
|
+
|
|
385
|
+
raise NotImplementedError, "#{self.class.name}#call must be implemented"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
|
|
390
|
+
# Creates a success Result with the given value
|
|
391
|
+
#
|
|
392
|
+
#: [T] (T) -> Result[T]
|
|
393
|
+
def success(value)
|
|
394
|
+
Result.success(value)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Creates a failure Result with the given errors
|
|
398
|
+
#
|
|
399
|
+
#: (*Error) -> Result[untyped]
|
|
400
|
+
def failure(*errors)
|
|
401
|
+
Result.failure(*errors)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Creates a failure Result from an exception
|
|
405
|
+
#
|
|
406
|
+
#: (Exception, ?code: Symbol) -> Result[untyped]
|
|
407
|
+
def failure_from_exception(exception, code: :exception)
|
|
408
|
+
Result.from_exception(exception, code: code)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Executes a block and captures any exceptions as failures
|
|
412
|
+
#
|
|
413
|
+
#: [T] (*Class, ?code: Symbol) { () -> T } -> Result[T]
|
|
414
|
+
def capture(*exception_classes, code: :exception, &)
|
|
415
|
+
Result.capture(*exception_classes, code: code, &)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Executes the core logic with before/after/around hooks
|
|
419
|
+
#
|
|
420
|
+
#: (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
421
|
+
def execute_with_hooks(input, &core_block)
|
|
422
|
+
execution = build_around_chain(input, core_block)
|
|
423
|
+
run_before_hooks(input)
|
|
424
|
+
result = execution.call
|
|
425
|
+
run_after_hooks(input, result)
|
|
426
|
+
result
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Wraps a non-Result value in Result.success
|
|
430
|
+
#
|
|
431
|
+
#: (untyped) -> Result[untyped]
|
|
432
|
+
def wrap_result(value)
|
|
433
|
+
return value if value.is_a?(Result)
|
|
434
|
+
|
|
435
|
+
Result.success(value)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Builds the around hook chain
|
|
439
|
+
#
|
|
440
|
+
#: (untyped, Proc) -> Proc
|
|
441
|
+
def build_around_chain(input, core_block)
|
|
442
|
+
wrapped_core = -> { wrap_result(core_block.call) }
|
|
443
|
+
all_around_hooks = collect_around_hooks
|
|
444
|
+
|
|
445
|
+
all_around_hooks.reverse.reduce(wrapped_core) do |inner, hook|
|
|
446
|
+
-> { wrap_result(hook.call(input) { inner.call }) }
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Collects all around hooks from extensions and block-based hooks
|
|
451
|
+
#
|
|
452
|
+
#: () -> Array[Proc]
|
|
453
|
+
def collect_around_hooks
|
|
454
|
+
hooks = [] #: Array[Proc]
|
|
455
|
+
self.class.extensions.each do |ext|
|
|
456
|
+
hooks << ext.method(:around).to_proc if ext.respond_to?(:around)
|
|
457
|
+
end
|
|
458
|
+
hooks.concat(self.class.around_hooks)
|
|
459
|
+
hooks
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Runs all before hooks
|
|
463
|
+
#
|
|
464
|
+
#: (untyped) -> void
|
|
465
|
+
def run_before_hooks(input)
|
|
466
|
+
self.class.extensions.each do |ext|
|
|
467
|
+
ext.send(:before, input) if ext.respond_to?(:before)
|
|
468
|
+
end
|
|
469
|
+
self.class.before_hooks.each { |hook| hook.call(input) }
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Runs all after hooks
|
|
473
|
+
#
|
|
474
|
+
#: (untyped, Result[untyped]) -> void
|
|
475
|
+
def run_after_hooks(input, result)
|
|
476
|
+
self.class.extensions.each do |ext|
|
|
477
|
+
ext.send(:after, input, result) if ext.respond_to?(:after)
|
|
478
|
+
end
|
|
479
|
+
self.class.after_hooks.each { |hook| hook.call(input, result) }
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Resolves dependencies from the container
|
|
483
|
+
#
|
|
484
|
+
#: (Container, Hash[Symbol, untyped]) -> void
|
|
485
|
+
def resolve_dependencies(container, manual_dependencies)
|
|
486
|
+
self.class.dependencies.each do |name|
|
|
487
|
+
@_dependencies[name] = if manual_dependencies.key?(name)
|
|
488
|
+
manual_dependencies[name]
|
|
489
|
+
else
|
|
490
|
+
resolve_from_container(container, name)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Resolves a single dependency from the container
|
|
496
|
+
#
|
|
497
|
+
#: (Container, Symbol) -> untyped
|
|
498
|
+
def resolve_from_container(container, name)
|
|
499
|
+
namespace = effective_namespace
|
|
500
|
+
if namespace
|
|
501
|
+
container.resolve_in(namespace, name)
|
|
502
|
+
else
|
|
503
|
+
container.resolve(name)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Returns the effective namespace for dependency resolution
|
|
508
|
+
#
|
|
509
|
+
#: () -> (Symbol | String)?
|
|
510
|
+
def effective_namespace
|
|
511
|
+
return self.class.use_case_namespace if self.class.use_case_namespace
|
|
512
|
+
return nil unless SenroUsecaser.configuration.infer_namespace_from_module
|
|
513
|
+
|
|
514
|
+
infer_namespace_from_class
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Infers namespace from the class's module structure
|
|
518
|
+
#
|
|
519
|
+
#: () -> String?
|
|
520
|
+
def infer_namespace_from_class
|
|
521
|
+
class_name = self.class.name
|
|
522
|
+
return nil unless class_name
|
|
523
|
+
|
|
524
|
+
parts = class_name.split("::")
|
|
525
|
+
return nil if parts.length <= 1
|
|
526
|
+
|
|
527
|
+
module_parts = parts[0...-1] || [] #: Array[String]
|
|
528
|
+
return nil if module_parts.empty?
|
|
529
|
+
|
|
530
|
+
module_parts.map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2').downcase }.join("::")
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Executes the organized UseCase pipeline
|
|
534
|
+
#
|
|
535
|
+
#: (untyped) -> Result[untyped]
|
|
536
|
+
def execute_pipeline(input)
|
|
537
|
+
case self.class.on_failure_strategy
|
|
538
|
+
when :stop
|
|
539
|
+
execute_pipeline_stop(input)
|
|
540
|
+
when :continue
|
|
541
|
+
execute_pipeline_continue(input)
|
|
542
|
+
when :collect
|
|
543
|
+
execute_pipeline_collect(input)
|
|
544
|
+
else
|
|
545
|
+
raise ArgumentError, "Unknown on_failure strategy: #{self.class.on_failure_strategy}"
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Executes pipeline with :stop strategy
|
|
550
|
+
#
|
|
551
|
+
#: (untyped) -> Result[untyped]
|
|
552
|
+
def execute_pipeline_stop(input)
|
|
553
|
+
current_input = input
|
|
554
|
+
result = nil #: Result[untyped]?
|
|
555
|
+
|
|
556
|
+
self.class.organized_steps&.each do |step|
|
|
557
|
+
next unless step.should_execute?(current_input, self)
|
|
558
|
+
|
|
559
|
+
step_result = execute_step(step, current_input)
|
|
560
|
+
return step_result if step_result.failure? && step_should_stop?(step)
|
|
561
|
+
|
|
562
|
+
current_input = step_result.value if step_result.success?
|
|
563
|
+
result = step_result
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
result || success(current_input)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Executes pipeline with :continue strategy
|
|
570
|
+
#
|
|
571
|
+
#: (untyped) -> Result[untyped]
|
|
572
|
+
def execute_pipeline_continue(input)
|
|
573
|
+
current_input = input
|
|
574
|
+
result = nil #: Result[untyped]?
|
|
575
|
+
|
|
576
|
+
self.class.organized_steps&.each do |step|
|
|
577
|
+
next unless step.should_execute?(current_input, self)
|
|
578
|
+
|
|
579
|
+
step_result = execute_step(step, current_input)
|
|
580
|
+
return step_result if step_result.failure? && step.on_failure == :stop
|
|
581
|
+
|
|
582
|
+
current_input = step_result.value if step_result.success?
|
|
583
|
+
result = step_result
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
result || success(current_input)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Executes pipeline with :collect strategy
|
|
590
|
+
#
|
|
591
|
+
#: (untyped) -> Result[untyped]
|
|
592
|
+
def execute_pipeline_collect(input)
|
|
593
|
+
errors = [] #: Array[Error]
|
|
594
|
+
state = { input: input, errors: errors, last_success: nil }
|
|
595
|
+
|
|
596
|
+
self.class.organized_steps&.each do |step|
|
|
597
|
+
next unless step.should_execute?(state[:input], self)
|
|
598
|
+
|
|
599
|
+
result = execute_step(step, state[:input])
|
|
600
|
+
break if should_stop_collect_pipeline?(result, step, state)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
build_collect_result(state)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Updates collect state and checks if pipeline should stop
|
|
607
|
+
#
|
|
608
|
+
#: (Result[untyped], Step, Hash[Symbol, untyped]) -> bool
|
|
609
|
+
def should_stop_collect_pipeline?(result, step, state)
|
|
610
|
+
if result.failure?
|
|
611
|
+
state[:errors].concat(result.errors)
|
|
612
|
+
step.on_failure == :stop
|
|
613
|
+
else
|
|
614
|
+
state[:input] = result.value
|
|
615
|
+
state[:last_success] = result
|
|
616
|
+
false
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Builds the final result for collect mode
|
|
621
|
+
#
|
|
622
|
+
#: (Hash[Symbol, untyped]) -> Result[untyped]
|
|
623
|
+
def build_collect_result(state)
|
|
624
|
+
if state[:errors].any?
|
|
625
|
+
Result.failure(*state[:errors])
|
|
626
|
+
else
|
|
627
|
+
state[:last_success] || success(state[:input])
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Executes a single step in the pipeline
|
|
632
|
+
#
|
|
633
|
+
#: (Step, untyped) -> Result[untyped]
|
|
634
|
+
def execute_step(step, input)
|
|
635
|
+
mapped_input = step.map_input(input, self)
|
|
636
|
+
call_use_case(step.use_case_class, mapped_input)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
# Determines if a step failure should stop the pipeline
|
|
640
|
+
#
|
|
641
|
+
#: (Step) -> bool
|
|
642
|
+
def step_should_stop?(step)
|
|
643
|
+
step_strategy = step.on_failure || self.class.on_failure_strategy
|
|
644
|
+
step_strategy == :stop
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Calls a single UseCase in the pipeline
|
|
648
|
+
# Requires input_class to be defined for pipeline steps
|
|
649
|
+
#
|
|
650
|
+
#: (singleton(Base), untyped) -> Result[untyped]
|
|
651
|
+
def call_use_case(use_case_class, input)
|
|
652
|
+
unless use_case_class.input_class
|
|
653
|
+
raise ArgumentError, "#{use_case_class.name} must define `input` class to be used in a pipeline"
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
call_method = @_capture_exceptions || false ? :call! : :call #: Symbol
|
|
657
|
+
use_case_class.public_send(call_method, input, container: @_container)
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|