module Cuprum::Steps

The Steps supports step by step processes that halt on a failed step.

After including Cuprum::Steps, use the steps instance method to wrap a series of instructions. Each instruction is then defined using the step method. Steps can be defined either as a block or as a method invocation.

When the steps block is evaluated, each step is called in sequence. If the step resolves to a passing result, the result value is returned and execution continues to the next step. If all of the steps pass, then the result of the final step is returned from the steps block.

Conversely, if any step resolves to a failing result, that failing result is immediately returned from the steps block. No further steps will be called.

For example, consider updating a database record using a primary key and an attributes hash. Broken down into its basics, this requires the following instructions:

Note that each of these steps can fail for different reasons. For example, if a record with the given primary key does not exist in the database, then the first instruction will fail, and the follow up steps should not be executed. Further, whatever context is executing these steps probably wants to know which step failed, and why.

@example Defining Methods As Steps

def assign_attributes(record, attributes); end

def find_record(primary_key); end

def save_record(record); end

def update_record(primary_key, attributes)
  steps do
    record = step :find_record,       primary_key
    record = step :assign_attributes, record, attributes
    step :save_record, record
  end
end

@example Defining Blocks As Steps

class AssignAttributes < Cuprum::Command; end

class FindRecord < Cuprum::Command; end

class SaveRecord < Cuprum::Command; end

def update_record(primary_key, attributes)
  steps do
    record = step { FindRecord.new.call(primary_key) }
    record = step { AssignAttributes.new.call(record, attributes) }
    step { SaveRecord.new.call(record) }
  end
end

Constants

UNDEFINED

Public Class Methods

execute_method(receiver, method_name, *args, **kwargs, &block) click to toggle source

@!visibility private

# File lib/cuprum/steps.rb, line 71
def execute_method(receiver, method_name, *args, **kwargs, &block)
  if block_given? && kwargs.empty?
    receiver.send(method_name, *args, &block)
  elsif block_given?
    receiver.send(method_name, *args, **kwargs, &block)
  elsif kwargs.empty?
    receiver.send(method_name, *args)
  else
    receiver.send(method_name, *args, **kwargs)
  end
end
extract_result_value(result) click to toggle source

@!visibility private

# File lib/cuprum/steps.rb, line 84
def extract_result_value(result)
  return result unless result.respond_to?(:to_cuprum_result)

  result = result.to_cuprum_result

  return result.value if result.success?

  throw :cuprum_failed_step, result
end
validate_method_name(method_name) click to toggle source

rubocop:disable Metrics/MethodLength @!visibility private

# File lib/cuprum/steps.rb, line 96
def validate_method_name(method_name)
  if method_name.nil?
    raise ArgumentError,
      'expected a block or a method name',
      caller(1..-1)
  end

  unless method_name.is_a?(String) || method_name.is_a?(Symbol)
    raise ArgumentError,
      'expected method name to be a String or Symbol',
      caller(1..-1)
  end

  return unless method_name.empty?

  raise ArgumentError, "method name can't be blank", caller(1..-1)
end

Public Instance Methods

step(method_name = UNDEFINED, *args, **kwargs, &block) click to toggle source

Executes the block and returns the value, or halts on a failure.

@yield Called with no parameters.

@return [Object] the value of the result, or the returned object.

The step method is used to evaluate a sequence of processes, and to fail fast and halt processing if any of the steps returns a failing result. Each invocation of step should be wrapped in a steps block, or used inside the process method of a Command.

If the object returned by the block is a Cuprum result or compatible object (such as a called operation), the value is converted to a Cuprum result via the to_cuprum_result method. Otherwise, the object is returned directly from step.

If the returned object is a passing result, the value of the result is returned by step.

If the returned object is a failing result, then step will throw :cuprum_failed_result and the failing result. This is caught by the steps block, and halts execution of any subsequent steps.

@example Calling a Step

# The #do_something method returns the string 'some value'.
step { do_something() } #=> 'some value'

value = step { do_something() }
value #=> 'some value'

@example Calling a Step with a Passing Result

# The #do_something_else method returns a Cuprum result with a value
# of 'another value'.
step { do_something_else() } #=> 'another value'

# The result is passing, so the value is extracted and returned.
value = step { do_something_else() }
value #=> 'another value'

@example Calling a Step with a Failing Result

# The #do_something_wrong method returns a failing Cuprum result.
step { do_something_wrong() } # Throws the :cuprum_failed_step symbol.
# File lib/cuprum/steps.rb, line 158
def step(method_name = UNDEFINED, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength
  result =
    if method_name != UNDEFINED || !args.empty? || !kwargs.empty?
      SleepingKingStudios::Tools::CoreTools.deprecate(
        "#{self.class}#step(method_name)",
        message: 'Use the block form: step { method_name(*args, **kwargs) }'
      )

      Cuprum::Steps.validate_method_name(method_name)

      Cuprum::Steps
        .execute_method(self, method_name, *args, **kwargs, &block)
    elsif !block_given?
      raise ArgumentError, 'expected a block'
    else
      block.call
    end

  Cuprum::Steps.extract_result_value(result)
end
steps(&block) click to toggle source

Returns the first failing step result, or the final result if none fail.

The steps method is used to wrap a series of step calls. Each step is executed in sequence. If any of the steps returns a failing result, that result is immediately returned from steps. Otherwise, steps wraps the value returned by a block in a Cuprum result.

@yield Called with no parameters.

@yieldreturn A Cuprum result, or an object to be wrapped in a result.

@return [Cuprum::Result] the result or object returned by the block,

wrapped in a Cuprum result.

@example With A Passing Step

result = steps do
  step { success('some value') }
end
result.class    #=> Cuprum::Result
result.success? #=> true
result.value    #=> 'some value'

@example With A Failing Step

result = steps do
  step { failure('something went wrong') }
end
result.class    #=> Cuprum::Result
result.success? #=> false
result.error    #=> 'something went wrong'

@example With Multiple Steps

result = steps do
  # This step is passing, so execution continues on to the next step.
  step { success('first step') }

  # This step is failing, so execution halts and returns this result.
  step { failure('second step') }

  # This step will never be called.
  step { success('third step') }
end
result.class    #=> Cuprum::Result
result.success? #=> false
result.error    #=> 'second step'
# File lib/cuprum/steps.rb, line 223
def steps(&block)
  raise ArgumentError, 'no block given' unless block_given?

  result = catch(:cuprum_failed_step) { block.call }

  return result if result.respond_to?(:to_cuprum_result)

  success(result)
end