README.md in composable_operations-0.1.0 vs README.md in composable_operations-0.2.0
- old
+ new
@@ -1,8 +1,14 @@
# ComposableOperations
-TODO: Write a gem description
+Composable Operations is a tool set for creating operations and assembling
+multiple of these operations in operation pipelines. An operation is, at its
+core, an implementation of the [strategy
+pattern](http://en.wikipedia.org/wiki/Strategy_pattern) and in this sense an
+encapsulation of an algorithm. An operation pipeline is an assembly of multiple
+operations and useful for implementing complex algorithms. Pipelines themselves
+can be part of other pipelines.
## Installation
Add this line to your application's Gemfile:
@@ -16,10 +22,215 @@
$ gem install composable_operations
## Usage
-TODO: Write usage instructions here
+Operations can be defined by subclassing `ComposableOperations::Operation` and
+operation pipelines by subclassing `ComposableOperations::ComposedOperation`.
+
+### Defining an Operation
+
+To define an operation, two steps are necessary:
+
+1. create a new subclass of `ComposableOperations::Operations`, and
+2. implement the `#execute` method.
+
+The listing below shows an operation that extracts a timestamp in the format
+`yyyy-mm-dd` from a string.
+
+```ruby
+class DateExtractor < ComposableOperations::Operation
+
+ processes :text
+
+ def execute
+ text.scan(/(\d{4})-(\d{2})-(\d{2})/)
+ end
+
+end
+```
+
+The macro method `.processes` followed by a single argument denotes that the
+operation expects a single object as input and results in the definition of a
+getter method named as specified by this argument. The macro method can also be
+called with multiple arguments resulting in the creation of multiple getter
+methods. The latter is useful if the operation requires more than one object as
+input to operate. Calling the macro method is entirely optional. An operation's
+input can always be accessed by calling the getter method `#input`. This method
+returns either a single object or an array of objects.
+
+There are two ways to execute this operation:
+
+1. create a new instance of this operation and call `#perform`, or
+2. directly call `.perform` on the operation class.
+
+The major difference between these two approaches is that in case of a failure
+the latter raises an exception while the former returns `nil` and sets the
+operation's state to `failed`. For more information on canceling the execution
+of an operation, see below. 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 operation defined above.
+
+```ruby
+text = "This gem was first published on 2013-06-10."
+
+extractor = DateExtractor.new(text)
+extractor.perform # => [["2013", "06", "10"]]
+
+DateExtractor.perform(text) # => [["2013", "06", "10"]]
+```
+
+### Defining an Operation Pipeline
+
+Assume that we are provided an operation that converts these arrays of strings
+into actual `Time` objects. The following listing provides a potential
+implementation of such an operation.
+
+```ruby
+class DateArrayToTimeObjectConverter < ComposableOperations::Operation
+
+ processes :collection_of_date_arrays
+
+ def execute
+ collection_of_date_arrays.map do |date_array|
+ Time.new(*(date_array.map(&:to_i)))
+ end
+ end
+
+end
+```
+
+Using these two operations, it is possible to create a composed operation that
+extracts dates from a string and directly converts them into `Time` objects. To
+define a composed operation, two steps are necessary:
+
+1. create a subclass of `ComposableOperations::ComposedOperation`, and
+2. use the macro method `use` to assemble the operation.
+
+The listing below shows how to assemble the two operations, `DateExtractor` and
+`DateArrayToTimeObjectConverter`, into a composed operation named `DateParser`.
+
+```ruby
+class DateParser < ComposableOperations::ComposedOperation
+
+ use DateExtractor
+ use DateArrayToTimeObjectConverter
+
+end
+```
+
+Composed operations provide the same interface as normal operations. Hence,
+they can be invoked the same way. For the sake of completeness, the listing
+below shows how to use the `DateParser` operation.
+
+```ruby
+text = "This gem was first published on 2013-06-10."
+
+parser = DateParser.new(text)
+parser.perform # => 2013-06-07 00:00:00 +0200
+
+DateParser.perform(text) # => 2013-06-07 00:00:00 +0200
+```
+
+### Control Flow
+
+An operation can be *halted* or *aborted* if a successful execution is not
+possible. Aborting an operation will result in an exception if the operation
+was invoked using the class method `.perform`. If the operation was invoked
+using the instance method `#perform`, the operation's state will be updated
+accordingly, but no exception will be raised. The listing below provides, among
+other things, examples on how to access an operation's state.
+
+```ruby
+class StrictDateParser < DateParser
+
+ def execute
+ result = super
+ fail "no timestamp found" if result.empty?
+ result
+ end
+
+end
+
+class LessStrictDateParser < DateParser
+
+ def execute
+ result = super
+ halt "no timestamp found" if result.empty?
+ result
+ end
+
+end
+
+parser = StrictDateParser.new("")
+parser.message # => "no timestamp found"
+parser.perform # => nil
+parser.succeeded? # => false
+parser.halted? # => false
+parser.failed? # => true
+
+StrictDateParser.perform("") # => ComposableOperations::OperationError: no timestamp found
+
+parser = LessStricDateParser.new("")
+parser.message # => "no timestamp found"
+parser.perform # => nil
+parser.succeeded? # => false
+parser.halted? # => true
+parser.failed? # => false
+
+StrictDateParser.perform("") # => nil
+```
+
+Instead of cluttering the `#execute` method with sentinel code or in general
+with code that is not part of an operation's algorithmic core, we can move this
+code into `before` or `after` callbacks. The listing below provides an alternative
+implementation of the `StrictDateParser` operation.
+
+
+```ruby
+class StrictDateParser < DateParser
+
+ after do
+ fail "no timestamp found" if result.empty?
+ end
+
+end
+
+parser = StrictDateParser.new("")
+parser.message # => "no timestamp found"
+parser.perform # => nil
+parser.failed? # => true
+
+StrictDateParser.perform("") # => ComposableOperations::OperationError: no timestamp found
+```
+
+### Configuring Operations
+
+Operations and composed operations support
+[SmartProperties](http://github.com/t6d/smart_properties) to conveniently
+provide additional settings upon initialization of an operation. In the
+example, below an operation 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.
+
+```ruby
+class Indention < ComposableOperations::Operation
+
+ property :indent, default: 2,
+ converts: lambda { |value| value.to_s.to_i },
+ accepts: lambda { |value| value >= 0 },
+ required: true
+
+ def execute
+ input.split("\n").map { |line| " " * indent + line }.join("\n")
+ end
+
+end
+
+Indention.perform("Hello World", indent: 4) # => " Hello World"
+```
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)