Class: SenroUsecaser::Base

Inherits:
Object
  • Object
show all
Extended by:
DependsOn
Defined in:
lib/senro_usecaser/base.rb

Overview

Base class for all UseCases

Examples:

Basic UseCase with keyword arguments

class CreateUserUseCase < SenroUsecaser::Base
  def call(name:, email:)
    user = User.create(name: name, email: email)
    success(user)
  end
end

result = CreateUserUseCase.call(name: "Taro", email: "[email protected]")

With input/output classes (recommended for pipelines)

class CreateUserUseCase < SenroUsecaser::Base
  input CreateUserInput
  output CreateUserOutput

  def call(input)
    user = User.create(name: input.name, email: input.email)
    success(CreateUserOutput.new(user: user))
  end
end

Pipeline with input/output chaining

class StepA < SenroUsecaser::Base
  input AInput
  output AOutput
  def call(input)
    success(AOutput.new(value: input.value * 2))
  end
end

class StepB < SenroUsecaser::Base
  input AOutput  # Receives StepA's output directly
  output BOutput
  def call(input)
    success(BOutput.new(result: input.value + 1))
  end
end

class Pipeline < SenroUsecaser::Base
  organize StepA, StepB
end

Defined Under Namespace

Classes: StepExecutionRecord

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DependsOn

copy_depends_on_to, declared_namespace, dependencies, dependency_types, depends_on, extended, namespace

Constructor Details

#initialize(container: nil, dependencies: {}) ⇒ Base

Initializes the UseCase with dependencies resolved from the container

: (?container: Container?, ?dependencies: Hash[Symbol, untyped]) -> void



449
450
451
452
453
454
# File 'lib/senro_usecaser/base.rb', line 449

def initialize(container: nil, dependencies: {})
  @_container = container || SenroUsecaser.container
  @_dependencies = {} #: Hash[Symbol, untyped]

  resolve_dependencies(@_container, dependencies)
end

Class Attribute Details

.organized_steps ⇒ Object (readonly)

Returns the list of organized steps

: () -> Array?



182
183
184
# File 'lib/senro_usecaser/base.rb', line 182

def organized_steps
  @organized_steps
end

.output_schema ⇒ Object (readonly)

Returns the output schema

: () -> (Class | Hash[Symbol, Class])?



389
390
391
# File 'lib/senro_usecaser/base.rb', line 389

def output_schema
  @output_schema
end

Class Method Details

.after(&block) ⇒ Object

Adds an after hook

: () { (untyped, Result) -> void } -> void



222
223
224
# File 'lib/senro_usecaser/base.rb', line 222

def after(&block)
  after_hooks << block
end

.after_hooks ⇒ Object

Returns the list of after hooks

: () -> Array



229
230
231
# File 'lib/senro_usecaser/base.rb', line 229

def after_hooks
  @after_hooks ||= []
end

.after_retries_exhausted(&block) ⇒ Object

Adds an after_retries_exhausted hook

: () { (untyped, Result, RetryContext) -> void } -> void



332
333
334
# File 'lib/senro_usecaser/base.rb', line 332

def after_retries_exhausted(&block)
  after_retries_exhausted_hooks << block
end

.after_retries_exhausted_hooks ⇒ Object

Returns the list of after_retries_exhausted hooks

: () -> Array



339
340
341
# File 'lib/senro_usecaser/base.rb', line 339

def after_retries_exhausted_hooks
  @after_retries_exhausted_hooks ||= []
end

.around(&block) ⇒ Object

Adds an around hook Block receives (input, use_case, &block) where use_case allows access to dependencies

: () { (untyped, Base) { () -> Result } -> Result } -> void



237
238
239
# File 'lib/senro_usecaser/base.rb', line 237

def around(&block)
  around_hooks << block if block
end

.around_hooks ⇒ Object

Returns the list of around hooks

: () -> Array



244
245
246
# File 'lib/senro_usecaser/base.rb', line 244

def around_hooks
  @around_hooks ||= []
end

.before(&block) ⇒ Object

Adds a before hook

: () { (untyped) -> void } -> void



208
209
210
# File 'lib/senro_usecaser/base.rb', line 208

def before(&block)
  before_hooks << block
end

.before_hooks ⇒ Object

Returns the list of before hooks

: () -> Array



215
216
217
# File 'lib/senro_usecaser/base.rb', line 215

def before_hooks
  @before_hooks ||= []
end

.before_retry(&block) ⇒ Object

Adds a before_retry hook

: () { (untyped, Result, RetryContext) -> void } -> void



318
319
320
# File 'lib/senro_usecaser/base.rb', line 318

def before_retry(&block)
  before_retry_hooks << block
end

.before_retry_hooks ⇒ Object

Returns the list of before_retry hooks

: () -> Array



325
326
327
# File 'lib/senro_usecaser/base.rb', line 325

def before_retry_hooks
  @before_retry_hooks ||= []
end

.call(input = nil, container: nil, **args) ⇒ Object

Calls the UseCase with the given input

: [T] (?untyped, ?container: Container, **untyped) -> Result



394
395
396
# File 'lib/senro_usecaser/base.rb', line 394

def call(input = nil, container: nil, **args)
  new(container: container).perform(input, capture_exceptions: false, **args)
end

.call!(input = nil, container: nil, **args) ⇒ Object

Calls the UseCase and captures any exceptions as failures

: [T] (?untyped, ?container: Container, **untyped) -> Result



401
402
403
404
405
# File 'lib/senro_usecaser/base.rb', line 401

def call!(input = nil, container: nil, **args)
  new(container: container).perform(input, capture_exceptions: true, **args)
rescue StandardError => e
  Result.from_exception(e)
end

.call_with_capture(input:, container: nil, exception_classes: [StandardError], code: :exception) ⇒ Object

Calls the UseCase with custom exception handling options

: [T] (input: untyped, ?container: Container, ?exception_classes: Array, ?code: Symbol) -> Result



410
411
412
413
414
# File 'lib/senro_usecaser/base.rb', line 410

def call_with_capture(input:, container: nil, exception_classes: [StandardError], code: :exception)
  new(container: container).perform(input)
rescue *exception_classes => e
  Result.from_exception(e, code: code)
end

.discard_matchers ⇒ Object

Returns the list of discard matchers

: () -> Array[(Symbol | Class)]



311
312
313
# File 'lib/senro_usecaser/base.rb', line 311

def discard_matchers
  @discard_matchers ||= []
end

.discard_on(*error_matchers) ⇒ Object

Configures errors that should immediately discard (no retry)

: (*(Symbol | Class)) -> void

Examples:

Discard on validation errors

discard_on :validation_error, :not_found

Discard on exception class

discard_on ArgumentError


304
305
306
# File 'lib/senro_usecaser/base.rb', line 304

def discard_on(*error_matchers)
  discard_matchers.concat(error_matchers.flatten)
end

.extend_with(*extensions) ⇒ Object

Adds extension modules with hooks

: (*Module) -> void



194
195
196
# File 'lib/senro_usecaser/base.rb', line 194

def extend_with(*extensions)
  extensions.each { |ext| self.extensions << ext }
end

.extensions ⇒ Object

Returns the list of extensions

: () -> Array



201
202
203
# File 'lib/senro_usecaser/base.rb', line 201

def extensions
  @extensions ||= []
end

.inherited(subclass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



417
418
419
420
421
# File 'lib/senro_usecaser/base.rb', line 417

def inherited(subclass)
  super
  copy_configuration_to(subclass)
  copy_hooks_to(subclass)
end

.input(*types) ⇒ Object

Declares the expected input type(s) for this UseCase Accepts a Class or one or more Modules that input must include

: (*Module) -> void

Examples:

Single class

input UserInput

Single module (interface)

input HasUserId

Multiple modules (interfaces)

input HasUserId, HasEmail


356
357
358
# File 'lib/senro_usecaser/base.rb', line 356

def input(*types)
  @input_types = types
end

.input_class ⇒ Object

Returns the input class (for backwards compatibility) If a Class is specified, returns it. Otherwise returns the first type.

: () -> Module?



371
372
373
374
375
376
377
# File 'lib/senro_usecaser/base.rb', line 371

def input_class
  types = input_types
  return nil if types.empty?

  # Class があればそれを返す(単一 Class 指定の後方互換)
  types.find { |t| t.is_a?(Class) } || types.first
end

.input_types ⇒ Object

Returns the input types as an array

: () -> Array



363
364
365
# File 'lib/senro_usecaser/base.rb', line 363

def input_types
  @input_types || []
end

.on_failure(&block) ⇒ Object

Adds an on_failure hook

: () { (untyped, Result, ?RetryContext?) -> void } -> void



251
252
253
# File 'lib/senro_usecaser/base.rb', line 251

def on_failure(&block)
  on_failure_hooks << block
end

.on_failure_hooks ⇒ Object

Returns the list of on_failure hooks

: () -> Array



258
259
260
# File 'lib/senro_usecaser/base.rb', line 258

def on_failure_hooks
  @on_failure_hooks ||= []
end

.on_failure_strategy ⇒ Object

Returns the failure handling strategy

: () -> Symbol



187
188
189
# File 'lib/senro_usecaser/base.rb', line 187

def on_failure_strategy
  @on_failure_strategy || :stop
end

.organize(*use_case_classes, on_failure: :stop, &block) ⇒ Object

Declares a sequence of UseCases to execute as a pipeline

: (*Class, ?on_failure: Symbol) ?{ () -> void } -> void

Examples:

Basic organize

organize StepA, StepB, StepC

With block and step

organize do
  step StepA
  step StepB, if: :should_run?
  step StepC, on_failure: :continue
end


145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/senro_usecaser/base.rb', line 145

def organize(*use_case_classes, on_failure: :stop, &block)
  @on_failure_strategy = on_failure

  if block
    @organized_steps = [] #: Array[Step]
    @_defining_steps = true
    instance_eval(&block) # steep:ignore BlockTypeMismatch
    @_defining_steps = false
  else
    @organized_steps = use_case_classes.map { |klass| Step.new(klass) }
  end
end

.output(type_or_schema) ⇒ Object

Declares the expected output type for this UseCase

: ((Class | Hash[Symbol, Class])) -> void



382
383
384
# File 'lib/senro_usecaser/base.rb', line 382

def output(type_or_schema)
  @output_schema = type_or_schema
end

.retry_configurations ⇒ Object

Returns the list of retry configurations

: () -> Array



291
292
293
# File 'lib/senro_usecaser/base.rb', line 291

def retry_configurations
  @retry_configurations ||= []
end

.retry_on(*error_matchers, attempts: 3, wait: 0, backoff: :fixed, max_wait: nil, jitter: 0) ⇒ Object

Configures automatic retry for specific error types

rubocop:disable Metrics/ParameterLists : (*(Symbol | Class), ?attempts: Integer, ?wait: (Float | Integer), : ?backoff: Symbol, ?max_wait: (Float | Integer)?, ?jitter: (Float | Integer)) -> void

Examples:

Retry on network errors

retry_on :network_error, attempts: 3, wait: 1

Retry on exception class

retry_on Net::OpenTimeout, attempts: 5, wait: 2, backoff: :exponential

Multiple error types with jitter

retry_on :rate_limited, :timeout, attempts: 3, wait: 1, jitter: 0.1


276
277
278
279
280
281
282
283
284
285
# File 'lib/senro_usecaser/base.rb', line 276

def retry_on(*error_matchers, attempts: 3, wait: 0, backoff: :fixed, max_wait: nil, jitter: 0)
  retry_configurations << RetryConfiguration.new(
    matchers: error_matchers.flatten,
    attempts: attempts,
    wait: wait,
    backoff: backoff,
    max_wait: max_wait,
    jitter: jitter
  )
end

.step(use_case_class, if: nil, unless: nil, on_failure: nil, all: nil, any: nil, input: nil) ⇒ Object

Defines a step in the organize block

rubocop:disable Metrics/ParameterLists : (Class, ?if: (Symbol | Proc)?, ?unless: (Symbol | Proc)?, ?on_failure: Symbol?, : ?all: Array[(Symbol | Proc)]?, ?any: Array[(Symbol | Proc)]?, : ?input: (Symbol | Proc)?) -> void



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/senro_usecaser/base.rb', line 164

def step(use_case_class, if: nil, unless: nil, on_failure: nil, all: nil, any: nil, input: nil)
  raise "step can only be called inside organize block" unless @_defining_steps

  @organized_steps << Step.new(
    use_case_class,
    if_condition: binding.local_variable_get(:if),
    unless_condition: binding.local_variable_get(:unless),
    on_failure: on_failure,
    all_conditions: all,
    any_conditions: any,
    input_mapping: input
  )
end

Instance Method Details

#call(input = nil) ⇒ Object

Executes the UseCase logic

: (?untyped input) -> Result

Raises:

  • (NotImplementedError)


473
474
475
476
477
# File 'lib/senro_usecaser/base.rb', line 473

def call(input = nil)
  return execute_pipeline(input) if self.class.organized_steps

  raise NotImplementedError, "#{self.class.name}#call must be implemented"
end

#perform(input, capture_exceptions: false) ⇒ Object

Performs the UseCase with hooks

: (untyped, ?capture_exceptions: bool) -> Result



459
460
461
462
463
464
465
466
467
468
# File 'lib/senro_usecaser/base.rb', line 459

def perform(input, capture_exceptions: false)
  @_capture_exceptions = capture_exceptions

  unless self.class.input_class || self.class.organized_steps
    raise ArgumentError, "#{self.class.name} must define `input` class"
  end

  validate_input!(input)
  execute_with_retry(input)
end