Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/t6d/composable_operations

Composable Operations is a tool set for creating operations and assembling multiple of these operations in operation pipelines.
https://github.com/t6d/composable_operations

Last synced: 3 months ago
JSON representation

Composable Operations is a tool set for creating operations and assembling multiple of these operations in operation pipelines.

Awesome Lists containing this project

README

        

# ComposableOperations

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:

gem 'composable_operations'

And then execute:

$ bundle

Or install it yourself as:

$ gem install composable_operations

## Usage

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

processes :text

property :indent, default: 2,
converts: lambda { |value| value.to_s.to_i },
accepts: lambda { |value| value >= 0 },
required: true

def execute
text.split("\n").map { |line| " " * indent + line }.join("\n")
end

end

Indention.perform("Hello World", indent: 4) # => " Hello World"
```

Operations that are part of an composed operation can be configured by calling
the `.use` method with a hash of options as the second argument. See the
listing below for an example.

```ruby
class SomeComposedOperation < ComposableOperations::ComposedOperation

# ...
use Indention, indent: 4
# ...

end
```

## Contributing

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request