ComposedCommands
Composed Commands is a tool set for creating commands and assembling multiple of these commands in operation pipelines. A command is, at its core, an implementation of the strategy pattern and in this sense an encapsulation of an algorithm. An operation pipeline is an assembly of multiple commands and useful for implementing complex algorithms. Pipelines themselves can be part of other pipelines.
Installation
Add this line to your application's Gemfile:
gem 'composed_commands'
And then execute:
$ bundle
Or install it yourself as:
$ gem install composed_commands
Usage
Operations can be defined by subclassing ComposedCommands::Command
and
operation pipelines by subclassing ComposedCommands::ComposedCommand
.
Defining an Command
To define an command, two steps are necessary:
- create a new subclass of
ComposedCommands::Command
, and - implement the
#execute
method.
The listing below shows an operation that extracts a timestamp in the format
yyyy-mm-dd
from a string.
class DateExtractor < ComposedCommands::Command
def execute(text)
text.scan(/(\d{4})-(\d{2})-(\d{2})/)
end
end
There are two ways to execute this operation:
- create a new instance of this command and call
#perform
, or - directly call
.perform
on the command class.
Please note that directly calling the #execute
method is prohibited. To enforce this constraint, the method is automatically
marked as protected upon definition.
The listing below demonstrates how to execute the command defined above.
text = "This gem was first published on 2013-06-10."
extractor = DateExtractor.new
extractor.perform(text) # => [["2013", "06", "10"]]
DateExtractor.perform(text) # => [["2013", "06", "10"]]
Defining an Command Pipeline
Assume that we are provided a command that converts these arrays of strings
into actual Time
objects. The following listing provides a potential
implementation of such an operation.
class DateArrayToTimeObjectConverter < ComposedCommands::Command
def execute(collection_of_date_arrays)
collection_of_date_arrays.map do |date_array|
Time.new(*(date_array.map(&:to_i)))
end
end
end
Using these two commands, it is possible to create a composed command that
extracts dates from a string and directly converts them into Time
objects. To
define a composed command, two steps are necessary:
- create a subclass of
ComposedCommands::ComposedCommand
, and - use the macro method
use
to assemble the command.
The listing below shows how to assemble the two commands, DateExtractor
and
DateArrayToTimeObjectConverter
, into a composed command named DateParser
.
class DateParser < ComposedCommands::ComposedCommand
use DateExtractor
use DateArrayToTimeObjectConverter
end
Composed commands provide the same interface as normal commands. Hence,
they can be invoked the same way. For the sake of completeness, the listing
below shows how to use the DateParser
operation.
text = "This gem was first published on 2013-06-10."
parser = DateParser.new
parser.perform(text) # => 2013-06-07 00:00:00 +0200
DateParser.perform(text) # => 2013-06-07 00:00:00 +0200
Control Flow
A command can be aborted if a successful execution is not possible. The listing below provides examples on how to access an command's state.
class StrictDateParser < DateParser
def execute
result = super
fail! "no timestamp found" if result.empty?
result
end
end
parser = StrictDateParser.new("")
parser. # => "no timestamp found"
parser.perform
parser.succeed? # => false
parser.failed? # => true
Configuring Commands
Commands and composed commands support Virtus to conveniently provide additional settings upon initialization of a command. In the example below, a command is defined that indents a given string. The indent is set to 2 by default but can easily be changed by supplying an options hash to the initializer.
class Indention < ComposedCommands::Command
attribute :indent, Integer, default: 2, required: true
def execute(text)
text.split("\n").map { |line| " " * indent + line }.join("\n")
end
end
command = Indention.perform("Hello World", indent: 4)
command.result = # => " Hello World"
Commands that are part of a composed command can be configured by calling
the .use
method with a hash of options as the second argument. See the
listing below for an example.
class SomeComposedCommand < ComposedCommands::ComposedCommand
# ...
use Indention, indent: 4
# ...
end
You can configure part of Composed Command at runtime using before_execute
callback':
class AbilityChecker < ComposedCommands::ComposedCommand
attribute :user, User, require: true
use Presence
use RegisteredAt, after: '2010-10-10
use Admin
# ...
def before_execute(command)
command.user = user
end
end
ability = AbilityChecker.new(user: current_user)
ability.perform(post)
This gem based on Composable Operations written by Konstantin Tennhard
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request