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::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 # method process
end # class

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::Processing

  private

  # 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
    FindTag.new.call(tag_name).
      # The chained command is called with the value of the previous
      # command, in this case the Tag or nil returned by FindTag.
      chain(:on => :failure) do |tag|
        # Chained commands share a result object, including errors. To
        # rescue a command chain and return the execution to the "happy
        # path", use on: :failure and clear the errors.
        result.errors.clear

        Tag.create(tag_name)
      end.
      chain(:on => :success) do |tag|
        tag.create_tagging(taggable)
      end
  end # method process
end # method class

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

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

result = CreateTaggingCommand.new.call(post, 'Another Tag')
result.success? #=> true
result.errors   #=> []
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.errors   #=> [{ 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::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

        # A halted result prevents further :on => :failure commands from
        # being called.
        result.halt!
      end.
      yield_result 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 or halted.
        if result.failure?
          log_errors(
            :command => UpdatePostCommand,
            :errors => result.errors
          )
        end
      end
  end
end

See Also:

Instance Method Summary collapse

Instance Method Details

#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 and is not halted. If the value is :failure, the block will be called only if the previous result failed and is not halted. If the value is :always, the block will be called regardless of the previous result status, even if the previous result is halted. If no value is given, the command will run whether the previous command was a success or a failure, but not if the command chain has been halted.

  • #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 and is not halted. If the value is :failure, the block will be called only if the previous result failed and is not halted. If the value is :always, the block will be called regardless of the previous result status, even if the previous result is halted. If no value is given, the command will run whether the previous command was a success or a failure, but not if the command chain has been halted.

    Yield Parameters:

    • value (Object)

      The value of the previous result.

Returns:

See Also:



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/cuprum/chaining.rb', line 195

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

  clone.tap do |fn|
    fn.chained_procs <<
      {
        :proc => chain_command(command),
        :on   => on
      } # end hash
  end # tap
end

#failure(command) ⇒ Cuprum::Chaining #failure {|value| ... } ⇒ Cuprum::Chaining

Shorthand for command.chain(:on => :failure). Creates a copy of the first command, and then chains the given command or block to execute after the first command’s implementation, but only if the previous command is failing.

Overloads:

  • #failure(command) ⇒ Cuprum::Chaining

    Parameters:

  • #failure {|value| ... } ⇒ Cuprum::Chaining

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

    Yield Parameters:

    • value (Object)

      The value of the previous result.

Returns:

See Also:



225
226
227
# File 'lib/cuprum/chaining.rb', line 225

def failure command = nil, &block
  chain(command, :on => :failure, &block)
end

#success(command) ⇒ Cuprum::Chaining #success {|value| ... } ⇒ Cuprum::Chaining

Shorthand for command.chain(:on => :success). Creates a copy of the first command, and then chains the given command or block to execute after the first command’s implementation, but only if the previous command is failing.

Overloads:

  • #success(command) ⇒ Cuprum::Chaining

    Parameters:

  • #success {|value| ... } ⇒ Cuprum::Chaining

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

    Yield Parameters:

    • value (Object)

      The value of the previous result.

Returns:

See Also:



247
248
249
# File 'lib/cuprum/chaining.rb', line 247

def success command = nil, &block
  chain(command, :on => :success, &block)
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 and is not halted. If the value is :failure, the block will be called only if the previous result failed and is not halted. If the value is :always, the block will be called regardless of the previous result status, even if the previous result is halted. If no value is given, the command will run whether the previous command was a success or a failure, but not if the command chain has been halted.

Yield Parameters:

Returns:

See Also:



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/cuprum/chaining.rb', line 261

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

  clone.tap do |fn|
    fn.chained_procs <<
      {
        :proc => tapped,
        :on   => on
      } # end hash
  end # tap
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 and is not halted. If the value is :failure, the block will be called only if the previous result failed and is not halted. If the value is :always, the block will be called regardless of the previous result status, even if the previous result is halted. If no value is given, the command will run whether the previous command was a success or a failure, but not if the command chain has been halted.

Yield Parameters:

Returns:

See Also:



293
294
295
296
297
298
299
300
301
# File 'lib/cuprum/chaining.rb', line 293

def yield_result on: nil, &block
  clone.tap do |fn|
    fn.chained_procs <<
      {
        :proc => block,
        :on   => on
      } # end hash
  end # tap
end