Module: Cuprum::Chaining

Included in:
Command
Defined in:
lib/cuprum/chaining.rb

Overview

Mixin to implement command chaining functionality for a command class. Chaining commands allows you to define complex logic by composing it from simpler commands, including branching logic and error handling.

Examples:

Chaining Commands

# By chaining commands together with the #chain instance method, we set up
# a series of commands to run in sequence. Each chained command is passed
# the value of the previous command.

class GenerateUrlCommand
  include Cuprum::Chaining
  include Cuprum::Processing

  private

  # Acts as a pipeline, taking a value (the title of the given post) and
  # calling the underscore, URL safe, and prepend date commands. By
  # passing parameters to PrependDateCommand, we can customize the command
  # in the pipeline to the current context (in this case, the Post).
  def process post
    UnderscoreCommand.new.
      chain(UrlSafeCommand.new).
      chain(PrependDateCommand.new(post.created_at)).
      call(post.title)
  end
end

title = 'Greetings, programs!'
date  = '1982-07-09'
post  = Post.new(:title => title, :created_at => date)
url   = GenerateUrlCommand.new.call(post).value
#=> '1982_07_09_greetings_programs'

title = 'Plasma-based Einhanders in Popular Media'
date  = '1977-05-25'
post  = Post.new(:title => title, :created_at => date)
url   = GenerateUrlCommand.new.call(post).value
#=> '1977_05_25_plasma_based_einhanders_in_popular_media'

Conditional Chaining

# Commands can be conditionally chained based on the success or failure of
# the previous command using the on: keyword. If the command is chained
# using on: :success, it will only be called if the result is passing.
# If the command is chained using on: :failure, it will only be called if
# the command is failing. This can be used to perform error handling.

class CreateTaggingCommand
  include Cuprum::Chaining
  include Cuprum::Processing

  private

  def create_tag
    Command.new do |tag_name|
      tag = Tag.new(name: tag_name)

      return tag if tag.save

      Cuprum::Result.new(error: tag.errors)
    end
  end

  def create_tagging(taggable)
    Command.new do |tag|
      tagging = tag.build_tagging(taggable)

      return tagging if tagging.save

      Cuprum::Result.new(error: tagging.errors)
    end
  end

  def find_tag
    Command.new do |tag_name|
      tag = Tag.where(name: tag_name).first

      tag || Cuprum::Result.new(error: 'tag not found')
    end
  end

  # Tries to find the tag with the given name. If that fails, creates a
  # new tag with the given name. If the tag is found, or if the new tag is
  # successfully created, then creates a tagging using the tag. If the tag
  # is not found and cannot be created, then the tagging is not created
  # and the result of the CreateTaggingCommand is a failure with the
  # appropriate error messages.
  def process(taggable, tag_name)
    find_tag
      .chain(on: :failure) do
        # If the finding the tag fails, this step is called, returning a
        # result with a newly created tag.
        create_tag.call(tag_name)
      end
      .chain(:on => :success) do |tag|
        # Finally, the tag has been either found or created, so we can
        # create the tagging relation.
        tag.create_tagging(taggable)
      end
      .call(tag_name)
  end
end

post        = Post.create(:title => 'Tagging Example')
example_tag = Tag.create(:name => 'Example Tag')

result = CreateTaggingCommand.new.call(post, 'Example Tag')
result.success? #=> true
result.error    #=> nil
result.value    #=> an instance of Tagging
post.tags.map(&:name)
#=> ['Example Tag']

result = CreateTaggingCommand.new.call(post, 'Another Tag')
result.success? #=> true
result.error    #=> nil
result.value    #=> an instance of Tagging
post.tags.map(&:name)
#=> ['Example Tag', 'Another Tag']

result = CreateTaggingCommand.new.call(post, 'An Invalid Tag Name')
result.success? #=> false
result.error    #=> [{ tag: { name: ['is invalid'] }}]
post.tags.map(&:name)
#=> ['Example Tag', 'Another Tag']

Yield Result and Tap Result

# The #yield_result method allows for advanced control over a step in the
# command chain. The block will be yielded the result at that point in the
# chain, and will wrap the returned value in a result to the next chained
# command (or return it directly if the returned value is a result).
#
# The #tap_result method inserts arbitrary code into the command chain
# without interrupting it. The block will be yielded the result at that
# point in the chain and will pass that same result to the next chained
# command after executing the block. The return value of the block is
# ignored.

class UpdatePostCommand
  include Cuprum::Chaining
  include Cuprum::Processing

  private

  def process id, attributes
    # First, find the referenced post.
    Find.new(Post).call(id).
      yield_result(:on => :failure) do |result|
        redirect_to posts_path
      end.
      yield_result(on: :success) do |result|
        # Assign our attributes and save the post.
        UpdateAttributes.new.call(result.value, attributes)
      end.
      tap_result(:on => :success) do |result|
        # Create our tags, but still return the result of our update.
        attributes[:tags].each do |tag_name|
          CreateTaggingCommand.new.call(result.value, tag_name)
        end
      end.
      tap_result(:on => :always) do |result|
        # Chaining :on => :always ensures that the command will be run,
        # even if the previous result is failing.
        if result.failure?
          log_errors(
            :command => UpdatePostCommand,
            :error   => result.error
          )
        end
      end
  end
end

Protected Chaining Methods

# Using the protected chaining methods #chain!, #tap_result!, and
# #yield_result!, you can create a command class that composes other
# commands.

# We subclass the build command, which will be executed first.
class CreateCommentCommand < BuildCommentCommand
  include Cuprum::Chaining
  include Cuprum::Processing

  def initialize
    # After the build step is run, we validate the comment.
    chain!(ValidateCommentCommand.new)

    # If the validation passes, we then save the comment.
    chain!(SaveCommentCommand.new, on: :success)
  end
end

See Also:

Instance Method Summary collapse

Instance Method Details

#call(*arguments, **keywords) { ... } ⇒ Cuprum::Result

Executes the command implementation and returns a Cuprum::Result or compatible object.

Each time #call is invoked, the object performs the following steps:

  1. The #process method is called, passing the arguments, keywords, and block that were passed to #call.

  2. If the value returned by #process is a Cuprum::Result or compatible object, that result is directly returned by #call.

  3. Otherwise, the value returned by #process will be wrapped in a successful result, which will be returned by #call.

Parameters:

  • arguments (Array)

    Arguments to be passed to the implementation.

  • keywords (Hash)

    Keywords to be passed to the implementation.

Yields:

  • If a block argument is given, it will be passed to the implementation.

Returns:



200
201
202
# File 'lib/cuprum/chaining.rb', line 200

def call(*args, &block)
  yield_chain(super)
end

#chain(command, on: nil) ⇒ Cuprum::Chaining #chain(on: nil) {|value| ... } ⇒ Cuprum::Chaining

Creates a copy of the first command, and then chains the given command or block to execute after the first command’s implementation. When #call is executed, each chained command will be called with the previous result value, and its result property will be set to the previous result. The return value will be wrapped in a result and returned or yielded to the next block.

Overloads:

  • #chain(command, on: nil) ⇒ Cuprum::Chaining

    Parameters:

    • command (Cuprum::Command)

      The command to chain.

    • on (Symbol) (defaults to: nil)

      Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

  • #chain(on: nil) {|value| ... } ⇒ Cuprum::Chaining

    Creates an anonymous command from the given block. The command will be passed the value of the previous result.

    Parameters:

    • on (Symbol) (defaults to: nil)

      Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

    Yield Parameters:

    • value (Object)

      The value of the previous result.

Returns:

See Also:



242
243
244
# File 'lib/cuprum/chaining.rb', line 242

def chain(command = nil, on: nil, &block)
  clone.chain!(command, on: on, &block)
end

#chain!(command, on: nil) ⇒ Cuprum::Chaining #chain!(on: nil) {|value| ... } ⇒ Cuprum::Chaining

As #chain, but modifies the current command instead of creating a clone. This is a protected method, and is meant to be called by the command to be chained, such as during #initialize.

Overloads:

  • #chain!(command, on: nil) ⇒ Cuprum::Chaining

    Parameters:

    • command (Cuprum::Command)

      The command to chain.

    • on (Symbol) (defaults to: nil)

      Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

  • #chain!(on: nil) {|value| ... } ⇒ Cuprum::Chaining

    Creates an anonymous command from the given block. The command will be passed the value of the previous result.

    Parameters:

    • on (Symbol) (defaults to: nil)

      Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

    Yield Parameters:

    • value (Object)

      The value of the previous result.

Returns:

See Also:



321
322
323
324
325
326
327
328
329
330
331
# File 'lib/cuprum/chaining.rb', line 321

def chain!(command = nil, on: nil, &block)
  command ||= Cuprum::Command.new(&block)

  chained_procs <<
    {
      proc: chain_command(command),
      on:   on
    } # end hash

  self
end

#tap_result(on: nil) {|result| ... } ⇒ Cuprum::Chaining

As #yield_result, but always returns the previous result when the block is called. The return value of the block is discarded.

Parameters:

  • on (Symbol) (defaults to: nil)

    Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

Yield Parameters:

Returns:

See Also:



256
257
258
# File 'lib/cuprum/chaining.rb', line 256

def tap_result(on: nil, &block)
  clone.tap_result!(on: on, &block)
end

#tap_result!(on: nil) {|result| ... } ⇒ Cuprum::Chaining

As #tap_result, but modifies the current command instead of creating a clone. This is a protected method, and is meant to be called by the command to be chained, such as during #initialize.

Parameters:

  • on (Symbol) (defaults to: nil)

    Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

Yield Parameters:

Returns:

See Also:



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/cuprum/chaining.rb', line 350

def tap_result!(on: nil, &block)
  tapped = ->(result) { result.tap { block.call(result) } }

  chained_procs <<
    {
      proc: tapped,
      on:   on
    } # end hash

  self
end

#yield_result(on: nil) {|result| ... } ⇒ Cuprum::Chaining

Creates a copy of the command, and then chains the block to execute after the command implementation. When #call is executed, each chained block will be yielded the previous result, and the return value wrapped in a result and returned or yielded to the next block.

Parameters:

  • on (Symbol) (defaults to: nil)

    Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

Yield Parameters:

Returns:

See Also:



279
280
281
# File 'lib/cuprum/chaining.rb', line 279

def yield_result(on: nil, &block)
  clone.yield_result!(on: on, &block)
end

#yield_result!(on: nil) {|result| ... } ⇒ Cuprum::Chaining

As #yield_result, but modifies the current command instead of creating a clone. This is a protected method, and is meant to be called by the command to be chained, such as during #initialize.

Parameters:

  • on (Symbol) (defaults to: nil)

    Sets a condition on when the chained block can run, based on the previous result. Valid values are :success, :failure, and :always. If the value is :success, the block will be called only if the previous result succeeded. If the value is :failure, the block will be called only if the previous result failed. If the value is :always, the block will be called regardless of the previous result status. If no value is given, the command will run whether the previous command was a success or a failure.

Yield Parameters:

Returns:

See Also:



375
376
377
378
379
380
381
382
383
# File 'lib/cuprum/chaining.rb', line 375

def yield_result!(on: nil, &block)
  chained_procs <<
    {
      proc: block,
      on:   on
    } # end hash

  self
end