Class: BetterService::Workflowable::Step

Inherits:
Object
  • Object
show all
Defined in:
lib/better_service/concerns/workflowable/step.rb

Overview

Step - Represents a single step in a workflow pipeline

Each step wraps a service class and defines how data flows into it, whether it’s optional, conditional execution, and rollback behavior.

Example:

step = Step.new(
  name: :create_order,
  service_class: Order::CreateService,
  input: ->(ctx) { { items: ctx.cart_items } },
  optional: false,
  condition: ->(ctx) { ctx.cart_items.any? },
  rollback: ->(ctx) { ctx.order.destroy! }
)

step.call(context, user, params)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, service_class:, input: nil, optional: false, condition: nil, rollback: nil) ⇒ Step

Returns a new instance of Step.



24
25
26
27
28
29
30
31
# File 'lib/better_service/concerns/workflowable/step.rb', line 24

def initialize(name:, service_class:, input: nil, optional: false, condition: nil, rollback: nil)
  @name = name
  @service_class = service_class
  @input_mapper = input
  @optional = optional
  @condition = condition
  @rollback_block = rollback
end

Instance Attribute Details

#conditionObject (readonly)

Returns the value of attribute condition.



22
23
24
# File 'lib/better_service/concerns/workflowable/step.rb', line 22

def condition
  @condition
end

#input_mapperObject (readonly)

Returns the value of attribute input_mapper.



22
23
24
# File 'lib/better_service/concerns/workflowable/step.rb', line 22

def input_mapper
  @input_mapper
end

#nameObject (readonly)

Returns the value of attribute name.



22
23
24
# File 'lib/better_service/concerns/workflowable/step.rb', line 22

def name
  @name
end

#optionalObject (readonly)

Returns the value of attribute optional.



22
23
24
# File 'lib/better_service/concerns/workflowable/step.rb', line 22

def optional
  @optional
end

#rollback_blockObject (readonly)

Returns the value of attribute rollback_block.



22
23
24
# File 'lib/better_service/concerns/workflowable/step.rb', line 22

def rollback_block
  @rollback_block
end

#service_classObject (readonly)

Returns the value of attribute service_class.



22
23
24
# File 'lib/better_service/concerns/workflowable/step.rb', line 22

def service_class
  @service_class
end

Instance Method Details

#call(context, user, base_params = {}) ⇒ Hash

Execute the step

Parameters:

  • context (Context)

    The workflow context

  • user (Object)

    The current user

  • params (Hash)

    Base params for the workflow

Returns:

  • (Hash)

    Service result (normalized to hash format for workflow compatibility)



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/better_service/concerns/workflowable/step.rb', line 39

def call(context, user, base_params = {})
  # Check if step should be skipped due to condition
  if should_skip?(context)
    return {
      success: true,
      skipped: true,
      message: "Step #{name} skipped due to condition"
    }
  end

  # Build input params for the service
  service_params = build_params(context, base_params)

  # Call the service - returns [object, metadata] tuple
  service_result = service_class.new(user, params: service_params).call

  # Normalize result to hash format (services now return [object, metadata] tuple)
  result = normalize_service_result(service_result)

  # Store result in context if successful
  if result[:success]
    store_result_in_context(context, result)
  elsif optional
    # If step is optional and failed, continue but log the failure
    context.add(:"#{name}_error", result[:errors] || result[:validation_errors])
    return {
      success: true,
      optional_failure: true,
      message: "Optional step #{name} failed but continuing",
      errors: result[:errors] || result[:validation_errors]
    }
  end

  result
rescue StandardError => e
  # If step is optional, swallow the error and continue
  if optional
    context.add(:"#{name}_error", e.message)
    {
      success: true,
      optional_failure: true,
      message: "Optional step #{name} raised error but continuing",
      error: e.message
    }
  else
    raise e
  end
end

#rollback(context) ⇒ Object

Execute rollback for this step

Parameters:

  • context (Context)

    The workflow context

Raises:

  • (StandardError)

    If rollback fails (caught and wrapped by workflow)



92
93
94
95
96
97
98
99
100
101
# File 'lib/better_service/concerns/workflowable/step.rb', line 92

def rollback(context)
  return unless rollback_block

  if rollback_block.is_a?(Proc)
    context.instance_exec(context, &rollback_block)
  else
    rollback_block.call(context)
  end
  # Note: Exceptions are propagated to workflow which wraps them in RollbackError
end