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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2815a0e9caf243a6e502b0f277cb98c36b44b9d5
4
- data.tar.gz: 668f9e8de07e9287f7498909fca06f2d61ab4904
3
+ metadata.gz: c6eeabae9917ce517a8f6ba380b3485479edde5a
4
+ data.tar.gz: 94ab6cddddbea6be52e2e426a9cc56088bd4b5ac
5
5
  SHA512:
6
- metadata.gz: 962cef4189be9cabea7c2df388d1f6be24929a211eafe43f9fadddec9ff4375fd553a977a01641ee812fb1e887fc2d62853bdb3eb29e2636b336f10899d3473f
7
- data.tar.gz: e3f3d3df80f08542fa285e27fbe7d4ca9a7c217a41f0d74dfa97e737dc95e9d6742877ec08d2f5f41ccc9f28ac75bffc6b419a33ca1983d6047ea76dfe36db8f
6
+ metadata.gz: bb094f4b9cd7c25d96cb1ebdc27727c0e23d9dd86ad6c386aa1b40107bad8060cc8635a27d4a01bbb91cad34b1cb3644a6eed92ed88ba27c0df456f8671afb73
7
+ data.tar.gz: 62f381ac83ebdf643a5e04d1e1d61b60cd1e578fa6202e386fe88db20972d187223a606ba67fd9b091a02a434a78baaf0a1b0230425114b18e65bad46e3686b6
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # ComposableOperations
2
2
 
3
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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
@@ -1 +1,4 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task :default => :spec
@@ -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 easy-to-use operation pipelines.}
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.11"
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 = input)
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
@@ -1,3 +1,3 @@
1
1
  module ComposableOperations
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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 == "chunky bacon"
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 => [:halt_in_outer_before],
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 => [:halt_in_inner_before],
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 => [:halt_in_execute],
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 => [:halt_in_inner_after],
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 => [:halt_in_outer_after],
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.1.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-10 00:00:00.000000000 Z
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.11'
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.11'
69
- description: Composable Operations is a tool set for creating easy-to-use operation
70
- pipelines.
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: []