Tzu

Usage

Tzu commands must include Tzu and implement a #call method.

class MyCommand
  include Tzu

  def call(params)
    "My Command Response - #{params.message}"
  end
end

Tzu exposes #run at the class level, and returns an Outcome object. The Outcome's result will be the return value of the command's #call method.

outcome = MyCommand.run(message: 'Hello!')
#=> #<Tzu::Outcome @success=false, @result='My Command Response - Hello!'>

outcome.success? #=> true
outcome.failure? #=> false
outcome.result #=> 'My Command Response - Hello!'

Validation

Tzu also provides an invalid! method that allows you to elegantly escape execution.

class MyCommand
  include Tzu

  def call(params)
    invalid!('You did not do it') unless params[:message] == 'I did it!'
    "My Command Response - #{params[:message]}"
  end
end

When invoking Tzu with #run, invalid! will return an invalid Outcome.

outcome = MyCommand.run(message: 'Hello!')
outcome.success? #=> false
outcome.failure? #=> true
outcome.type #=> :validation
outcome.result #=> { errors: 'You did not do it' }

When invoking Tzu with #run!, invalid! will throw a Tzu::Invalid error.

outcome = MyCommand.run!(message: 'Hello!') #=> Tzu::Invalid: 'You did not do it'

If you use invalid! while catching an exception, you can pass the exception as an argument. The exception's #message value will be passed along to the outcome.

class MyRescueCommand
  include Tzu

  def call(params)
    raise StandardError.new('You did not do it')
  rescue StandardError => e
    invalid!(e)
  end
end
outcome = MyRescueCommand.run!(params_that_cause_error)
#=> Tzu::Invalid: 'You did not do it'

Note that if you pass a string to invalid!, it will coerce the result into a hash of the form:

{ errors: 'Error String' }

Any other type will simply be passed through.

Passing Blocks

You can also pass a block to Tzu commands.

Successful commands will execute the success block, and invalid commands will execute the invalid block. This is particularly useful in controllers:

MyCommand.run(message: params[:message]) do
  success do |result|
    render(json: {message: result}.to_json, status: 200)
  end

  invalid do |errors|
    render(json: errors.to_json, status: 422)
  end
end

Hooks

Tzu commands accept before, after, and around hooks. All hooks are executed in the order they are declared.

class MyCommand
  include Tzu

  around do |command|
    puts 'Begin Around 1'
    command.call
    puts 'End Around 1'
  end

  around do |command|
    puts 'Begin Around 2'
    command.call
    puts 'End Around 2'
  end

  before { puts 'Before 1' }
  before { puts 'Before 2' }

  after { puts 'After 1' }
  after { puts 'After 2' }

  def call(params)
    puts "My Command Response - #{params[:message]}"
  end
end

MyCommand.run(message: 'Hello!')

#=> Begin Around 1
#=> Begin Around 2
#=> Before 1
#=> Before 2
#=> My Command Response - Hello!
#=> After 1
#=> After 2
#=> End Around 2
#=> End Around 1

Request objects

You can define a request object for your command using the #request_object method.

class MyValidatedCommand
  include Tzu, Tzu::Validation

  request_object MyRequestObject

  def call(request)
    "Name: #{request.name}, Age: #{request.age}"
  end
end

Request objects must implement an initializer that accepts the command's parameters.

If you wish to validate your parameters, the Request object must implement #valid? and #errors.

class MySimpleRequestObject
  def initialize(params)
    @params = params
  end

  def valid?
    # Validate Parameters
  end

  def errors
    # Why aren't I valid?
  end
end

A very useful combination for request objects is Virtus.model and ActiveModel::Validations.

ActiveModel::Validations exposes all of the validators used on Rails models. Virtus.model validates the types of your inputs, and also makes them available via dot notation.

class MyRequestObject
  include Virtus.model
  include ActiveModel::Validations
  validates :name, :age, presence: :true

  attribute :name, String
  attribute :age, Integer
end

If your request object is invalid, Tzu will return an invalid outcome before reaching the #call method. The invalid Outcome's result is populated by the request object's #errors method.

class MyValidatedCommand
  include Tzu, Tzu::Validation

  request_object MyRequestObject

  def call(request)
    "Name: #{request.name}, Age: #{request.age}"
  end
end

outcome = MyValidatedCommand.run(name: 'Charles')
#=> #<Command::Outcome @success=false, @result={:age=>["can't be blank"]}, @type=:validation>

outcome.success? #=> false
outcome.type? #=> :validation
outcome.result #=> {:age=>["can't be blank"]}

Configure a sequence of Tzu commands

Tzu provides a declarative way of encapsulating sequential command execution.

Consider the following commands:

class SayMyName
  include Tzu

  def call(params)
    "Hello, #{params[:name]}"
  end
end

class MakeMeSoundImportant
  include Tzu

  def call(params)
    "#{params[:boring_message]}! You are the most important citizen of #{params[:country]}!"
  end
end

The Tzu::Sequence provides a DSL for executing them in sequence:

class ProclaimMyImportance
  include Tzu::Sequence

  step SayMyName do
    receives do |params|
      { name: params[:name] }
    end
  end

  step MakeMeSoundImportant do
    receives do |params, prior_results|
      {
        boring_message: prior_results[:say_my_name],
        country: params[:country]
      }
    end
  end
end

Each command to be executed is defined as the first argument of step. The receives method inside the step block allows you to mutate the parameters being passed into the command. It is passed both the original parameters and a hash containing the results of prior commands.

By default, the keys of the prior_results hash are underscored/symbolized command names. You can define your own keys using the as method.

step SayMyName do
  as :first_command_key
  receives do |params|
    { name: params[:name] }
  end
end

Executing the sequence

By default, Sequences return the result of the final command within an Outcome object,

outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
outcome.success? #=> true
outcome.result #=> 'Hello, Jessica! You are the most important citizen of Azerbaijan!'

Sequences can be configured to return the entire prior_results hash by passing :take_all to the result method.

class ProclaimMyImportance
  include Tzu::Sequence

  step SayMyName do
    receives do |params|
      { name: params[:name] }
    end
  end

  step MakeMeSoundImportant do
    receives do |params, prior_results|
      {
        boring_message: prior_results[:say_my_name],
        country: params[:country]
      }
    end
  end

  result :take_all
end

outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
outcome.result
#=> { say_my_name: 'Hello, Jessica', make_me_sound_important: 'Hello, Jessica! You are the most important citizen of Azerbaijan!' }

You can also mutate the result into any form you choose by passing a block to result.

class ProclaimMyImportance
  include Tzu::Sequence

  step SayMyName do
    receives do |params|
      { name: params[:name] }
    end
  end

  step MakeMeSoundImportant do
    as :final_command
    receives do |params, prior_results|
      {
        boring_message: prior_results[:say_my_name],
        country: params[:country]
      }
    end
  end

  result do |params, prior_results|
    {
      name: params[:name],
      original_message: prior_results[:say_my_name],
      message: "BULLETIN: #{prior_results[:final_command]}"
    }
  end
end

outcome = ProclaimMyImportance.run(name: 'Jessica', country: 'Azerbaijan')
outcome.result
#=> { name: 'Jessica', original_message: 'Hello, Jessica', message: 'BULLETIN: Hello, Jessica! You are the most important citizen of Azerbaijan!' }

Hooks for Sequences

Tzu sequences have the same before, after, and around hooks available in Tzu commands. This is particularly useful for wrapping multiple commands in a transaction.

class ProclaimMyImportance
  include Tzu::Sequence

  around do |sequence|
    ActiveRecord::Base.transaction do
      sequence.call
    end
  end

  step SayMyName do
    receives do |params|
      { name: params[:name] }
    end
  end

  step MakeMeSoundImportant do
    receives do |params, prior_results|
      {
        boring_message: prior_results[:say_my_name],
        country: params[:country]
      }
    end
  end
end