senro_usecaser 0.3.0 → 0.4.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 +4 -0
- data/README.md +655 -0
- data/examples/namespace_demo.rb +50 -15
- data/examples/order_system.rb +222 -34
- data/examples/sig/namespace_demo.rbs +35 -10
- data/examples/sig/order_system.rbs +196 -20
- data/lib/senro_usecaser/base.rb +308 -76
- data/lib/senro_usecaser/depends_on.rb +257 -0
- data/lib/senro_usecaser/hook.rb +28 -82
- data/lib/senro_usecaser/retry_configuration.rb +131 -0
- data/lib/senro_usecaser/retry_context.rb +133 -0
- data/lib/senro_usecaser/version.rb +1 -1
- data/lib/senro_usecaser.rb +3 -0
- data/sig/generated/senro_usecaser/base.rbs +143 -30
- data/sig/generated/senro_usecaser/depends_on.rbs +197 -0
- data/sig/generated/senro_usecaser/hook.rbs +23 -35
- data/sig/generated/senro_usecaser/retry_configuration.rbs +90 -0
- data/sig/generated/senro_usecaser/retry_context.rbs +101 -0
- data/sig/overrides.rbs +0 -1
- metadata +7 -1
data/lib/senro_usecaser/base.rb
CHANGED
|
@@ -121,44 +121,13 @@ module SenroUsecaser
|
|
|
121
121
|
# organize StepA, StepB
|
|
122
122
|
# end
|
|
123
123
|
class Base
|
|
124
|
-
|
|
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
|
|
124
|
+
extend DependsOn
|
|
150
125
|
|
|
151
|
-
|
|
152
|
-
#
|
|
153
|
-
#: ((Symbol | String)) -> void
|
|
154
|
-
def namespace(name)
|
|
155
|
-
@use_case_namespace = name
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Returns the declared namespace
|
|
126
|
+
class << self
|
|
127
|
+
# Alias for backward compatibility
|
|
159
128
|
#
|
|
160
129
|
#: () -> (Symbol | String)?
|
|
161
|
-
|
|
130
|
+
alias use_case_namespace declared_namespace
|
|
162
131
|
|
|
163
132
|
# Declares a sequence of UseCases to execute as a pipeline
|
|
164
133
|
#
|
|
@@ -276,6 +245,101 @@ module SenroUsecaser
|
|
|
276
245
|
@around_hooks ||= []
|
|
277
246
|
end
|
|
278
247
|
|
|
248
|
+
# Adds an on_failure hook
|
|
249
|
+
#
|
|
250
|
+
#: () { (untyped, Result[untyped], ?RetryContext?) -> void } -> void
|
|
251
|
+
def on_failure(&block)
|
|
252
|
+
on_failure_hooks << block
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the list of on_failure hooks
|
|
256
|
+
#
|
|
257
|
+
#: () -> Array[Proc]
|
|
258
|
+
def on_failure_hooks
|
|
259
|
+
@on_failure_hooks ||= []
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Configures automatic retry for specific error types
|
|
263
|
+
#
|
|
264
|
+
# @example Retry on network errors
|
|
265
|
+
# retry_on :network_error, attempts: 3, wait: 1
|
|
266
|
+
#
|
|
267
|
+
# @example Retry on exception class
|
|
268
|
+
# retry_on Net::OpenTimeout, attempts: 5, wait: 2, backoff: :exponential
|
|
269
|
+
#
|
|
270
|
+
# @example Multiple error types with jitter
|
|
271
|
+
# retry_on :rate_limited, :timeout, attempts: 3, wait: 1, jitter: 0.1
|
|
272
|
+
#
|
|
273
|
+
# rubocop:disable Metrics/ParameterLists
|
|
274
|
+
#: (*(Symbol | Class), ?attempts: Integer, ?wait: (Float | Integer),
|
|
275
|
+
#: ?backoff: Symbol, ?max_wait: (Float | Integer)?, ?jitter: (Float | Integer)) -> void
|
|
276
|
+
def retry_on(*error_matchers, attempts: 3, wait: 0, backoff: :fixed, max_wait: nil, jitter: 0)
|
|
277
|
+
retry_configurations << RetryConfiguration.new(
|
|
278
|
+
matchers: error_matchers.flatten,
|
|
279
|
+
attempts: attempts,
|
|
280
|
+
wait: wait,
|
|
281
|
+
backoff: backoff,
|
|
282
|
+
max_wait: max_wait,
|
|
283
|
+
jitter: jitter
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
# rubocop:enable Metrics/ParameterLists
|
|
287
|
+
|
|
288
|
+
# Returns the list of retry configurations
|
|
289
|
+
#
|
|
290
|
+
#: () -> Array[RetryConfiguration]
|
|
291
|
+
def retry_configurations
|
|
292
|
+
@retry_configurations ||= []
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Configures errors that should immediately discard (no retry)
|
|
296
|
+
#
|
|
297
|
+
# @example Discard on validation errors
|
|
298
|
+
# discard_on :validation_error, :not_found
|
|
299
|
+
#
|
|
300
|
+
# @example Discard on exception class
|
|
301
|
+
# discard_on ArgumentError
|
|
302
|
+
#
|
|
303
|
+
#: (*(Symbol | Class)) -> void
|
|
304
|
+
def discard_on(*error_matchers)
|
|
305
|
+
discard_matchers.concat(error_matchers.flatten)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Returns the list of discard matchers
|
|
309
|
+
#
|
|
310
|
+
#: () -> Array[(Symbol | Class)]
|
|
311
|
+
def discard_matchers
|
|
312
|
+
@discard_matchers ||= []
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Adds a before_retry hook
|
|
316
|
+
#
|
|
317
|
+
#: () { (untyped, Result[untyped], RetryContext) -> void } -> void
|
|
318
|
+
def before_retry(&block)
|
|
319
|
+
before_retry_hooks << block
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Returns the list of before_retry hooks
|
|
323
|
+
#
|
|
324
|
+
#: () -> Array[Proc]
|
|
325
|
+
def before_retry_hooks
|
|
326
|
+
@before_retry_hooks ||= []
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Adds an after_retries_exhausted hook
|
|
330
|
+
#
|
|
331
|
+
#: () { (untyped, Result[untyped], RetryContext) -> void } -> void
|
|
332
|
+
def after_retries_exhausted(&block)
|
|
333
|
+
after_retries_exhausted_hooks << block
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Returns the list of after_retries_exhausted hooks
|
|
337
|
+
#
|
|
338
|
+
#: () -> Array[Proc]
|
|
339
|
+
def after_retries_exhausted_hooks
|
|
340
|
+
@after_retries_exhausted_hooks ||= []
|
|
341
|
+
end
|
|
342
|
+
|
|
279
343
|
# Declares the expected input type(s) for this UseCase
|
|
280
344
|
# Accepts a Class or one or more Modules that input must include
|
|
281
345
|
#
|
|
@@ -359,13 +423,13 @@ module SenroUsecaser
|
|
|
359
423
|
private
|
|
360
424
|
|
|
361
425
|
def copy_configuration_to(subclass)
|
|
362
|
-
subclass
|
|
363
|
-
subclass.instance_variable_set(:@dependency_types, dependency_types.dup)
|
|
364
|
-
subclass.instance_variable_set(:@use_case_namespace, @use_case_namespace)
|
|
426
|
+
copy_depends_on_to(subclass)
|
|
365
427
|
subclass.instance_variable_set(:@organized_steps, @organized_steps&.dup)
|
|
366
428
|
subclass.instance_variable_set(:@on_failure_strategy, @on_failure_strategy)
|
|
367
429
|
subclass.instance_variable_set(:@input_types, @input_types&.dup)
|
|
368
430
|
subclass.instance_variable_set(:@output_schema, @output_schema)
|
|
431
|
+
subclass.instance_variable_set(:@retry_configurations, retry_configurations.dup)
|
|
432
|
+
subclass.instance_variable_set(:@discard_matchers, discard_matchers.dup)
|
|
369
433
|
end
|
|
370
434
|
|
|
371
435
|
def copy_hooks_to(subclass)
|
|
@@ -373,6 +437,9 @@ module SenroUsecaser
|
|
|
373
437
|
subclass.instance_variable_set(:@before_hooks, before_hooks.dup)
|
|
374
438
|
subclass.instance_variable_set(:@after_hooks, after_hooks.dup)
|
|
375
439
|
subclass.instance_variable_set(:@around_hooks, around_hooks.dup)
|
|
440
|
+
subclass.instance_variable_set(:@on_failure_hooks, on_failure_hooks.dup)
|
|
441
|
+
subclass.instance_variable_set(:@before_retry_hooks, before_retry_hooks.dup)
|
|
442
|
+
subclass.instance_variable_set(:@after_retries_exhausted_hooks, after_retries_exhausted_hooks.dup)
|
|
376
443
|
end
|
|
377
444
|
end
|
|
378
445
|
|
|
@@ -397,10 +464,7 @@ module SenroUsecaser
|
|
|
397
464
|
end
|
|
398
465
|
|
|
399
466
|
validate_input!(input)
|
|
400
|
-
|
|
401
|
-
execute_with_hooks(input) do
|
|
402
|
-
call(input)
|
|
403
|
-
end
|
|
467
|
+
execute_with_retry(input)
|
|
404
468
|
end
|
|
405
469
|
|
|
406
470
|
# Executes the UseCase logic
|
|
@@ -412,6 +476,9 @@ module SenroUsecaser
|
|
|
412
476
|
raise NotImplementedError, "#{self.class.name}#call must be implemented"
|
|
413
477
|
end
|
|
414
478
|
|
|
479
|
+
# Represents a record of a step execution in a pipeline
|
|
480
|
+
StepExecutionRecord = Struct.new(:step, :input, :result, keyword_init: true)
|
|
481
|
+
|
|
415
482
|
private
|
|
416
483
|
|
|
417
484
|
# Creates a success Result with the given value
|
|
@@ -496,6 +563,90 @@ module SenroUsecaser
|
|
|
496
563
|
result
|
|
497
564
|
end
|
|
498
565
|
|
|
566
|
+
# Executes the UseCase with retry support
|
|
567
|
+
#
|
|
568
|
+
#: (untyped) -> Result[untyped]
|
|
569
|
+
def execute_with_retry(input)
|
|
570
|
+
context = build_retry_context
|
|
571
|
+
current_input = input
|
|
572
|
+
|
|
573
|
+
loop do
|
|
574
|
+
result = execute_with_hooks(current_input) { call(current_input) }
|
|
575
|
+
|
|
576
|
+
return result if result.success?
|
|
577
|
+
return result if should_discard?(result)
|
|
578
|
+
|
|
579
|
+
retry_config = find_matching_retry_config(result)
|
|
580
|
+
run_on_failure_hooks(current_input, result, context)
|
|
581
|
+
|
|
582
|
+
should_retry = context.should_retry? || (retry_config && !context.exhausted?)
|
|
583
|
+
|
|
584
|
+
unless should_retry
|
|
585
|
+
run_after_retries_exhausted_hooks(current_input, result, context) if context.retried?
|
|
586
|
+
return result
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
wait_time = context.retry_wait || retry_config&.calculate_wait(context.attempt) || 0
|
|
590
|
+
run_before_retry_hooks(current_input, result, context)
|
|
591
|
+
|
|
592
|
+
sleep(wait_time) if wait_time.positive?
|
|
593
|
+
|
|
594
|
+
current_input = context.retry_input || current_input
|
|
595
|
+
context.increment!(last_error: result.errors.first)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Builds a retry context with max attempts from configurations
|
|
600
|
+
#
|
|
601
|
+
#: () -> RetryContext
|
|
602
|
+
def build_retry_context
|
|
603
|
+
max_attempts = self.class.retry_configurations.map(&:attempts).max
|
|
604
|
+
RetryContext.new(max_attempts: max_attempts)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Finds a retry configuration that matches the result
|
|
608
|
+
#
|
|
609
|
+
#: (Result[untyped]) -> RetryConfiguration?
|
|
610
|
+
def find_matching_retry_config(result)
|
|
611
|
+
self.class.retry_configurations.find { |c| c.matches?(result) }
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Checks if the result should be discarded (no retry)
|
|
615
|
+
#
|
|
616
|
+
#: (Result[untyped]) -> bool
|
|
617
|
+
def should_discard?(result)
|
|
618
|
+
return false unless result.failure?
|
|
619
|
+
|
|
620
|
+
result.errors.any? do |error|
|
|
621
|
+
self.class.discard_matchers.any? do |matcher|
|
|
622
|
+
case matcher
|
|
623
|
+
when Symbol
|
|
624
|
+
error.code == matcher
|
|
625
|
+
when Class
|
|
626
|
+
error.cause&.is_a?(matcher)
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Runs before_retry hooks
|
|
633
|
+
#
|
|
634
|
+
#: (untyped, Result[untyped], RetryContext) -> void
|
|
635
|
+
def run_before_retry_hooks(input, result, context)
|
|
636
|
+
self.class.before_retry_hooks.each do |hook|
|
|
637
|
+
instance_exec(input, result, context, &hook) # steep:ignore BlockTypeMismatch
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Runs after_retries_exhausted hooks
|
|
642
|
+
#
|
|
643
|
+
#: (untyped, Result[untyped], RetryContext) -> void
|
|
644
|
+
def run_after_retries_exhausted_hooks(input, result, context)
|
|
645
|
+
self.class.after_retries_exhausted_hooks.each do |hook|
|
|
646
|
+
instance_exec(input, result, context, &hook) # steep:ignore BlockTypeMismatch
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
499
650
|
# Wraps a non-Result value in Result.success
|
|
500
651
|
#
|
|
501
652
|
#: (untyped) -> Result[untyped]
|
|
@@ -576,6 +727,44 @@ module SenroUsecaser
|
|
|
576
727
|
self.class.after_hooks.each { |hook| instance_exec(input, result, &hook) } # steep:ignore BlockTypeMismatch
|
|
577
728
|
end
|
|
578
729
|
|
|
730
|
+
# Runs all on_failure hooks when result is a failure
|
|
731
|
+
#
|
|
732
|
+
#: (untyped, Result[untyped], ?RetryContext?) -> void
|
|
733
|
+
def run_on_failure_hooks(input, result, context = nil)
|
|
734
|
+
return unless result.failure?
|
|
735
|
+
|
|
736
|
+
hook_instances.each do |hook_instance|
|
|
737
|
+
call_on_failure_hook(hook_instance, :on_failure, input, result, context)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
self.class.extensions.each do |ext|
|
|
741
|
+
next if hook_class?(ext)
|
|
742
|
+
next unless ext.respond_to?(:on_failure)
|
|
743
|
+
|
|
744
|
+
call_on_failure_hook(ext, :on_failure, input, result, context)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
self.class.on_failure_hooks.each do |hook|
|
|
748
|
+
if context && (hook.arity == 3 || hook.arity.negative?)
|
|
749
|
+
instance_exec(input, result, context, &hook) # steep:ignore BlockTypeMismatch
|
|
750
|
+
else
|
|
751
|
+
instance_exec(input, result, &hook) # steep:ignore BlockTypeMismatch
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Calls an on_failure hook with appropriate arguments
|
|
757
|
+
#
|
|
758
|
+
#: (untyped, Symbol, untyped, Result[untyped], RetryContext?) -> void
|
|
759
|
+
def call_on_failure_hook(target, method_name, input, result, context)
|
|
760
|
+
method = target.method(method_name)
|
|
761
|
+
if context && (method.arity == 3 || method.arity.negative?)
|
|
762
|
+
target.send(method_name, input, result, context)
|
|
763
|
+
else
|
|
764
|
+
target.send(method_name, input, result)
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
579
768
|
# Returns instantiated hook objects
|
|
580
769
|
#
|
|
581
770
|
#: () -> Array[Hook]
|
|
@@ -609,43 +798,18 @@ module SenroUsecaser
|
|
|
609
798
|
end
|
|
610
799
|
|
|
611
800
|
# Resolves a single dependency from the container
|
|
801
|
+
# Overrides DependsOn::InstanceMethods to accept container as parameter
|
|
612
802
|
#
|
|
613
803
|
#: (Container, Symbol) -> untyped
|
|
614
804
|
def resolve_from_container(container, name)
|
|
615
|
-
|
|
616
|
-
if
|
|
617
|
-
container.resolve_in(
|
|
805
|
+
ns = effective_namespace
|
|
806
|
+
if ns
|
|
807
|
+
container.resolve_in(ns, name)
|
|
618
808
|
else
|
|
619
809
|
container.resolve(name)
|
|
620
810
|
end
|
|
621
811
|
end
|
|
622
812
|
|
|
623
|
-
# Returns the effective namespace for dependency resolution
|
|
624
|
-
#
|
|
625
|
-
#: () -> (Symbol | String)?
|
|
626
|
-
def effective_namespace
|
|
627
|
-
return self.class.use_case_namespace if self.class.use_case_namespace
|
|
628
|
-
return nil unless SenroUsecaser.configuration.infer_namespace_from_module
|
|
629
|
-
|
|
630
|
-
infer_namespace_from_class
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
# Infers namespace from the class's module structure
|
|
634
|
-
#
|
|
635
|
-
#: () -> String?
|
|
636
|
-
def infer_namespace_from_class
|
|
637
|
-
class_name = self.class.name
|
|
638
|
-
return nil unless class_name
|
|
639
|
-
|
|
640
|
-
parts = class_name.split("::")
|
|
641
|
-
return nil if parts.length <= 1
|
|
642
|
-
|
|
643
|
-
module_parts = parts[0...-1] || [] #: Array[String]
|
|
644
|
-
return nil if module_parts.empty?
|
|
645
|
-
|
|
646
|
-
module_parts.map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2').downcase }.join("::")
|
|
647
|
-
end
|
|
648
|
-
|
|
649
813
|
# Executes the organized UseCase pipeline
|
|
650
814
|
#
|
|
651
815
|
#: (untyped) -> Result[untyped]
|
|
@@ -668,12 +832,18 @@ module SenroUsecaser
|
|
|
668
832
|
def execute_pipeline_stop(input)
|
|
669
833
|
current_input = input
|
|
670
834
|
result = nil #: Result[untyped]?
|
|
835
|
+
executed_steps = [] #: Array[StepExecutionRecord]
|
|
671
836
|
|
|
672
837
|
self.class.organized_steps&.each do |step|
|
|
673
838
|
next unless step.should_execute?(current_input, self)
|
|
674
839
|
|
|
675
840
|
step_result = execute_step(step, current_input)
|
|
676
|
-
|
|
841
|
+
executed_steps << StepExecutionRecord.new(step: step, input: current_input, result: step_result)
|
|
842
|
+
|
|
843
|
+
if step_result.failure? && step_should_stop?(step)
|
|
844
|
+
execute_pipeline_rollback(executed_steps)
|
|
845
|
+
return step_result
|
|
846
|
+
end
|
|
677
847
|
|
|
678
848
|
current_input = step_result.value if step_result.success?
|
|
679
849
|
result = step_result
|
|
@@ -688,12 +858,18 @@ module SenroUsecaser
|
|
|
688
858
|
def execute_pipeline_continue(input)
|
|
689
859
|
current_input = input
|
|
690
860
|
result = nil #: Result[untyped]?
|
|
861
|
+
executed_steps = [] #: Array[StepExecutionRecord]
|
|
691
862
|
|
|
692
863
|
self.class.organized_steps&.each do |step|
|
|
693
864
|
next unless step.should_execute?(current_input, self)
|
|
694
865
|
|
|
695
866
|
step_result = execute_step(step, current_input)
|
|
696
|
-
|
|
867
|
+
executed_steps << StepExecutionRecord.new(step: step, input: current_input, result: step_result)
|
|
868
|
+
|
|
869
|
+
if step_result.failure? && step.on_failure == :stop
|
|
870
|
+
execute_pipeline_rollback(executed_steps)
|
|
871
|
+
return step_result
|
|
872
|
+
end
|
|
697
873
|
|
|
698
874
|
current_input = step_result.value if step_result.success?
|
|
699
875
|
result = step_result
|
|
@@ -707,16 +883,20 @@ module SenroUsecaser
|
|
|
707
883
|
#: (untyped) -> Result[untyped]
|
|
708
884
|
def execute_pipeline_collect(input)
|
|
709
885
|
errors = [] #: Array[Error]
|
|
886
|
+
executed_steps = [] #: Array[StepExecutionRecord]
|
|
710
887
|
state = { input: input, errors: errors, last_success: nil }
|
|
711
888
|
|
|
712
889
|
self.class.organized_steps&.each do |step|
|
|
713
890
|
next unless step.should_execute?(state[:input], self)
|
|
714
891
|
|
|
715
892
|
result = execute_step(step, state[:input])
|
|
893
|
+
executed_steps << StepExecutionRecord.new(step: step, input: state[:input], result: result)
|
|
716
894
|
break if should_stop_collect_pipeline?(result, step, state)
|
|
717
895
|
end
|
|
718
896
|
|
|
719
|
-
build_collect_result(state)
|
|
897
|
+
final_result = build_collect_result(state)
|
|
898
|
+
execute_pipeline_rollback(executed_steps) if final_result.failure?
|
|
899
|
+
final_result
|
|
720
900
|
end
|
|
721
901
|
|
|
722
902
|
# Updates collect state and checks if pipeline should stop
|
|
@@ -762,6 +942,7 @@ module SenroUsecaser
|
|
|
762
942
|
|
|
763
943
|
# Calls a single UseCase in the pipeline
|
|
764
944
|
# Requires input type(s) to be defined for pipeline steps
|
|
945
|
+
# Note: on_failure hooks are not called here - they're called in pipeline rollback
|
|
765
946
|
#
|
|
766
947
|
#: (singleton(Base), untyped) -> Result[untyped]
|
|
767
948
|
def call_use_case(use_case_class, input)
|
|
@@ -769,8 +950,59 @@ module SenroUsecaser
|
|
|
769
950
|
raise ArgumentError, "#{use_case_class.name} must define `input` type(s) to be used in a pipeline"
|
|
770
951
|
end
|
|
771
952
|
|
|
772
|
-
|
|
773
|
-
|
|
953
|
+
instance = use_case_class.new(container: @_container)
|
|
954
|
+
instance.send(:perform_as_pipeline_step, input, capture_exceptions: @_capture_exceptions || false)
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
# Performs the UseCase as a pipeline step (without on_failure hooks)
|
|
958
|
+
# on_failure hooks are handled by the pipeline's rollback mechanism instead
|
|
959
|
+
#
|
|
960
|
+
#: (untyped, ?capture_exceptions: bool) -> Result[untyped]
|
|
961
|
+
def perform_as_pipeline_step(input, capture_exceptions: false)
|
|
962
|
+
@_capture_exceptions = capture_exceptions
|
|
963
|
+
|
|
964
|
+
unless self.class.input_class || self.class.organized_steps
|
|
965
|
+
raise ArgumentError, "#{self.class.name} must define `input` class"
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
validate_input!(input)
|
|
969
|
+
execute_with_hooks(input) { call(input) }
|
|
970
|
+
rescue StandardError => e
|
|
971
|
+
raise unless capture_exceptions
|
|
972
|
+
|
|
973
|
+
Result.from_exception(e)
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
# Executes rollback by calling on_failure hooks on executed steps in reverse order
|
|
977
|
+
# Unlike run_on_failure_hooks, this method calls hooks regardless of result status
|
|
978
|
+
# because we want to rollback even successfully completed steps when pipeline fails
|
|
979
|
+
#
|
|
980
|
+
#: (Array[StepExecutionRecord]) -> void
|
|
981
|
+
def execute_pipeline_rollback(executed_steps)
|
|
982
|
+
executed_steps.reverse_each do |record|
|
|
983
|
+
step_instance = record.step.use_case_class.new(container: @_container)
|
|
984
|
+
step_instance.send(:run_rollback_hooks, record.input, record.result)
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
# Runs on_failure hooks for rollback purposes (regardless of result status)
|
|
989
|
+
#
|
|
990
|
+
#: (untyped, Result[untyped]) -> void
|
|
991
|
+
def run_rollback_hooks(input, result)
|
|
992
|
+
hook_instances.each do |hook_instance|
|
|
993
|
+
call_on_failure_hook(hook_instance, :on_failure, input, result, nil)
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
self.class.extensions.each do |ext|
|
|
997
|
+
next if hook_class?(ext)
|
|
998
|
+
next unless ext.respond_to?(:on_failure)
|
|
999
|
+
|
|
1000
|
+
call_on_failure_hook(ext, :on_failure, input, result, nil)
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
self.class.on_failure_hooks.each do |hook|
|
|
1004
|
+
instance_exec(input, result, &hook) # steep:ignore BlockTypeMismatch
|
|
1005
|
+
end
|
|
774
1006
|
end
|
|
775
1007
|
end
|
|
776
1008
|
end
|