Module: Teckel::Chain

Defined in:
lib/teckel/chain.rb

Overview

Railway style execution of multiple Operations.

  • Runs multiple Operations (steps) in order.

  • The output of an earlier step is passed as input to the next step.

  • Any failure will stop the execution chain (none of the later steps is called).

  • All Operations (steps) must behave like Teckel::Operation::Results and return a result object like Result

  • A failure response is wrapped into a StepFailure giving additional information about which step failed

Examples:

Defining a simple Chain with three steps

class CreateUser
  include ::Teckel::Operation::Results

  input  Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
  output Types.Instance(User)
  error  Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))

  def call(input)
    user = User.new(name: input[:name], age: input[:age])
    if user.save
      success!(user)
    else
      fail!(message: "Could not save User", errors: user.errors)
    end
  end
end

class LogUser
  include ::Teckel::Operation::Results

  input Types.Instance(User)
  output input

  def call(usr)
    Logger.new(File::NULL).info("User #{usr.name} created")
    usr # we need to return the correct output type
  end
end

class AddFriend
  class << self
    # Don't actually do this! It's not safe and for generating the failure sample only.
    attr_accessor :fail_befriend
  end

  include ::Teckel::Operation::Results

  input Types.Instance(User)
  output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
  error  Types::Hash.schema(message: Types::String)

  def call(user)
    if self.class.fail_befriend
      fail!(message: "Did not find a friend.")
    else
      { user: user, friend: User.new(name: "A friend", age: 42) }
    end
  end
end

class MyChain
  include Teckel::Chain

  step :create, CreateUser
  step :log, LogUser
  step :befriend, AddFriend
end

result = MyChain.call(name: "Bob", age: 23)
result.is_a?(Teckel::Result)          #=> true
result.success[:user].is_a?(User)    #=> true
result.success[:friend].is_a?(User)  #=> true

AddFriend.fail_befriend = true
failure_result = MyChain.call(name: "Bob", age: 23)
failure_result.is_a?(Teckel::Chain::StepFailure) #=> true

# additional step information
failure_result.step_name                        #=> :befriend
failure_result.step                             #=> AddFriend

# otherwise behaves just like a normal +Result+
failure_result.failure?                         #=> true
failure_result.failure                          #=> {message: "Did not find a friend."}

DB transaction around hook

class CreateUser
  include ::Teckel::Operation::Results

  input  Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer.optional)
  output Types.Instance(User)
  error  Types::Hash.schema(message: Types::String, errors: Types::Array.of(Types::Hash))

  def call(input)
    user = User.new(name: input[:name], age: input[:age])
    if user.save
      success!(user)
    else
      fail!(message: "Could not safe User", errors: user.errors)
    end
  end
end

class AddFriend
  class << self
    # Don't actually do this! It's not safe and for generating the failure sample only.
    attr_accessor :fail_befriend
  end

  include ::Teckel::Operation::Results

  input Types.Instance(User)
  output Types::Hash.schema(user: Types.Instance(User), friend: Types.Instance(User))
  error  Types::Hash.schema(message: Types::String)

  def call(user)
    if self.class.fail_befriend
      fail!(message: "Did not find a friend.")
    else
      { user: user, friend: User.new(name: "A friend", age: 42) }
    end
  end
end

LOG = []

class MyChain
  include Teckel::Chain

  around ->(chain, input) {
    result = nil
    begin
      LOG << :before

      FakeDB.transaction do
        result = chain.call(input)
        raise FakeDB::Rollback if result.failure?
      end

      LOG << :after
      result
    rescue FakeDB::Rollback
      LOG << :rollback
      result
    end
  }

  step :create, CreateUser
  step :befriend, AddFriend
end

AddFriend.fail_befriend = true
failure_result = MyChain.call(name: "Bob", age: 23)
failure_result.is_a?(Teckel::Chain::StepFailure) #=> true

# triggered DB rollback
LOG                                              #=> [:before, :rollback]

# additional step information
failure_result.step_name                         #=> :befriend
failure_result.step                              #=> AddFriend

# otherwise behaves just like a normal +Result+
failure_result.failure?                          #=> true
failure_result.failure                           #=> {message: "Did not find a friend."}

See Also:

Defined Under Namespace

Modules: ClassMethods Classes: Runner, StepFailure

Class Method Summary collapse

Class Method Details

.included(receiver) ⇒ Object



382
383
384
385
386
387
388
389
# File 'lib/teckel/chain.rb', line 382

def self.included(receiver)
  receiver.extend ClassMethods

  receiver.class_eval do
    @steps = []
    @config = Config.new
  end
end