composable_operations 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|