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
|
@@ -89,30 +89,14 @@ module SenroUsecaser
|
|
|
89
89
|
# organize StepA, StepB
|
|
90
90
|
# end
|
|
91
91
|
class Base
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
# : (Symbol, ?Class) -> void
|
|
95
|
-
def self.depends_on: (Symbol, ?Class) -> void
|
|
92
|
+
extend DependsOn
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
# : () -> Array[Symbol]
|
|
100
|
-
def self.dependencies: () -> Array[Symbol]
|
|
94
|
+
include DependsOn::InstanceMethods
|
|
101
95
|
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# : () -> Hash[Symbol, Class]
|
|
105
|
-
def self.dependency_types: () -> Hash[Symbol, Class]
|
|
106
|
-
|
|
107
|
-
# Sets the namespace for dependency resolution
|
|
108
|
-
#
|
|
109
|
-
# : ((Symbol | String)) -> void
|
|
110
|
-
def self.namespace: (Symbol | String) -> void
|
|
111
|
-
|
|
112
|
-
# Returns the declared namespace
|
|
96
|
+
# Alias for backward compatibility
|
|
113
97
|
#
|
|
114
98
|
# : () -> (Symbol | String)?
|
|
115
|
-
|
|
99
|
+
alias self.use_case_namespace self.declared_namespace
|
|
116
100
|
|
|
117
101
|
# Declares a sequence of UseCases to execute as a pipeline
|
|
118
102
|
#
|
|
@@ -188,6 +172,73 @@ module SenroUsecaser
|
|
|
188
172
|
# : () -> Array[Proc]
|
|
189
173
|
def self.around_hooks: () -> Array[Proc]
|
|
190
174
|
|
|
175
|
+
# Adds an on_failure hook
|
|
176
|
+
#
|
|
177
|
+
# : () { (untyped, Result[untyped], ?RetryContext?) -> void } -> void
|
|
178
|
+
def self.on_failure: () { (untyped, Result[untyped], ?RetryContext?) -> void } -> void
|
|
179
|
+
|
|
180
|
+
# Returns the list of on_failure hooks
|
|
181
|
+
#
|
|
182
|
+
# : () -> Array[Proc]
|
|
183
|
+
def self.on_failure_hooks: () -> Array[Proc]
|
|
184
|
+
|
|
185
|
+
# Configures automatic retry for specific error types
|
|
186
|
+
#
|
|
187
|
+
# @example Retry on network errors
|
|
188
|
+
# retry_on :network_error, attempts: 3, wait: 1
|
|
189
|
+
#
|
|
190
|
+
# @example Retry on exception class
|
|
191
|
+
# retry_on Net::OpenTimeout, attempts: 5, wait: 2, backoff: :exponential
|
|
192
|
+
#
|
|
193
|
+
# @example Multiple error types with jitter
|
|
194
|
+
# retry_on :rate_limited, :timeout, attempts: 3, wait: 1, jitter: 0.1
|
|
195
|
+
#
|
|
196
|
+
# rubocop:disable Metrics/ParameterLists
|
|
197
|
+
# : (*(Symbol | Class), ?attempts: Integer, ?wait: (Float | Integer),
|
|
198
|
+
# : ?backoff: Symbol, ?max_wait: (Float | Integer)?, ?jitter: (Float | Integer)) -> void
|
|
199
|
+
def self.retry_on: (*untyped error_matchers, ?attempts: untyped, ?wait: untyped, ?backoff: untyped, ?max_wait: untyped, ?jitter: untyped) -> untyped
|
|
200
|
+
|
|
201
|
+
# Returns the list of retry configurations
|
|
202
|
+
#
|
|
203
|
+
# : () -> Array[RetryConfiguration]
|
|
204
|
+
def self.retry_configurations: () -> Array[RetryConfiguration]
|
|
205
|
+
|
|
206
|
+
# Configures errors that should immediately discard (no retry)
|
|
207
|
+
#
|
|
208
|
+
# @example Discard on validation errors
|
|
209
|
+
# discard_on :validation_error, :not_found
|
|
210
|
+
#
|
|
211
|
+
# @example Discard on exception class
|
|
212
|
+
# discard_on ArgumentError
|
|
213
|
+
#
|
|
214
|
+
# : (*(Symbol | Class)) -> void
|
|
215
|
+
def self.discard_on: (*Symbol | Class) -> void
|
|
216
|
+
|
|
217
|
+
# Returns the list of discard matchers
|
|
218
|
+
#
|
|
219
|
+
# : () -> Array[(Symbol | Class)]
|
|
220
|
+
def self.discard_matchers: () -> Array[Symbol | Class]
|
|
221
|
+
|
|
222
|
+
# Adds a before_retry hook
|
|
223
|
+
#
|
|
224
|
+
# : () { (untyped, Result[untyped], RetryContext) -> void } -> void
|
|
225
|
+
def self.before_retry: () { (untyped, Result[untyped], RetryContext) -> void } -> void
|
|
226
|
+
|
|
227
|
+
# Returns the list of before_retry hooks
|
|
228
|
+
#
|
|
229
|
+
# : () -> Array[Proc]
|
|
230
|
+
def self.before_retry_hooks: () -> Array[Proc]
|
|
231
|
+
|
|
232
|
+
# Adds an after_retries_exhausted hook
|
|
233
|
+
#
|
|
234
|
+
# : () { (untyped, Result[untyped], RetryContext) -> void } -> void
|
|
235
|
+
def self.after_retries_exhausted: () { (untyped, Result[untyped], RetryContext) -> void } -> void
|
|
236
|
+
|
|
237
|
+
# Returns the list of after_retries_exhausted hooks
|
|
238
|
+
#
|
|
239
|
+
# : () -> Array[Proc]
|
|
240
|
+
def self.after_retries_exhausted_hooks: () -> Array[Proc]
|
|
241
|
+
|
|
191
242
|
# Declares the expected input type(s) for this UseCase
|
|
192
243
|
# Accepts a Class or one or more Modules that input must include
|
|
193
244
|
#
|
|
@@ -261,6 +312,18 @@ module SenroUsecaser
|
|
|
261
312
|
# : (?untyped input) -> Result[untyped]
|
|
262
313
|
def call: (?untyped input) -> Result[untyped]
|
|
263
314
|
|
|
315
|
+
# Represents a record of a step execution in a pipeline
|
|
316
|
+
class StepExecutionRecord < Struct[untyped]
|
|
317
|
+
attr_accessor step(): untyped
|
|
318
|
+
|
|
319
|
+
attr_accessor input(): untyped
|
|
320
|
+
|
|
321
|
+
attr_accessor result(): untyped
|
|
322
|
+
|
|
323
|
+
def self.new: (?step: untyped, ?input: untyped, ?result: untyped) -> instance
|
|
324
|
+
| ({ ?step: untyped, ?input: untyped, ?result: untyped }) -> instance
|
|
325
|
+
end
|
|
326
|
+
|
|
264
327
|
private
|
|
265
328
|
|
|
266
329
|
# Creates a success Result with the given value
|
|
@@ -301,6 +364,36 @@ module SenroUsecaser
|
|
|
301
364
|
# : (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
302
365
|
def execute_with_hooks: (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
303
366
|
|
|
367
|
+
# Executes the UseCase with retry support
|
|
368
|
+
#
|
|
369
|
+
# : (untyped) -> Result[untyped]
|
|
370
|
+
def execute_with_retry: (untyped) -> Result[untyped]
|
|
371
|
+
|
|
372
|
+
# Builds a retry context with max attempts from configurations
|
|
373
|
+
#
|
|
374
|
+
# : () -> RetryContext
|
|
375
|
+
def build_retry_context: () -> RetryContext
|
|
376
|
+
|
|
377
|
+
# Finds a retry configuration that matches the result
|
|
378
|
+
#
|
|
379
|
+
# : (Result[untyped]) -> RetryConfiguration?
|
|
380
|
+
def find_matching_retry_config: (Result[untyped]) -> RetryConfiguration?
|
|
381
|
+
|
|
382
|
+
# Checks if the result should be discarded (no retry)
|
|
383
|
+
#
|
|
384
|
+
# : (Result[untyped]) -> bool
|
|
385
|
+
def should_discard?: (Result[untyped]) -> bool
|
|
386
|
+
|
|
387
|
+
# Runs before_retry hooks
|
|
388
|
+
#
|
|
389
|
+
# : (untyped, Result[untyped], RetryContext) -> void
|
|
390
|
+
def run_before_retry_hooks: (untyped, Result[untyped], RetryContext) -> void
|
|
391
|
+
|
|
392
|
+
# Runs after_retries_exhausted hooks
|
|
393
|
+
#
|
|
394
|
+
# : (untyped, Result[untyped], RetryContext) -> void
|
|
395
|
+
def run_after_retries_exhausted_hooks: (untyped, Result[untyped], RetryContext) -> void
|
|
396
|
+
|
|
304
397
|
# Wraps a non-Result value in Result.success
|
|
305
398
|
#
|
|
306
399
|
# : (untyped) -> Result[untyped]
|
|
@@ -336,6 +429,16 @@ module SenroUsecaser
|
|
|
336
429
|
# : (untyped, Result[untyped]) -> void
|
|
337
430
|
def run_after_hooks: (untyped, Result[untyped]) -> void
|
|
338
431
|
|
|
432
|
+
# Runs all on_failure hooks when result is a failure
|
|
433
|
+
#
|
|
434
|
+
# : (untyped, Result[untyped], ?RetryContext?) -> void
|
|
435
|
+
def run_on_failure_hooks: (untyped, Result[untyped], ?RetryContext?) -> void
|
|
436
|
+
|
|
437
|
+
# Calls an on_failure hook with appropriate arguments
|
|
438
|
+
#
|
|
439
|
+
# : (untyped, Symbol, untyped, Result[untyped], RetryContext?) -> void
|
|
440
|
+
def call_on_failure_hook: (untyped, Symbol, untyped, Result[untyped], RetryContext?) -> void
|
|
441
|
+
|
|
339
442
|
# Returns instantiated hook objects
|
|
340
443
|
#
|
|
341
444
|
# : () -> Array[Hook]
|
|
@@ -352,20 +455,11 @@ module SenroUsecaser
|
|
|
352
455
|
def resolve_dependencies: (Container, Hash[Symbol, untyped]) -> void
|
|
353
456
|
|
|
354
457
|
# Resolves a single dependency from the container
|
|
458
|
+
# Overrides DependsOn::InstanceMethods to accept container as parameter
|
|
355
459
|
#
|
|
356
460
|
# : (Container, Symbol) -> untyped
|
|
357
461
|
def resolve_from_container: (Container, Symbol) -> untyped
|
|
358
462
|
|
|
359
|
-
# Returns the effective namespace for dependency resolution
|
|
360
|
-
#
|
|
361
|
-
# : () -> (Symbol | String)?
|
|
362
|
-
def effective_namespace: () -> (Symbol | String)?
|
|
363
|
-
|
|
364
|
-
# Infers namespace from the class's module structure
|
|
365
|
-
#
|
|
366
|
-
# : () -> String?
|
|
367
|
-
def infer_namespace_from_class: () -> String?
|
|
368
|
-
|
|
369
463
|
# Executes the organized UseCase pipeline
|
|
370
464
|
#
|
|
371
465
|
# : (untyped) -> Result[untyped]
|
|
@@ -408,8 +502,27 @@ module SenroUsecaser
|
|
|
408
502
|
|
|
409
503
|
# Calls a single UseCase in the pipeline
|
|
410
504
|
# Requires input type(s) to be defined for pipeline steps
|
|
505
|
+
# Note: on_failure hooks are not called here - they're called in pipeline rollback
|
|
411
506
|
#
|
|
412
507
|
# : (singleton(Base), untyped) -> Result[untyped]
|
|
413
508
|
def call_use_case: (singleton(Base), untyped) -> Result[untyped]
|
|
509
|
+
|
|
510
|
+
# Performs the UseCase as a pipeline step (without on_failure hooks)
|
|
511
|
+
# on_failure hooks are handled by the pipeline's rollback mechanism instead
|
|
512
|
+
#
|
|
513
|
+
# : (untyped, ?capture_exceptions: bool) -> Result[untyped]
|
|
514
|
+
def perform_as_pipeline_step: (untyped, ?capture_exceptions: bool) -> Result[untyped]
|
|
515
|
+
|
|
516
|
+
# Executes rollback by calling on_failure hooks on executed steps in reverse order
|
|
517
|
+
# Unlike run_on_failure_hooks, this method calls hooks regardless of result status
|
|
518
|
+
# because we want to rollback even successfully completed steps when pipeline fails
|
|
519
|
+
#
|
|
520
|
+
# : (Array[StepExecutionRecord]) -> void
|
|
521
|
+
def execute_pipeline_rollback: (Array[StepExecutionRecord]) -> void
|
|
522
|
+
|
|
523
|
+
# Runs on_failure hooks for rollback purposes (regardless of result status)
|
|
524
|
+
#
|
|
525
|
+
# : (untyped, Result[untyped]) -> void
|
|
526
|
+
def run_rollback_hooks: (untyped, Result[untyped]) -> void
|
|
414
527
|
end
|
|
415
528
|
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Generated from lib/senro_usecaser/depends_on.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SenroUsecaser
|
|
4
|
+
# Module that provides dependency injection support
|
|
5
|
+
#
|
|
6
|
+
# This module can be extended into any class to enable the full DI functionality
|
|
7
|
+
# similar to UseCase and Hook classes, including:
|
|
8
|
+
# - `depends_on` for declaring dependencies
|
|
9
|
+
# - `namespace` for scoped dependency resolution
|
|
10
|
+
# - Automatic `infer_namespace_from_module` support
|
|
11
|
+
# - Default `initialize` that sets up dependency injection (uses SenroUsecaser.container if not provided)
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage (no initialize needed)
|
|
14
|
+
# class MyService
|
|
15
|
+
# extend SenroUsecaser::DependsOn
|
|
16
|
+
#
|
|
17
|
+
# depends_on :logger, Logger
|
|
18
|
+
# depends_on :repository
|
|
19
|
+
#
|
|
20
|
+
# # No initialize needed! Default is provided automatically.
|
|
21
|
+
#
|
|
22
|
+
# def perform
|
|
23
|
+
# logger.info("Performing...")
|
|
24
|
+
# repository.find(1)
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# service = MyService.new # Uses SenroUsecaser.container
|
|
29
|
+
# service.logger # => Logger instance
|
|
30
|
+
#
|
|
31
|
+
# @example Custom initialize with super
|
|
32
|
+
# class MyService
|
|
33
|
+
# extend SenroUsecaser::DependsOn
|
|
34
|
+
#
|
|
35
|
+
# depends_on :logger
|
|
36
|
+
# attr_reader :extra
|
|
37
|
+
#
|
|
38
|
+
# def initialize(extra:, container: nil)
|
|
39
|
+
# super(container: container) # Handles dependency resolution
|
|
40
|
+
# @extra = extra
|
|
41
|
+
# end
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# @example With explicit namespace
|
|
45
|
+
# class Admin::UserService
|
|
46
|
+
# extend SenroUsecaser::DependsOn
|
|
47
|
+
#
|
|
48
|
+
# namespace :admin
|
|
49
|
+
# depends_on :user_repository, UserRepository
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# @example With infer_namespace_from_module (when configured)
|
|
53
|
+
# # When SenroUsecaser.configuration.infer_namespace_from_module = true
|
|
54
|
+
# class Admin::Orders::ProcessService
|
|
55
|
+
# extend SenroUsecaser::DependsOn
|
|
56
|
+
#
|
|
57
|
+
# depends_on :order_repository # resolved from "admin::orders" namespace
|
|
58
|
+
# end
|
|
59
|
+
module DependsOn
|
|
60
|
+
# Hook called when module is extended into a class
|
|
61
|
+
# Automatically includes InstanceMethods
|
|
62
|
+
def self.extended: (untyped base) -> untyped
|
|
63
|
+
|
|
64
|
+
# Declares a dependency to be injected from the container
|
|
65
|
+
#
|
|
66
|
+
# @param name [Symbol] The name of the dependency
|
|
67
|
+
# @param type [Class, nil] Optional expected type for the dependency
|
|
68
|
+
#
|
|
69
|
+
# @example Basic dependency
|
|
70
|
+
# depends_on :logger
|
|
71
|
+
#
|
|
72
|
+
# @example Typed dependency
|
|
73
|
+
# depends_on :repository, UserRepository
|
|
74
|
+
#
|
|
75
|
+
# : (Symbol, ?Class) -> void
|
|
76
|
+
def depends_on: (Symbol, ?Class) -> void
|
|
77
|
+
|
|
78
|
+
# Returns the list of declared dependencies
|
|
79
|
+
#
|
|
80
|
+
# : () -> Array[Symbol]
|
|
81
|
+
def dependencies: () -> Array[Symbol]
|
|
82
|
+
|
|
83
|
+
# Returns the dependency type mapping
|
|
84
|
+
#
|
|
85
|
+
# : () -> Hash[Symbol, Class]
|
|
86
|
+
def dependency_types: () -> Hash[Symbol, Class]
|
|
87
|
+
|
|
88
|
+
# Sets or returns the namespace for dependency resolution
|
|
89
|
+
#
|
|
90
|
+
# @param name [Symbol, String, nil] The namespace to set
|
|
91
|
+
# @return [Symbol, String, nil] The current namespace when no argument given
|
|
92
|
+
#
|
|
93
|
+
# @example Setting namespace
|
|
94
|
+
# namespace :admin
|
|
95
|
+
#
|
|
96
|
+
# @example Getting namespace
|
|
97
|
+
# current_ns = namespace
|
|
98
|
+
#
|
|
99
|
+
# : (?(Symbol | String)) -> (Symbol | String)?
|
|
100
|
+
def namespace: (?Symbol | String) -> (Symbol | String)?
|
|
101
|
+
|
|
102
|
+
# Returns the declared namespace
|
|
103
|
+
#
|
|
104
|
+
# : () -> (Symbol | String)?
|
|
105
|
+
def declared_namespace: () -> (Symbol | String)?
|
|
106
|
+
|
|
107
|
+
# Copies dependency configuration to a subclass
|
|
108
|
+
#
|
|
109
|
+
# @param subclass [Class] The subclass to copy dependencies to
|
|
110
|
+
#
|
|
111
|
+
# @example In inherited hook
|
|
112
|
+
# def self.inherited(subclass)
|
|
113
|
+
# super
|
|
114
|
+
# copy_depends_on_to(subclass)
|
|
115
|
+
# end
|
|
116
|
+
#
|
|
117
|
+
# : (Class) -> void
|
|
118
|
+
def copy_depends_on_to: (Class) -> void
|
|
119
|
+
|
|
120
|
+
# Instance methods for dependency resolution
|
|
121
|
+
#
|
|
122
|
+
# These methods are automatically included when DependsOn is extended.
|
|
123
|
+
# They require @_container and @_dependencies instance variables to be set.
|
|
124
|
+
module InstanceMethods
|
|
125
|
+
# Default initialize for classes using DependsOn
|
|
126
|
+
#
|
|
127
|
+
# This provides a default initialize that sets up dependency injection.
|
|
128
|
+
# Classes can override this and call super to extend the behavior.
|
|
129
|
+
#
|
|
130
|
+
# @param container [Container, nil] The DI container to resolve dependencies from.
|
|
131
|
+
# If nil, uses SenroUsecaser.container.
|
|
132
|
+
#
|
|
133
|
+
# @example Default usage (no arguments needed)
|
|
134
|
+
# class MyService
|
|
135
|
+
# extend SenroUsecaser::DependsOn
|
|
136
|
+
# depends_on :logger
|
|
137
|
+
# end
|
|
138
|
+
# service = MyService.new # Uses SenroUsecaser.container
|
|
139
|
+
#
|
|
140
|
+
# @example With explicit container
|
|
141
|
+
# service = MyService.new(container: custom_container)
|
|
142
|
+
#
|
|
143
|
+
# @example Custom initialize with super
|
|
144
|
+
# class MyService
|
|
145
|
+
# extend SenroUsecaser::DependsOn
|
|
146
|
+
# depends_on :logger
|
|
147
|
+
# attr_reader :extra
|
|
148
|
+
#
|
|
149
|
+
# def initialize(extra:, container: nil)
|
|
150
|
+
# super(container: container)
|
|
151
|
+
# @extra = extra
|
|
152
|
+
# end
|
|
153
|
+
# end
|
|
154
|
+
#
|
|
155
|
+
# : (?container: Container?) -> void
|
|
156
|
+
def initialize: (?container: Container?) -> void
|
|
157
|
+
|
|
158
|
+
# Resolves all declared dependencies from the container
|
|
159
|
+
#
|
|
160
|
+
# Call this in your initialize method after setting @_container and @_dependencies.
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# def initialize(container:)
|
|
164
|
+
# @_container = container
|
|
165
|
+
# @_dependencies = {}
|
|
166
|
+
# resolve_dependencies
|
|
167
|
+
# end
|
|
168
|
+
#
|
|
169
|
+
# : () -> void
|
|
170
|
+
def resolve_dependencies: () -> void
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Returns the effective namespace for dependency resolution
|
|
175
|
+
#
|
|
176
|
+
# Priority:
|
|
177
|
+
# 1. Explicitly declared namespace via `namespace :name`
|
|
178
|
+
# 2. Inferred namespace from module structure (if configured)
|
|
179
|
+
#
|
|
180
|
+
# : () -> (Symbol | String)?
|
|
181
|
+
def effective_namespace: () -> (Symbol | String)?
|
|
182
|
+
|
|
183
|
+
# Infers namespace from the class's module structure
|
|
184
|
+
#
|
|
185
|
+
# Converts CamelCase module names to snake_case and joins with "::"
|
|
186
|
+
# e.g., Admin::Orders::ProcessService -> "admin::orders"
|
|
187
|
+
#
|
|
188
|
+
# : () -> String?
|
|
189
|
+
def infer_namespace_from_class: () -> String?
|
|
190
|
+
|
|
191
|
+
# Resolves a single dependency from the container
|
|
192
|
+
#
|
|
193
|
+
# : (Symbol) -> untyped
|
|
194
|
+
def resolve_from_container: (Symbol) -> untyped
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -36,30 +36,14 @@ module SenroUsecaser
|
|
|
36
36
|
# end
|
|
37
37
|
# end
|
|
38
38
|
class Hook
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
# : (Symbol, ?Class) -> void
|
|
42
|
-
def self.depends_on: (Symbol, ?Class) -> void
|
|
43
|
-
|
|
44
|
-
# Returns the list of declared dependencies
|
|
45
|
-
#
|
|
46
|
-
# : () -> Array[Symbol]
|
|
47
|
-
def self.dependencies: () -> Array[Symbol]
|
|
48
|
-
|
|
49
|
-
# Returns the dependency type mapping
|
|
50
|
-
#
|
|
51
|
-
# : () -> Hash[Symbol, Class]
|
|
52
|
-
def self.dependency_types: () -> Hash[Symbol, Class]
|
|
39
|
+
extend DependsOn
|
|
53
40
|
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
# : (?(Symbol | String)) -> (Symbol | String)?
|
|
57
|
-
def self.namespace: (?Symbol | String) -> (Symbol | String)?
|
|
41
|
+
include DependsOn::InstanceMethods
|
|
58
42
|
|
|
59
|
-
# Alias for
|
|
43
|
+
# Alias for backward compatibility
|
|
60
44
|
#
|
|
61
45
|
# : () -> (Symbol | String)?
|
|
62
|
-
|
|
46
|
+
alias self.hook_namespace self.declared_namespace
|
|
63
47
|
|
|
64
48
|
# @api private
|
|
65
49
|
def self.inherited: (untyped subclass) -> untyped
|
|
@@ -87,26 +71,30 @@ module SenroUsecaser
|
|
|
87
71
|
# : (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
88
72
|
def around: (untyped) { () -> Result[untyped] } -> Result[untyped]
|
|
89
73
|
|
|
74
|
+
# Called when the UseCase fails
|
|
75
|
+
# Override in subclass to add failure handling or rollback logic
|
|
76
|
+
#
|
|
77
|
+
# @example Basic logging
|
|
78
|
+
# def on_failure(input, result)
|
|
79
|
+
# logger.error("Failed: #{result.errors.first&.message}")
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# @example Request retry
|
|
83
|
+
# def on_failure(input, result, context)
|
|
84
|
+
# if result.errors.first&.code == :network_error && context.attempt < 3
|
|
85
|
+
# context.retry!(wait: 2.0)
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
#
|
|
89
|
+
# : (untyped, Result[untyped], ?RetryContext?) -> void
|
|
90
|
+
def on_failure: (untyped, Result[untyped], ?RetryContext?) -> void
|
|
91
|
+
|
|
90
92
|
private
|
|
91
93
|
|
|
92
94
|
# Returns the effective namespace for dependency resolution
|
|
95
|
+
# Overrides DependsOn::InstanceMethods to add use_case_namespace fallback
|
|
93
96
|
#
|
|
94
97
|
# : () -> (Symbol | String)?
|
|
95
98
|
def effective_namespace: () -> (Symbol | String)?
|
|
96
|
-
|
|
97
|
-
# Infers namespace from the class's module structure
|
|
98
|
-
#
|
|
99
|
-
# : () -> String?
|
|
100
|
-
def infer_namespace_from_class: () -> String?
|
|
101
|
-
|
|
102
|
-
# Resolves dependencies from the container
|
|
103
|
-
#
|
|
104
|
-
# : () -> void
|
|
105
|
-
def resolve_dependencies: () -> void
|
|
106
|
-
|
|
107
|
-
# Resolves a single dependency from the container
|
|
108
|
-
#
|
|
109
|
-
# : (Symbol) -> untyped
|
|
110
|
-
def resolve_from_container: (Symbol) -> untyped
|
|
111
99
|
end
|
|
112
100
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Generated from lib/senro_usecaser/retry_configuration.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SenroUsecaser
|
|
4
|
+
# Configuration for automatic retry behavior
|
|
5
|
+
#
|
|
6
|
+
# This class defines when and how retries should occur based on
|
|
7
|
+
# error codes or exception classes, with configurable backoff strategies.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic retry configuration
|
|
10
|
+
# RetryConfiguration.new(
|
|
11
|
+
# matchers: [:network_error, Net::OpenTimeout],
|
|
12
|
+
# attempts: 3,
|
|
13
|
+
# wait: 1.0
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example With exponential backoff
|
|
17
|
+
# RetryConfiguration.new(
|
|
18
|
+
# matchers: [:rate_limited],
|
|
19
|
+
# attempts: 5,
|
|
20
|
+
# wait: 2.0,
|
|
21
|
+
# backoff: :exponential,
|
|
22
|
+
# max_wait: 60
|
|
23
|
+
# )
|
|
24
|
+
class RetryConfiguration
|
|
25
|
+
# Returns the list of error matchers (Symbols for error codes, Classes for exceptions)
|
|
26
|
+
# : () -> Array[(Symbol | Class)]
|
|
27
|
+
attr_reader matchers: untyped
|
|
28
|
+
|
|
29
|
+
# Returns the maximum number of attempts
|
|
30
|
+
# : () -> Integer
|
|
31
|
+
attr_reader attempts: untyped
|
|
32
|
+
|
|
33
|
+
# Returns the base wait time in seconds
|
|
34
|
+
# : () -> (Float | Integer)
|
|
35
|
+
attr_reader wait: untyped
|
|
36
|
+
|
|
37
|
+
# Returns the backoff strategy (:fixed, :linear, :exponential)
|
|
38
|
+
# : () -> Symbol
|
|
39
|
+
attr_reader backoff: untyped
|
|
40
|
+
|
|
41
|
+
# Returns the maximum wait time in seconds
|
|
42
|
+
# : () -> (Float | Integer)
|
|
43
|
+
attr_reader max_wait: untyped
|
|
44
|
+
|
|
45
|
+
# Returns the jitter factor (0.0 to 1.0)
|
|
46
|
+
# : () -> (Float | Integer)
|
|
47
|
+
attr_reader jitter: untyped
|
|
48
|
+
|
|
49
|
+
# Initializes a new retry configuration
|
|
50
|
+
#
|
|
51
|
+
# @param matchers [Array<Symbol, Class>] Error codes or exception classes to match
|
|
52
|
+
# @param attempts [Integer] Maximum number of attempts (default: 3)
|
|
53
|
+
# @param wait [Numeric] Base wait time in seconds (default: 0)
|
|
54
|
+
# @param backoff [Symbol] Backoff strategy: :fixed, :linear, or :exponential (default: :fixed)
|
|
55
|
+
# @param max_wait [Numeric] Maximum wait time in seconds (default: 3600)
|
|
56
|
+
# @param jitter [Numeric] Jitter factor 0.0-1.0 to randomize wait times (default: 0)
|
|
57
|
+
#
|
|
58
|
+
# rubocop:disable Metrics/ParameterLists
|
|
59
|
+
# : (matchers: Array[(Symbol | Class)], ?attempts: Integer, ?wait: (Float | Integer),
|
|
60
|
+
# : ?backoff: Symbol, ?max_wait: (Float | Integer)?, ?jitter: (Float | Integer)) -> void
|
|
61
|
+
def initialize: (matchers: untyped, ?attempts: untyped, ?wait: untyped, ?backoff: untyped, ?max_wait: untyped, ?jitter: untyped) -> untyped
|
|
62
|
+
|
|
63
|
+
# Checks if this configuration matches the given result
|
|
64
|
+
#
|
|
65
|
+
# : (Result[untyped]) -> bool
|
|
66
|
+
def matches?: (Result[untyped]) -> bool
|
|
67
|
+
|
|
68
|
+
# Calculates the wait time for the given attempt number
|
|
69
|
+
#
|
|
70
|
+
# : (Integer) -> Float
|
|
71
|
+
def calculate_wait: (Integer) -> Float
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Checks if an error matches any of the configured matchers
|
|
76
|
+
#
|
|
77
|
+
# : (Error) -> bool
|
|
78
|
+
def matches_error?: (Error) -> bool
|
|
79
|
+
|
|
80
|
+
# Calculates the base wait time based on backoff strategy
|
|
81
|
+
#
|
|
82
|
+
# : (Integer) -> (Float | Integer)
|
|
83
|
+
def calculate_base_wait: (Integer) -> (Float | Integer)
|
|
84
|
+
|
|
85
|
+
# Applies jitter to the wait time
|
|
86
|
+
#
|
|
87
|
+
# : ((Float | Integer)) -> Float
|
|
88
|
+
def apply_jitter: (Float | Integer) -> Float
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Generated from lib/senro_usecaser/retry_context.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SenroUsecaser
|
|
4
|
+
# Represents the context of a retry operation
|
|
5
|
+
#
|
|
6
|
+
# This class tracks the state of retry attempts including:
|
|
7
|
+
# - Current attempt number
|
|
8
|
+
# - Maximum attempts allowed
|
|
9
|
+
# - Elapsed time since first attempt
|
|
10
|
+
# - Whether a retry should occur
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage in on_failure hook
|
|
13
|
+
# on_failure do |input, result, context|
|
|
14
|
+
# if context.attempt < 3
|
|
15
|
+
# context.retry!(wait: 1.0)
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example With modified input
|
|
20
|
+
# on_failure do |input, result, context|
|
|
21
|
+
# if result.errors.first&.code == :rate_limited
|
|
22
|
+
# context.retry!(input: input.with_reduced_batch_size, wait: 5.0)
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
class RetryContext
|
|
26
|
+
# Returns the current attempt number (1-indexed)
|
|
27
|
+
# : () -> Integer
|
|
28
|
+
attr_reader attempt: untyped
|
|
29
|
+
|
|
30
|
+
# Returns the maximum number of attempts allowed
|
|
31
|
+
# : () -> Integer?
|
|
32
|
+
attr_reader max_attempts: untyped
|
|
33
|
+
|
|
34
|
+
# Returns the time when the first attempt started
|
|
35
|
+
# : () -> Time
|
|
36
|
+
attr_reader started_at: untyped
|
|
37
|
+
|
|
38
|
+
# Returns the error from the last failed attempt
|
|
39
|
+
# : () -> Error?
|
|
40
|
+
attr_reader last_error: untyped
|
|
41
|
+
|
|
42
|
+
# Returns the input to use for the retry (nil means use original)
|
|
43
|
+
# : () -> untyped
|
|
44
|
+
attr_reader retry_input: untyped
|
|
45
|
+
|
|
46
|
+
# Returns the wait time before retrying
|
|
47
|
+
# : () -> (Float | Integer)?
|
|
48
|
+
attr_reader retry_wait: untyped
|
|
49
|
+
|
|
50
|
+
# Initializes a new retry context
|
|
51
|
+
#
|
|
52
|
+
# : (?max_attempts: Integer?) -> void
|
|
53
|
+
def initialize: (?max_attempts: Integer?) -> void
|
|
54
|
+
|
|
55
|
+
# Returns true if this is a retry (attempt > 1)
|
|
56
|
+
#
|
|
57
|
+
# : () -> bool
|
|
58
|
+
def retried?: () -> bool
|
|
59
|
+
|
|
60
|
+
# Returns the elapsed time since the first attempt
|
|
61
|
+
#
|
|
62
|
+
# : () -> Float
|
|
63
|
+
def elapsed_time: () -> Float
|
|
64
|
+
|
|
65
|
+
# Returns true if max_attempts has been reached
|
|
66
|
+
#
|
|
67
|
+
# : () -> bool
|
|
68
|
+
def exhausted?: () -> bool
|
|
69
|
+
|
|
70
|
+
# Returns true if a retry has been requested
|
|
71
|
+
#
|
|
72
|
+
# : () -> bool
|
|
73
|
+
def should_retry?: () -> bool
|
|
74
|
+
|
|
75
|
+
# Requests a retry with optional modified input and wait time
|
|
76
|
+
#
|
|
77
|
+
# @example Retry with default settings
|
|
78
|
+
# context.retry!
|
|
79
|
+
#
|
|
80
|
+
# @example Retry with wait time
|
|
81
|
+
# context.retry!(wait: 2.0)
|
|
82
|
+
#
|
|
83
|
+
# @example Retry with modified input
|
|
84
|
+
# context.retry!(input: modified_input, wait: 1.0)
|
|
85
|
+
#
|
|
86
|
+
# : (?input: untyped, ?wait: (Float | Integer)?) -> void
|
|
87
|
+
def retry!: (?input: untyped, ?wait: (Float | Integer)?) -> void
|
|
88
|
+
|
|
89
|
+
# Increments the attempt counter and resets retry state
|
|
90
|
+
# Called internally between retry attempts
|
|
91
|
+
#
|
|
92
|
+
# : (?last_error: Error?) -> void
|
|
93
|
+
def increment!: (?last_error: Error?) -> void
|
|
94
|
+
|
|
95
|
+
# Resets the retry request state
|
|
96
|
+
# Called internally after processing retry decision
|
|
97
|
+
#
|
|
98
|
+
# : () -> void
|
|
99
|
+
def reset_retry_state!: () -> void
|
|
100
|
+
end
|
|
101
|
+
end
|
data/sig/overrides.rbs
CHANGED
|
@@ -10,7 +10,6 @@ module SenroUsecaser
|
|
|
10
10
|
class Base
|
|
11
11
|
# Class methods that rbs-inline doesn't generate correctly
|
|
12
12
|
def self.organized_steps: () -> Array[Step]?
|
|
13
|
-
def self.use_case_namespace: () -> (Symbol | String)?
|
|
14
13
|
def self.output_schema: () -> (Class | Hash[Symbol, Class])?
|
|
15
14
|
end
|
|
16
15
|
end
|