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 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: []