composable_operations 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +213 -2
- data/Rakefile +3 -0
- data/composable_operations.gemspec +3 -2
- data/lib/composable_operations/operation.rb +1 -1
- data/lib/composable_operations/version.rb +1 -1
- data/spec/composable_operations/composed_operation_spec.rb +1 -1
- data/spec/composable_operations/control_flow_spec.rb +5 -5
- data/spec/composable_operations/operation_spec.rb +2 -2
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6eeabae9917ce517a8f6ba380b3485479edde5a
|
4
|
+
data.tar.gz: 94ab6cddddbea6be52e2e426a9cc56088bd4b5ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb094f4b9cd7c25d96cb1ebdc27727c0e23d9dd86ad6c386aa1b40107bad8060cc8635a27d4a01bbb91cad34b1cb3644a6eed92ed88ba27c0df456f8671afb73
|
7
|
+
data.tar.gz: 62f381ac83ebdf643a5e04d1e1d61b60cd1e578fa6202e386fe88db20972d187223a606ba67fd9b091a02a434a78baaf0a1b0230425114b18e65bad46e3686b6
|
data/README.md
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
# ComposableOperations
|
2
2
|
|
3
|
-
|
3
|
+
Composable Operations is a tool set for creating operations and assembling
|
4
|
+
multiple of these operations in operation pipelines. An operation is, at its
|
5
|
+
core, an implementation of the [strategy
|
6
|
+
pattern](http://en.wikipedia.org/wiki/Strategy_pattern) and in this sense an
|
7
|
+
encapsulation of an algorithm. An operation pipeline is an assembly of multiple
|
8
|
+
operations and useful for implementing complex algorithms. Pipelines themselves
|
9
|
+
can be part of other pipelines.
|
4
10
|
|
5
11
|
## Installation
|
6
12
|
|
@@ -18,7 +24,212 @@ Or install it yourself as:
|
|
18
24
|
|
19
25
|
## Usage
|
20
26
|
|
21
|
-
|
27
|
+
Operations can be defined by subclassing `ComposableOperations::Operation` and
|
28
|
+
operation pipelines by subclassing `ComposableOperations::ComposedOperation`.
|
29
|
+
|
30
|
+
### Defining an Operation
|
31
|
+
|
32
|
+
To define an operation, two steps are necessary:
|
33
|
+
|
34
|
+
1. create a new subclass of `ComposableOperations::Operations`, and
|
35
|
+
2. implement the `#execute` method.
|
36
|
+
|
37
|
+
The listing below shows an operation that extracts a timestamp in the format
|
38
|
+
`yyyy-mm-dd` from a string.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class DateExtractor < ComposableOperations::Operation
|
42
|
+
|
43
|
+
processes :text
|
44
|
+
|
45
|
+
def execute
|
46
|
+
text.scan(/(\d{4})-(\d{2})-(\d{2})/)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
The macro method `.processes` followed by a single argument denotes that the
|
53
|
+
operation expects a single object as input and results in the definition of a
|
54
|
+
getter method named as specified by this argument. The macro method can also be
|
55
|
+
called with multiple arguments resulting in the creation of multiple getter
|
56
|
+
methods. The latter is useful if the operation requires more than one object as
|
57
|
+
input to operate. Calling the macro method is entirely optional. An operation's
|
58
|
+
input can always be accessed by calling the getter method `#input`. This method
|
59
|
+
returns either a single object or an array of objects.
|
60
|
+
|
61
|
+
There are two ways to execute this operation:
|
62
|
+
|
63
|
+
1. create a new instance of this operation and call `#perform`, or
|
64
|
+
2. directly call `.perform` on the operation class.
|
65
|
+
|
66
|
+
The major difference between these two approaches is that in case of a failure
|
67
|
+
the latter raises an exception while the former returns `nil` and sets the
|
68
|
+
operation's state to `failed`. For more information on canceling the execution
|
69
|
+
of an operation, see below. Please note that directly calling the `#execute`
|
70
|
+
method is prohibited. To enforce this constraint, the method is automatically
|
71
|
+
marked as protected upon definition.
|
72
|
+
|
73
|
+
The listing below demonstrates how to execute the operation defined above.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
text = "This gem was first published on 2013-06-10."
|
77
|
+
|
78
|
+
extractor = DateExtractor.new(text)
|
79
|
+
extractor.perform # => [["2013", "06", "10"]]
|
80
|
+
|
81
|
+
DateExtractor.perform(text) # => [["2013", "06", "10"]]
|
82
|
+
```
|
83
|
+
|
84
|
+
### Defining an Operation Pipeline
|
85
|
+
|
86
|
+
Assume that we are provided an operation that converts these arrays of strings
|
87
|
+
into actual `Time` objects. The following listing provides a potential
|
88
|
+
implementation of such an operation.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class DateArrayToTimeObjectConverter < ComposableOperations::Operation
|
92
|
+
|
93
|
+
processes :collection_of_date_arrays
|
94
|
+
|
95
|
+
def execute
|
96
|
+
collection_of_date_arrays.map do |date_array|
|
97
|
+
Time.new(*(date_array.map(&:to_i)))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
Using these two operations, it is possible to create a composed operation that
|
105
|
+
extracts dates from a string and directly converts them into `Time` objects. To
|
106
|
+
define a composed operation, two steps are necessary:
|
107
|
+
|
108
|
+
1. create a subclass of `ComposableOperations::ComposedOperation`, and
|
109
|
+
2. use the macro method `use` to assemble the operation.
|
110
|
+
|
111
|
+
The listing below shows how to assemble the two operations, `DateExtractor` and
|
112
|
+
`DateArrayToTimeObjectConverter`, into a composed operation named `DateParser`.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class DateParser < ComposableOperations::ComposedOperation
|
116
|
+
|
117
|
+
use DateExtractor
|
118
|
+
use DateArrayToTimeObjectConverter
|
119
|
+
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
Composed operations provide the same interface as normal operations. Hence,
|
124
|
+
they can be invoked the same way. For the sake of completeness, the listing
|
125
|
+
below shows how to use the `DateParser` operation.
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
text = "This gem was first published on 2013-06-10."
|
129
|
+
|
130
|
+
parser = DateParser.new(text)
|
131
|
+
parser.perform # => 2013-06-07 00:00:00 +0200
|
132
|
+
|
133
|
+
DateParser.perform(text) # => 2013-06-07 00:00:00 +0200
|
134
|
+
```
|
135
|
+
|
136
|
+
### Control Flow
|
137
|
+
|
138
|
+
An operation can be *halted* or *aborted* if a successful execution is not
|
139
|
+
possible. Aborting an operation will result in an exception if the operation
|
140
|
+
was invoked using the class method `.perform`. If the operation was invoked
|
141
|
+
using the instance method `#perform`, the operation's state will be updated
|
142
|
+
accordingly, but no exception will be raised. The listing below provides, among
|
143
|
+
other things, examples on how to access an operation's state.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
class StrictDateParser < DateParser
|
147
|
+
|
148
|
+
def execute
|
149
|
+
result = super
|
150
|
+
fail "no timestamp found" if result.empty?
|
151
|
+
result
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
class LessStrictDateParser < DateParser
|
157
|
+
|
158
|
+
def execute
|
159
|
+
result = super
|
160
|
+
halt "no timestamp found" if result.empty?
|
161
|
+
result
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
parser = StrictDateParser.new("")
|
167
|
+
parser.message # => "no timestamp found"
|
168
|
+
parser.perform # => nil
|
169
|
+
parser.succeeded? # => false
|
170
|
+
parser.halted? # => false
|
171
|
+
parser.failed? # => true
|
172
|
+
|
173
|
+
StrictDateParser.perform("") # => ComposableOperations::OperationError: no timestamp found
|
174
|
+
|
175
|
+
parser = LessStricDateParser.new("")
|
176
|
+
parser.message # => "no timestamp found"
|
177
|
+
parser.perform # => nil
|
178
|
+
parser.succeeded? # => false
|
179
|
+
parser.halted? # => true
|
180
|
+
parser.failed? # => false
|
181
|
+
|
182
|
+
StrictDateParser.perform("") # => nil
|
183
|
+
```
|
184
|
+
|
185
|
+
Instead of cluttering the `#execute` method with sentinel code or in general
|
186
|
+
with code that is not part of an operation's algorithmic core, we can move this
|
187
|
+
code into `before` or `after` callbacks. The listing below provides an alternative
|
188
|
+
implementation of the `StrictDateParser` operation.
|
189
|
+
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
class StrictDateParser < DateParser
|
193
|
+
|
194
|
+
after do
|
195
|
+
fail "no timestamp found" if result.empty?
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
parser = StrictDateParser.new("")
|
201
|
+
parser.message # => "no timestamp found"
|
202
|
+
parser.perform # => nil
|
203
|
+
parser.failed? # => true
|
204
|
+
|
205
|
+
StrictDateParser.perform("") # => ComposableOperations::OperationError: no timestamp found
|
206
|
+
```
|
207
|
+
|
208
|
+
### Configuring Operations
|
209
|
+
|
210
|
+
Operations and composed operations support
|
211
|
+
[SmartProperties](http://github.com/t6d/smart_properties) to conveniently
|
212
|
+
provide additional settings upon initialization of an operation. In the
|
213
|
+
example, below an operation is defined that indents a given string. The indent
|
214
|
+
is set to 2 by default but can easily be changed by supplying an options hash
|
215
|
+
to the initializer.
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
class Indention < ComposableOperations::Operation
|
219
|
+
|
220
|
+
property :indent, default: 2,
|
221
|
+
converts: lambda { |value| value.to_s.to_i },
|
222
|
+
accepts: lambda { |value| value >= 0 },
|
223
|
+
required: true
|
224
|
+
|
225
|
+
def execute
|
226
|
+
input.split("\n").map { |line| " " * indent + line }.join("\n")
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
Indention.perform("Hello World", indent: 4) # => " Hello World"
|
232
|
+
```
|
22
233
|
|
23
234
|
## Contributing
|
24
235
|
|
data/Rakefile
CHANGED
@@ -9,7 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Konstantin Tennhard"]
|
10
10
|
spec.email = ["me@t6d.de"]
|
11
11
|
spec.summary = %q{Tool set for operation pipelines.}
|
12
|
-
spec.description = %q{Composable Operations is a tool set for creating
|
12
|
+
spec.description = %q{Composable Operations is a tool set for creating operations and assembling
|
13
|
+
multiple of these operations in operation pipelines.}
|
13
14
|
spec.homepage = "http://github.com/t6d/composable_operations"
|
14
15
|
spec.license = "MIT"
|
15
16
|
|
@@ -22,5 +23,5 @@ Gem::Specification.new do |spec|
|
|
22
23
|
|
23
24
|
spec.add_development_dependency "bundler", "~> 1.3"
|
24
25
|
spec.add_development_dependency "rake"
|
25
|
-
spec.add_development_dependency "rspec", "~> 2.
|
26
|
+
spec.add_development_dependency "rspec", "~> 2.13"
|
26
27
|
end
|
@@ -140,7 +140,7 @@ module ComposableOperations
|
|
140
140
|
throw :halt, return_value
|
141
141
|
end
|
142
142
|
|
143
|
-
def halt(message = nil, return_value =
|
143
|
+
def halt(message = nil, return_value = nil)
|
144
144
|
raise "Operation execution has already been aborted" if halted? or failed?
|
145
145
|
|
146
146
|
self.state = :halted
|
@@ -88,7 +88,7 @@ describe ComposableOperations::ComposedOperation do
|
|
88
88
|
end
|
89
89
|
|
90
90
|
it "should return a capitalized version of the generated string" do
|
91
|
-
composed_operation.perform.should be ==
|
91
|
+
composed_operation.perform.should be == nil
|
92
92
|
end
|
93
93
|
|
94
94
|
it "should only execute the first two operations" do
|
@@ -108,31 +108,31 @@ describe "An operation with two before and two after filters =>" do
|
|
108
108
|
}, {
|
109
109
|
:context => "halting in outer_before filter =>",
|
110
110
|
:input => [:halt_in_outer_before],
|
111
|
-
:output =>
|
111
|
+
:output => nil,
|
112
112
|
:trace => [:initialize, :outer_before, :inner_after, :outer_after],
|
113
113
|
:state => :halted
|
114
114
|
}, {
|
115
115
|
:context => "halting in inner_before filter =>",
|
116
116
|
:input => [:halt_in_inner_before],
|
117
|
-
:output =>
|
117
|
+
:output => nil,
|
118
118
|
:trace => [:initialize, :outer_before, :inner_before, :inner_after, :outer_after],
|
119
119
|
:state => :halted
|
120
120
|
}, {
|
121
121
|
:context => "halting in execute =>",
|
122
122
|
:input => [:halt_in_execute],
|
123
|
-
:output =>
|
123
|
+
:output => nil,
|
124
124
|
:trace => [:initialize, :outer_before, :inner_before, :execute_start, :inner_after, :outer_after],
|
125
125
|
:state => :halted
|
126
126
|
}, {
|
127
127
|
:context => "halting in inner_after filter =>",
|
128
128
|
:input => [:halt_in_inner_after],
|
129
|
-
:output =>
|
129
|
+
:output => nil,
|
130
130
|
:trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
|
131
131
|
:state => :halted
|
132
132
|
}, {
|
133
133
|
:context => "halting in outer_after filter =>",
|
134
134
|
:input => [:halt_in_outer_after],
|
135
|
-
:output =>
|
135
|
+
:output => nil,
|
136
136
|
:trace => [:initialize, :outer_before, :inner_before, :execute_start, :execute_stop, :inner_after, :outer_after],
|
137
137
|
:state => :halted
|
138
138
|
}
|
@@ -17,12 +17,12 @@ describe ComposableOperations::Operation do
|
|
17
17
|
|
18
18
|
end
|
19
19
|
|
20
|
-
context "that always halts" do
|
20
|
+
context "that always halts and returns its original input" do
|
21
21
|
|
22
22
|
let(:halting_operation) do
|
23
23
|
Class.new(described_class) do
|
24
24
|
def execute
|
25
|
-
halt "Full stop!"
|
25
|
+
halt "Full stop!", input
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: composable_operations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Tennhard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-06-
|
11
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: smart_properties
|
@@ -58,16 +58,17 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ~>
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '2.
|
61
|
+
version: '2.13'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ~>
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '2.
|
69
|
-
description:
|
70
|
-
|
68
|
+
version: '2.13'
|
69
|
+
description: |-
|
70
|
+
Composable Operations is a tool set for creating operations and assembling
|
71
|
+
multiple of these operations in operation pipelines.
|
71
72
|
email:
|
72
73
|
- me@t6d.de
|
73
74
|
executables: []
|