carry_out 0.3.0 → 1.0.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 +99 -162
- data/carry_out.gemspec +1 -0
- data/lib/carry_out/configurator.rb +11 -0
- data/lib/carry_out/plan/guard.rb +22 -0
- data/lib/carry_out/plan/guard_context.rb +13 -0
- data/lib/carry_out/plan/node.rb +71 -0
- data/lib/carry_out/plan/node_context.rb +40 -0
- data/lib/carry_out/plan/node_result.rb +11 -0
- data/lib/carry_out/plan_builder.rb +70 -0
- data/lib/carry_out/plan_runner.rb +34 -0
- data/lib/carry_out/result.rb +4 -0
- data/lib/carry_out/unit.rb +12 -1
- data/lib/carry_out/version.rb +1 -1
- data/lib/carry_out.rb +23 -52
- metadata +24 -7
- data/lib/carry_out/context.rb +0 -17
- data/lib/carry_out/plan.rb +0 -165
- data/lib/carry_out/plan_node.rb +0 -68
- data/lib/carry_out/reference.rb +0 -22
- data/lib/carry_out/unit_error.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85928342e0fda93fadd48a0609af90372244b406
|
4
|
+
data.tar.gz: 50a55f7b851ae3363d1f8ff3a9bccae4720c0963
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb251b2ce082f94ede7d254a57a6973a1f609c775951e1390dc8105fa14d007bb17a54c7849f51f8f540e9cf293e556137a167d6c9f0dc656f20f9d545d6c122
|
7
|
+
data.tar.gz: 84f8722a4a273fd1b9ad0e5ce15fb608ba1f66a99681cc2aad81be3c2dee41c5657a313639d3f80fad1ede719c8825219303ce89d3a465453e58987d274a52c5
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# CarryOut
|
2
2
|
|
3
|
-
CarryOut
|
3
|
+
CarryOut runs isolated units of logic in a series. Each unit can extend the DSL with methods for passing input parameters. Artifacts and errors are collected as the series executes and are returned in a result bundle upon completion.
|
4
4
|
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/carry_out.svg)](https://badge.fury.io/rb/carry_out) [![Build Status](https://travis-ci.org/ryanfields/carry_out.svg?branch=master)](https://travis-ci.org/ryanfields/carry_out) [![Coverage Status](https://coveralls.io/repos/github/ryanfields/carry_out/badge.svg?branch=master)](https://coveralls.io/github/ryanfields/carry_out?branch=master) [![Code Climate](https://codeclimate.com/github/ryanfields/carry_out/badges/gpa.svg)](https://codeclimate.com/github/ryanfields/carry_out)
|
6
6
|
|
@@ -25,7 +25,7 @@ Or install it yourself as:
|
|
25
25
|
Execution units extend CarryOut::Unit and should implement ```CarryOut::Unit#execute(result)```.
|
26
26
|
```ruby
|
27
27
|
class SayHello < CarryOut::Unit
|
28
|
-
def
|
28
|
+
def call
|
29
29
|
puts "Hello, World!"
|
30
30
|
end
|
31
31
|
end
|
@@ -33,16 +33,18 @@ end
|
|
33
33
|
|
34
34
|
CarryOut can then be used to create an execution plan using the unit.
|
35
35
|
```ruby
|
36
|
-
plan = CarryOut.
|
36
|
+
plan = CarryOut.plan do
|
37
|
+
call SayHello
|
38
|
+
end
|
37
39
|
```
|
38
40
|
|
39
41
|
Run the plan using:
|
40
42
|
```
|
41
|
-
result = plan.
|
43
|
+
result = plan.call
|
42
44
|
```
|
43
45
|
|
44
46
|
### Parameters
|
45
|
-
Execution units can be passed parameters statically during plan creation, or dynamically via a block.
|
47
|
+
Execution units can be passed parameters statically during plan creation, or dynamically via a block. There is also a special `context` method that will be explained futher down in this document.
|
46
48
|
|
47
49
|
#### parameter
|
48
50
|
|
@@ -51,7 +53,7 @@ Redefine the example above to greet someone by name:
|
|
51
53
|
class SayHello < CarryOut::Unit
|
52
54
|
parameter :to, :name
|
53
55
|
|
54
|
-
def
|
56
|
+
def call
|
55
57
|
puts "Hello, #{@name}!"
|
56
58
|
end
|
57
59
|
end
|
@@ -59,15 +61,19 @@ end
|
|
59
61
|
|
60
62
|
Define the plan as:
|
61
63
|
```ruby
|
62
|
-
plan = CarryOut
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
plan = CarryOut.plan do
|
65
|
+
call SayHello do
|
66
|
+
to "World"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
66
70
|
# or
|
67
71
|
|
68
|
-
plan = CarryOut
|
69
|
-
|
70
|
-
|
72
|
+
plan = CarryOut do
|
73
|
+
call SayHello do
|
74
|
+
to { "World" }
|
75
|
+
end
|
76
|
+
end
|
71
77
|
```
|
72
78
|
|
73
79
|
And execute the same way as above.
|
@@ -79,18 +85,19 @@ Appending parameters will convert the value of an existing parameter to an array
|
|
79
85
|
```ruby
|
80
86
|
class SayHello < CarryOut::Unit
|
81
87
|
parameter :to, :names
|
82
|
-
appending_parameter :
|
88
|
+
appending_parameter :and_to, :names
|
83
89
|
|
84
|
-
def
|
90
|
+
def call
|
85
91
|
puts "Hello, #{@names}.join(", ")}!"
|
86
92
|
end
|
87
93
|
end
|
88
94
|
|
89
|
-
plan = CarryOut
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
95
|
+
plan = CarryOut.plan do
|
96
|
+
call SayHello do
|
97
|
+
to "John"
|
98
|
+
and_to "Jane"
|
99
|
+
end
|
100
|
+
end
|
94
101
|
```
|
95
102
|
|
96
103
|
Unlike `parameter`, `appending_parameter` must provide both a method name and an instance variable name.
|
@@ -105,218 +112,148 @@ A unit may wish to provide the syntactic sugar while ensuring the underlying ins
|
|
105
112
|
|
106
113
|
Plan executions return a `CarryOut::Result` object that contains any artifacts returned by units (in `Result#artifacts`), along with any errors raised (in `Result#errors`). If `errors` is empty, `Result#success?` will return `true`.
|
107
114
|
|
108
|
-
|
115
|
+
The result context can be accessed via the `context` method when creating a plan.
|
109
116
|
|
110
117
|
```ruby
|
111
118
|
class AddToCart < CarryOut::Unit
|
112
119
|
parameter :items
|
113
120
|
|
114
|
-
def
|
115
|
-
result.add :contents, @items
|
116
|
-
end
|
121
|
+
def call; @items; end
|
117
122
|
end
|
118
123
|
|
119
124
|
class CalculateSubtotal < CarryOut::Unit
|
120
125
|
parameters :items
|
121
126
|
|
122
|
-
def
|
123
|
-
|
124
|
-
result.add :subtotal, subtotal
|
127
|
+
def call
|
128
|
+
items.inject { |sum, item| sum + item.price }
|
125
129
|
end
|
126
130
|
end
|
127
131
|
```
|
128
132
|
```ruby
|
129
|
-
plan = CarryOut
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
133
|
+
plan = CarryOut.plan do
|
134
|
+
call AddToCart do
|
135
|
+
items [ item1, item2, item3 ]
|
136
|
+
return_as :cart
|
137
|
+
end
|
138
|
+
|
139
|
+
then_call CalculateSubtotal do
|
140
|
+
items context(:cart)
|
141
|
+
# or: items { context(:cart) }
|
142
|
+
return_as :subtotal
|
143
|
+
end
|
138
144
|
end
|
145
|
+
|
146
|
+
result = plan.call
|
147
|
+
puts "Subtotal: #{result.artifacts[:subtotal]}"
|
139
148
|
```
|
140
149
|
|
141
150
|
### Initial Artifacts
|
142
151
|
|
143
|
-
`Plan#
|
152
|
+
`Plan#call` accepts a hash that will seed the initial result context.
|
144
153
|
|
145
154
|
```ruby
|
146
|
-
plan = CarryOut
|
147
|
-
|
148
|
-
|
155
|
+
plan = CarryOut.plan do
|
156
|
+
call AddToCart do
|
157
|
+
items context(:items)
|
158
|
+
end
|
159
|
+
end
|
149
160
|
|
150
|
-
plan.
|
161
|
+
plan.call(items: [ item1, item2, item3 ])
|
151
162
|
```
|
152
163
|
|
153
|
-
###
|
164
|
+
### Altering a returned value
|
154
165
|
|
155
|
-
|
156
|
-
|
157
|
-
If `Plan#execute` is passed an initial artifact hash, and a wrapper injects an artifact hash, the two will be merged. The wrapper hash will get priority.
|
166
|
+
It should be considered preferable to encapsulate all logic inside units and always append to the context. However, it may be more pragmatic in some circumstances to make minor changes to a returned value as it is being returned. This can be achieved by providing a block to `return_as`.
|
158
167
|
|
159
168
|
```ruby
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
def execute
|
166
|
-
File.open(@file_path, "r") do |f|
|
167
|
-
yield file: f
|
168
|
-
end
|
169
|
+
plan = CarryOut.plan do
|
170
|
+
call EchoName do
|
171
|
+
name 'john'
|
172
|
+
return_as (:name) { |result| result.capitalize }
|
169
173
|
end
|
170
174
|
end
|
171
|
-
|
172
|
-
plan = CarryOut
|
173
|
-
.within(FileContext.new("path/to/file")) # Expects instance, not class
|
174
|
-
.will(DoAThing)
|
175
|
-
.with_file(CarryOut.get(:file))
|
176
175
|
```
|
177
176
|
|
178
|
-
The wrapping context can also be a block.
|
179
|
-
|
180
|
-
```ruby
|
181
|
-
plan = CarryOut
|
182
|
-
.within { |proc| ActiveRecordBase.transaction { proc.call } }
|
183
|
-
.will(CreateModel)
|
184
|
-
```
|
185
|
-
|
186
|
-
When using a block, `proc.call` can be used to seed the references hash in the same manner as `yield` in the first example.
|
187
|
-
|
188
|
-
Wrapper contexts will always be applied to an entire plan. If a plan has multiple phases that need to be wrapped in different contexts, it is better to create multiple plans and embed them together in a larger plan as shown below.
|
189
|
-
|
190
177
|
### Embedding Plans
|
191
178
|
|
192
|
-
A plan can be used in place of a `CarryOut::Unit`. This allows plans to be reused as part of larger
|
179
|
+
A plan can be used in place of a `CarryOut::Unit`. This allows plans to be reused as part of larger series. Compositing plans can also help when dealing with optional series.
|
193
180
|
|
194
181
|
```ruby
|
195
|
-
say_hello = CarryOut.
|
182
|
+
say_hello = CarryOut.plan { call SayHello }
|
196
183
|
|
197
|
-
plan = CarryOut
|
198
|
-
|
199
|
-
|
184
|
+
plan = CarryOut do
|
185
|
+
call DisplayBanner
|
186
|
+
then_call SayHello
|
187
|
+
end
|
200
188
|
```
|
201
189
|
|
202
|
-
Passing a plan to
|
190
|
+
Passing a plan to `call` works similar to passing a `CarryOut::Unit` class or instance. A block can be included in order to specify a `return_as` directive. The resulting artifact hash will be stored under the name given to `return_as`.
|
203
191
|
|
204
|
-
|
192
|
+
An embedded plan will receive the current result context as its initial context.
|
193
|
+
|
194
|
+
**Caveat**
|
195
|
+
Errors for embedded plans will be stored at the top level of `Result#errors`. The `return_as` label for embedded plans is not factored into the label path for errors. As a result, it can be tricky to determine whether an error was set by the outer plan or an embedded plan. This is a known bug and will be fixed in a future release.
|
205
196
|
|
206
197
|
### Conditional Units
|
207
198
|
|
208
|
-
Use the `
|
199
|
+
Use the `only_when` or `except_when` directives to conditionally execute a unit.
|
209
200
|
|
210
201
|
```ruby
|
211
|
-
plan = CarryOut
|
212
|
-
|
213
|
-
|
214
|
-
|
202
|
+
plan = CarryOut.plan do
|
203
|
+
call SayHello
|
204
|
+
only_when context(:audible)
|
205
|
+
end
|
206
|
+
end
|
215
207
|
```
|
216
208
|
|
217
209
|
```ruby
|
218
|
-
plan = CarryOut
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
```
|
223
|
-
|
224
|
-
### Magic Directives (Experimental)
|
225
|
-
|
226
|
-
*This feature is highly experimental. It has not been thoroughly tested in larger application environments like Rails and is not yet guaranteed to remain part of this gem.*
|
227
|
-
|
228
|
-
CarryOut provides some magic methods that can improve the readability of a plan. These rely on a search strategy to find classes by name. A very limited strategy is provided out-of-the-box. This strategy accepts an array of modules and will only find classes that are direct children of any of the provided modules. The first match gets priority.
|
229
|
-
|
230
|
-
```ruby
|
231
|
-
CarryOut.defaults = {
|
232
|
-
search: [ MyModule1 ]
|
233
|
-
}
|
210
|
+
plan = CarryOut.plan do
|
211
|
+
call SayHello
|
212
|
+
except_when context(:silenced)
|
213
|
+
end
|
234
214
|
```
|
235
215
|
|
236
|
-
|
216
|
+
These directives can be given blocks if more complex conditional logic is needed. As with parameter blocks, the `context` method is available inside the block.
|
237
217
|
|
238
|
-
|
239
|
-
CarryOut.defaults = {
|
240
|
-
search: -> (name) { name.constantize }
|
241
|
-
}
|
242
|
-
```
|
218
|
+
### Magic Unit Methods
|
243
219
|
|
244
|
-
|
220
|
+
CarryOut provides some magic to translate unit classes into method names that can replace the `call Class` syntax. This feature relies on a search strategy to find classes by name. A very limited strategy is provided out-of-the-box. This strategy accepts an array of modules and will only find classes that are direct children of any of the provided modules. The first match gets priority.
|
245
221
|
|
246
|
-
|
222
|
+
Assuming `MyModule1` contains definitions for units `DisplayBanner` and `SayHello`:
|
247
223
|
|
248
|
-
Using the default strategy as configured above:
|
249
224
|
```ruby
|
250
|
-
|
251
|
-
|
252
|
-
def execute; puts "Hello!"; end
|
253
|
-
end
|
225
|
+
CarryOut.configure do
|
226
|
+
search [ MyModule1 ]
|
254
227
|
end
|
255
228
|
|
256
|
-
plan = CarryOut.
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
The magic `returning_as_` directive is an alternative to passing the `as:` option to a `will`/`then` directive. The remainder of the directive becomes the key symbol into which the unit's return value will be stored.
|
262
|
-
|
263
|
-
```ruby
|
264
|
-
plan = CarryOut
|
265
|
-
.will_receive_message
|
266
|
-
.returning_as_message
|
267
|
-
.then_log
|
268
|
-
.message(CarryOut.get(:message))
|
229
|
+
plan = CarryOut.plan do
|
230
|
+
display_banner { with_text "This is my banner." }
|
231
|
+
say_hello { to "World" }
|
232
|
+
end
|
269
233
|
```
|
270
234
|
|
271
|
-
|
272
|
-
|
273
|
-
The magic `result_of_` directive is available within blocks passed to parameter methods. The remainder of the directive becomes the context key symbol from which the value will be retreived.
|
235
|
+
If the default strategy is insufficient (and it most likely will be), a custom strategy can be provided as a lambda/Proc. For example, a strategy that works in Rails is to put the following in an initializer:
|
274
236
|
|
275
237
|
```ruby
|
276
|
-
|
277
|
-
.
|
278
|
-
|
279
|
-
.then_log
|
280
|
-
.message { result_of_message }
|
281
|
-
# instead of .message(CarryOut.get(:message)
|
282
|
-
# or .message { |refs| refs.message }
|
238
|
+
CarryOut.configure do
|
239
|
+
search -> (name) { name.constantize }
|
240
|
+
end
|
283
241
|
```
|
284
242
|
|
285
|
-
|
243
|
+
## Configuration
|
244
|
+
The CarryOut global can be configured using `CarryOut#configure`. It accepts a block containing configuration directives. At the moment, the only directive is the `search` option described above.
|
286
245
|
|
287
|
-
|
288
|
-
|
289
|
-
```ruby
|
290
|
-
plan = CarryOut
|
291
|
-
.within_order_transaction
|
292
|
-
.will_order_bagel
|
293
|
-
.flavored('everything')
|
294
|
-
.toasted
|
295
|
-
.topped_with('butter')
|
296
|
-
.and('strawberry cream cheese')
|
297
|
-
.returning_as_bagel
|
298
|
-
.then_order_coffee
|
299
|
-
.with_cream
|
300
|
-
.and_sugar
|
301
|
-
.returning_as_coffee
|
302
|
-
.then_calculate_order_total
|
303
|
-
.for { result_of_bagel }
|
304
|
-
.and { result_of_coffee }
|
305
|
-
.then_swipe_credit_card
|
306
|
-
.returning_as_cc
|
307
|
-
.then_pay
|
308
|
-
.with_credit_card { result_of_cc }
|
309
|
-
```
|
246
|
+
If more than one configuration of CarryOut is needed, the `CarryOut#with_configuration` method can be used to obtain a configured instance of CarryOut. At the moment, this method accepts a hash of configuration options. *This will change in a future release, in which this method will be called just like the configure method.* This method returns an instance that operates just like the CarryOut global, but uses the provided configuration options when creating and running plans.
|
310
247
|
|
311
248
|
## Motivation
|
312
249
|
|
313
250
|
I've been trying to keep my Rails controllers clean, but I prefer to avoid shoving inter-model business logic inside database models. The recommendation I most frequently run into is to move that kind of logic into something akin to service objects. I like that idea, but I want to keep my services small and composable, and I want to separate the "what" from the "how" of my logic.
|
314
251
|
|
315
|
-
CarryOut is designed to be a consistent layer of glue between single-purpose or "simple-purpose" units of business logic. CarryOut describes what needs to be done and
|
252
|
+
CarryOut is designed to be a consistent layer of glue between single-purpose or "simple-purpose" units of business logic. CarryOut describes what needs to be done and which inputs are to be used. The units themselves worry about how to perform the actual work. These units tend to have names that describe their intent. They remain small and easier to test in isolation. What ends up in my controllers is a process description that that can be comprehended at a glance and remains fairly agnostic to the underlying details of my chosen ORM, job queue, message queue, etc.
|
316
253
|
|
317
|
-
I'm building up CarryOut alongside a new Rails application, but my intent is for CarryOut to remain just as useful outside of Rails. At present, it
|
254
|
+
I'm building up CarryOut alongside a new Rails application, but my intent is for CarryOut to remain just as useful outside of Rails. At present, it is not bound in any way to ActiveRecord. If those sorts of bindings emerge, I intend to provide an add-on gem or an alternate require.
|
318
255
|
|
319
|
-
CarryOut
|
256
|
+
A CarryOut series is synchronous. Support for asynchronous execution is desired, but not yet planned for a future release. A series can not loop. Branching is achievable in a round-about way through the `only_when` and `except_when` conditionals, but this becomes hard to follow in complex plans. If you find frequent need of complex branching and looping, a full workflow engine might be a better choice than CarryOut.
|
320
257
|
|
321
258
|
## Development
|
322
259
|
|
data/carry_out.gemspec
CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_development_dependency "bundler", "~> 1.12"
|
30
30
|
spec.add_development_dependency "rake", "~> 10.0"
|
31
31
|
spec.add_development_dependency "minitest", "~> 5.0"
|
32
|
+
spec.add_development_dependency "simplecov", "~> 0.12.0"
|
32
33
|
spec.add_development_dependency "coveralls", "~> 0.8"
|
33
34
|
|
34
35
|
if RUBY_VERSION < '2.0'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'carry_out/plan/guard_context'
|
2
|
+
|
3
|
+
module CarryOut
|
4
|
+
module Plan
|
5
|
+
class Guard
|
6
|
+
def initialize(proc, options = {})
|
7
|
+
@proc = proc
|
8
|
+
invert(options[:invert])
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(context = {})
|
12
|
+
result = GuardContext.new(context).instance_exec(context, &@proc)
|
13
|
+
result = !result if @invert
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
def invert(is_inverted = true)
|
18
|
+
@invert = !!is_inverted
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "carry_out/plan/guard"
|
2
|
+
require "carry_out/plan/node_result"
|
3
|
+
|
4
|
+
module CarryOut
|
5
|
+
module Plan
|
6
|
+
class Node
|
7
|
+
attr_accessor :action
|
8
|
+
attr_accessor :connects_to
|
9
|
+
attr_reader :guarded_by
|
10
|
+
attr_accessor :returns_as
|
11
|
+
attr_accessor :return_transform
|
12
|
+
|
13
|
+
def initialize(action = nil)
|
14
|
+
@action = action
|
15
|
+
@messages = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(context = {})
|
19
|
+
return NodeResult.new unless @action
|
20
|
+
return unless guard(context)
|
21
|
+
|
22
|
+
result = @action.call do |a|
|
23
|
+
@messages.map do |m|
|
24
|
+
value = m[:source]
|
25
|
+
|
26
|
+
if value.respond_to?(:call)
|
27
|
+
value = GuardContext.new(context).instance_exec(context, &value)
|
28
|
+
end
|
29
|
+
|
30
|
+
a.send(m[:method], value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
result = return_transform.call(result) unless return_transform.nil?
|
35
|
+
|
36
|
+
NodeResult.new(result)
|
37
|
+
end
|
38
|
+
|
39
|
+
def guard_with(guard)
|
40
|
+
guard = guard.respond_to?(:call) ? guard : Proc.new { guard }
|
41
|
+
guarded_by.push Guard.new(guard)
|
42
|
+
end
|
43
|
+
|
44
|
+
def guard_with_inverse(guard)
|
45
|
+
guard_with(guard)
|
46
|
+
guarded_by.last.invert
|
47
|
+
end
|
48
|
+
|
49
|
+
def guarded_by
|
50
|
+
@guarded_by ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def method_missing(method, *args, &block)
|
54
|
+
if respond_to?(method)
|
55
|
+
@messages.push({ method: method, source: block || (args.length == 0 ? true : args.first) })
|
56
|
+
else
|
57
|
+
raise NoMethodError, "undefined method `#{method}' for #{@action}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def respond_to?(method, private = false)
|
62
|
+
(@action && @action.respond_to?(:has_parameter?) && @action.has_parameter?(method)) || super
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def guard(context = {})
|
67
|
+
guarded_by.empty? || guarded_by.all? { |g| g.call(context) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CarryOut
|
2
|
+
module Plan
|
3
|
+
class NodeContext
|
4
|
+
def initialize(node)
|
5
|
+
@node = node
|
6
|
+
end
|
7
|
+
|
8
|
+
def action
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def context(*args)
|
13
|
+
-> (context) do
|
14
|
+
args.inject(context) { |c, k| c.nil? ? nil : c[k] }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def only_when(value = nil, &block)
|
19
|
+
@node.guard_with(value || block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def except_when(value = nil, &block)
|
23
|
+
@node.guard_with_inverse(value || block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method, *args, &block)
|
27
|
+
@node.send(method, *args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def respond_to?(method)
|
31
|
+
@node.respond_to?(method)
|
32
|
+
end
|
33
|
+
|
34
|
+
def return_as(key, &block)
|
35
|
+
@node.returns_as = key
|
36
|
+
@node.return_transform = block unless block.nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require "carry_out/plan/node"
|
2
|
+
require "carry_out/plan/node_context"
|
3
|
+
|
4
|
+
module CarryOut
|
5
|
+
class PlanBuilder
|
6
|
+
def initialize(options = {}, &block)
|
7
|
+
@plan = Plan::Node.new
|
8
|
+
@wrapper = nil
|
9
|
+
@constant_resolver = [ options[:search] ].flatten(1)
|
10
|
+
|
11
|
+
configure_node(@plan, &block) if block
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :plan
|
15
|
+
|
16
|
+
def self.build(options = {}, &block)
|
17
|
+
builder = PlanBuilder.new(options)
|
18
|
+
builder.instance_eval(&block)
|
19
|
+
builder.plan
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(unit = nil, &block)
|
23
|
+
unit = find_object(unit) if unit.is_a?(Symbol) || unit.is_a?(String)
|
24
|
+
node = Plan::Node.new(unit)
|
25
|
+
|
26
|
+
configure_node(node, &block) if block
|
27
|
+
current_node.connects_to = node
|
28
|
+
self.current_node = node
|
29
|
+
|
30
|
+
node
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :then_call, :call
|
34
|
+
|
35
|
+
def method_missing(method, *args, &block)
|
36
|
+
obj = find_object(method)
|
37
|
+
|
38
|
+
if obj
|
39
|
+
call(method, &block)
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
attr_writer :current_node
|
47
|
+
|
48
|
+
def configure_node(node, &block)
|
49
|
+
Plan::NodeContext.new(node).instance_eval(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_node
|
53
|
+
@current_node ||= @plan
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_object(name)
|
57
|
+
constant_name = name.to_s.split('_').map { |w| w.capitalize }.join('')
|
58
|
+
|
59
|
+
@constant_resolver.inject(nil) do |obj, m|
|
60
|
+
return obj if obj
|
61
|
+
|
62
|
+
if m.respond_to?(:call)
|
63
|
+
m.call(constant_name)
|
64
|
+
else
|
65
|
+
m.const_get(constant_name) rescue nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CarryOut
|
2
|
+
class PlanRunner
|
3
|
+
def self.run(plan, context = {})
|
4
|
+
PlanRunner.new.run(plan, context)
|
5
|
+
end
|
6
|
+
|
7
|
+
def run(plan, context = {})
|
8
|
+
Result.new(context).tap do |plan_result|
|
9
|
+
node = plan
|
10
|
+
|
11
|
+
until node.nil?
|
12
|
+
node_result = nil
|
13
|
+
|
14
|
+
begin
|
15
|
+
node_result = node.call(plan_result.artifacts)
|
16
|
+
|
17
|
+
if node_result.kind_of?(Plan::NodeResult) && node.returns_as
|
18
|
+
plan_result.add(node.returns_as, node_result.value)
|
19
|
+
|
20
|
+
if node_result.value.kind_of?(CarryOut::Result) && !node_result.value.success?
|
21
|
+
break
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
node = node.connects_to
|
26
|
+
rescue StandardError => error
|
27
|
+
plan_result.add (node.returns_as || :base), CarryOut::Error.new(error.message, error)
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/carry_out/result.rb
CHANGED
@@ -10,6 +10,10 @@ module CarryOut
|
|
10
10
|
add_error(group, object)
|
11
11
|
elsif object.kind_of?(Result)
|
12
12
|
add(group, object.to_hash)
|
13
|
+
|
14
|
+
object.errors.each do |g, errors|
|
15
|
+
errors.each { |e| add(g,e) }
|
16
|
+
end
|
13
17
|
elsif object.kind_of?(Hash)
|
14
18
|
artifacts[group] ||= {}
|
15
19
|
object.each { |k,v| artifacts[group][k] = v }
|
data/lib/carry_out/unit.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
module CarryOut
|
2
2
|
class Unit
|
3
3
|
|
4
|
-
def
|
4
|
+
def call
|
5
|
+
raise "Expected #{self.class} to define #{self.class}#call" unless self.class == Unit
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.call(&block)
|
9
|
+
unit = self.new
|
10
|
+
yield unit if block
|
11
|
+
unit.call
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.has_parameter?(method)
|
15
|
+
self.instance_methods.include?(method)
|
5
16
|
end
|
6
17
|
|
7
18
|
def self.appending_parameter(method_name, var)
|
data/lib/carry_out/version.rb
CHANGED
data/lib/carry_out.rb
CHANGED
@@ -1,69 +1,40 @@
|
|
1
|
-
require
|
1
|
+
require 'carry_out/version'
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require "carry_out/reference"
|
8
|
-
require "carry_out/result"
|
9
|
-
require "carry_out/unit"
|
10
|
-
require "carry_out/unit_error"
|
3
|
+
require 'carry_out/configurator'
|
4
|
+
require 'carry_out/error'
|
5
|
+
require 'carry_out/result'
|
6
|
+
require 'carry_out/unit'
|
11
7
|
|
12
|
-
|
13
|
-
|
14
|
-
MATCH_WITHIN_METHOD = /^within_/
|
8
|
+
require 'carry_out/plan_builder'
|
9
|
+
require 'carry_out/plan_runner'
|
15
10
|
|
16
|
-
|
11
|
+
module CarryOut
|
12
|
+
class ConfiguredInstance
|
17
13
|
def initialize(options = {})
|
18
|
-
@
|
19
|
-
|
20
|
-
|
21
|
-
def get(*args)
|
22
|
-
Reference.new(*args)
|
23
|
-
end
|
24
|
-
|
25
|
-
def method_missing(method, *args, &block)
|
26
|
-
if MATCH_CONTINUATION_METHOD =~ method
|
27
|
-
create_plan.send(method, *args, &block)
|
28
|
-
elsif MATCH_WITHIN_METHOD =~ method
|
29
|
-
create_plan.send(method, *args, &block)
|
30
|
-
else
|
31
|
-
super
|
32
|
-
end
|
14
|
+
@options = Hash.new
|
15
|
+
@options[:search] = options[:search] if options.has_key?(:search)
|
33
16
|
end
|
34
17
|
|
35
|
-
def
|
36
|
-
|
18
|
+
def plan(options = {}, &block)
|
19
|
+
CarryOut.plan(Hash.new.merge(@options).merge(options), &block)
|
37
20
|
end
|
38
|
-
|
39
|
-
def within(wrapper = nil, &block)
|
40
|
-
create_plan(within: wrapper || block)
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
def create_plan(options = {})
|
45
|
-
Plan.new(nil, @config.merge(options))
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.configured_with(options = {})
|
50
|
-
ConfiguredCarryOut.new(options)
|
51
21
|
end
|
52
22
|
|
53
|
-
def self.
|
54
|
-
|
55
|
-
@default_carry_out = nil
|
23
|
+
def self.configure(&block)
|
24
|
+
Configurator.new(configuration).instance_eval(&block)
|
56
25
|
end
|
57
26
|
|
58
|
-
def self.
|
59
|
-
|
27
|
+
def self.plan(options = {}, &block)
|
28
|
+
merged_options = Hash.new.merge(configuration).merge(options)
|
29
|
+
plan = PlanBuilder.build(merged_options, &block)
|
30
|
+
-> (context = nil) { PlanRunner.run(plan, context) }
|
60
31
|
end
|
61
32
|
|
62
|
-
def self.
|
63
|
-
@
|
33
|
+
def self.configuration
|
34
|
+
@configuration ||= {}
|
64
35
|
end
|
65
36
|
|
66
|
-
def self.
|
67
|
-
|
37
|
+
def self.with_configuration(options = {})
|
38
|
+
ConfiguredInstance.new(options)
|
68
39
|
end
|
69
40
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: carry_out
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Fields
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.12.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.12.0
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: coveralls
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,14 +102,17 @@ files:
|
|
88
102
|
- bin/setup
|
89
103
|
- carry_out.gemspec
|
90
104
|
- lib/carry_out.rb
|
91
|
-
- lib/carry_out/
|
105
|
+
- lib/carry_out/configurator.rb
|
92
106
|
- lib/carry_out/error.rb
|
93
|
-
- lib/carry_out/plan.rb
|
94
|
-
- lib/carry_out/
|
95
|
-
- lib/carry_out/
|
107
|
+
- lib/carry_out/plan/guard.rb
|
108
|
+
- lib/carry_out/plan/guard_context.rb
|
109
|
+
- lib/carry_out/plan/node.rb
|
110
|
+
- lib/carry_out/plan/node_context.rb
|
111
|
+
- lib/carry_out/plan/node_result.rb
|
112
|
+
- lib/carry_out/plan_builder.rb
|
113
|
+
- lib/carry_out/plan_runner.rb
|
96
114
|
- lib/carry_out/result.rb
|
97
115
|
- lib/carry_out/unit.rb
|
98
|
-
- lib/carry_out/unit_error.rb
|
99
116
|
- lib/carry_out/version.rb
|
100
117
|
homepage: https://github.com/ryanfields/carry_out
|
101
118
|
licenses:
|
data/lib/carry_out/context.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
module CarryOut
|
2
|
-
class Context
|
3
|
-
MATCH_RESULT_METHOD = /^result_of_(.+)/
|
4
|
-
|
5
|
-
def initialize(context)
|
6
|
-
@context = context
|
7
|
-
end
|
8
|
-
|
9
|
-
def method_missing(method, *args, &block)
|
10
|
-
if MATCH_RESULT_METHOD =~ method && args.empty? && block.nil?
|
11
|
-
@context[$1.to_sym]
|
12
|
-
else
|
13
|
-
super
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
data/lib/carry_out/plan.rb
DELETED
@@ -1,165 +0,0 @@
|
|
1
|
-
module CarryOut
|
2
|
-
class Plan
|
3
|
-
MATCH_CONTINUATION_METHOD = /^(?:will|then)_(.+)/
|
4
|
-
MATCH_CONTEXT_METHOD = /^within_(.+)/
|
5
|
-
MATCH_RETURNING_METHOD = /^returning_as_(.+)/
|
6
|
-
|
7
|
-
def initialize(unit = nil, options = {})
|
8
|
-
@nodes = {}
|
9
|
-
@node_meta = {}
|
10
|
-
@previously_added_node = nil
|
11
|
-
@wrapper = options[:within]
|
12
|
-
@search = options[:search] || []
|
13
|
-
|
14
|
-
self.then(unit, options) unless unit.nil?
|
15
|
-
end
|
16
|
-
|
17
|
-
def execute(context = nil, &block)
|
18
|
-
if @wrapper
|
19
|
-
if @wrapper.respond_to?(:execute)
|
20
|
-
@wrapper.execute do |wrapper_context|
|
21
|
-
execute_internal(Result.new(context, wrapper_context), &block)
|
22
|
-
end
|
23
|
-
else
|
24
|
-
@wrapper.call(Proc.new { |wrapper_context|
|
25
|
-
execute_internal(Result.new(context, wrapper_context), &block)
|
26
|
-
})
|
27
|
-
end
|
28
|
-
else
|
29
|
-
execute_internal(Result.new(context), &block)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def if(reference = nil, &block)
|
34
|
-
raise NoMethodError("Conditional execution must be applied to a unit") unless @previously_added_node
|
35
|
-
|
36
|
-
guards = node_meta(@previously_added_node)[:guards] ||= []
|
37
|
-
guards << (reference || block)
|
38
|
-
|
39
|
-
self
|
40
|
-
end
|
41
|
-
|
42
|
-
def will(*args)
|
43
|
-
self.then(*args)
|
44
|
-
end
|
45
|
-
|
46
|
-
def then(unit = nil, options = {})
|
47
|
-
add_node(PlanNode.new(unit), options[:as]) unless unit.nil?
|
48
|
-
self
|
49
|
-
end
|
50
|
-
|
51
|
-
def unless(reference = nil, &block)
|
52
|
-
self.if { |refs| !self.instance_exec(refs, &(reference || block)) }
|
53
|
-
|
54
|
-
self
|
55
|
-
end
|
56
|
-
|
57
|
-
def method_missing(method, *args, &block)
|
58
|
-
if MATCH_CONTINUATION_METHOD =~ method
|
59
|
-
obj = find_object($1)
|
60
|
-
return super if obj.nil?
|
61
|
-
self.then(obj, *args, &block)
|
62
|
-
elsif MATCH_CONTEXT_METHOD =~ method
|
63
|
-
obj = find_object($1)
|
64
|
-
return super if obj.nil?
|
65
|
-
@wrapper = obj.new
|
66
|
-
self
|
67
|
-
elsif @previously_added_node
|
68
|
-
if MATCH_RETURNING_METHOD =~ method
|
69
|
-
node_meta(@previously_added_node)[:as] = $1.to_sym
|
70
|
-
else
|
71
|
-
@previously_added_node.send(method, *args, &block)
|
72
|
-
end
|
73
|
-
self
|
74
|
-
else
|
75
|
-
super
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def respond_to?(method)
|
80
|
-
(@previously_added_node && @previously_added_node.respond_to?(method)) || super
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
84
|
-
def add_node(node, as = nil)
|
85
|
-
id = generate_node_name
|
86
|
-
|
87
|
-
if @previously_added_node.nil?
|
88
|
-
@initial_node_key = id
|
89
|
-
else
|
90
|
-
@previously_added_node.next = id
|
91
|
-
end
|
92
|
-
|
93
|
-
@nodes[id] = node
|
94
|
-
@node_meta[id] = { as: as }
|
95
|
-
@previously_added_node = node
|
96
|
-
|
97
|
-
id
|
98
|
-
end
|
99
|
-
|
100
|
-
def execute_internal(result = Result.new, &block)
|
101
|
-
id = @initial_node_key
|
102
|
-
|
103
|
-
until (node = @nodes[id]).nil? do
|
104
|
-
execute_node(node, result) if guard_node(node, result.artifacts)
|
105
|
-
break unless result.success?
|
106
|
-
id = node.next
|
107
|
-
end
|
108
|
-
|
109
|
-
unless block.nil?
|
110
|
-
block.call(result)
|
111
|
-
end
|
112
|
-
|
113
|
-
result
|
114
|
-
end
|
115
|
-
|
116
|
-
def execute_node(node, result)
|
117
|
-
meta = node_meta(node)
|
118
|
-
publish_to = meta[:as]
|
119
|
-
|
120
|
-
begin
|
121
|
-
node_result = node.execute(result.artifacts)
|
122
|
-
result.add(publish_to, node_result) unless publish_to.nil?
|
123
|
-
rescue UnitError => error
|
124
|
-
result.add(publish_to || key_for_node(node), CarryOut::Error.new(error.error.message, error.error))
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
def find_object(name)
|
129
|
-
constant_name = name.to_s.split('_').map { |w| w.capitalize }.join('')
|
130
|
-
|
131
|
-
if @search.respond_to?(:call)
|
132
|
-
@search.call(constant_name)
|
133
|
-
else
|
134
|
-
containing_module = @search.find { |m| m.const_get(constant_name) rescue nil }
|
135
|
-
containing_module.const_get(constant_name) unless containing_module.nil?
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def generate_node_name
|
140
|
-
id = @next_node_id ||= 1
|
141
|
-
@next_node_id += 1
|
142
|
-
"node_#{id}".to_sym
|
143
|
-
end
|
144
|
-
|
145
|
-
def guard_node(node, artifacts)
|
146
|
-
context = Context.new(artifacts)
|
147
|
-
guards = node_meta(node)[:guards]
|
148
|
-
guards.nil? || guards.map do |guard|
|
149
|
-
if guard.kind_of?(CarryOut::Reference)
|
150
|
-
reference_proc = -> (refs) { guard.call(refs) }
|
151
|
-
end
|
152
|
-
|
153
|
-
context.instance_exec(artifacts, &(reference_proc || guard))
|
154
|
-
end.all?
|
155
|
-
end
|
156
|
-
|
157
|
-
def key_for_node(node)
|
158
|
-
@nodes.key(node)
|
159
|
-
end
|
160
|
-
|
161
|
-
def node_meta(node)
|
162
|
-
@node_meta[key_for_node(node)]
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
data/lib/carry_out/plan_node.rb
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
module CarryOut
|
2
|
-
class PlanNode
|
3
|
-
attr_reader :next
|
4
|
-
|
5
|
-
def initialize(klass = nil)
|
6
|
-
@unitClass = klass
|
7
|
-
@messages = []
|
8
|
-
end
|
9
|
-
|
10
|
-
def method_missing(method, *args, &block)
|
11
|
-
if is_parameter_method?(*args, &block)
|
12
|
-
append_message(method, *args, &block)
|
13
|
-
else
|
14
|
-
super
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def respond_to?(method)
|
19
|
-
@unitClass.instance_methods.include?(method) || super
|
20
|
-
end
|
21
|
-
|
22
|
-
def execute(context)
|
23
|
-
unit = @unitClass.respond_to?(:execute) ? @unitClass : @unitClass.new
|
24
|
-
|
25
|
-
@messages.each do |message|
|
26
|
-
arg =
|
27
|
-
if message[:block]
|
28
|
-
Context.new(context).instance_exec(context, &message[:block])
|
29
|
-
else
|
30
|
-
message[:argument]
|
31
|
-
end
|
32
|
-
|
33
|
-
unit.send(message[:method], arg)
|
34
|
-
end
|
35
|
-
|
36
|
-
begin
|
37
|
-
unit.execute
|
38
|
-
rescue StandardError => error
|
39
|
-
raise UnitError.new(error)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def next=(value)
|
44
|
-
@next = value.to_sym
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
def append_message(method, *args, &block)
|
49
|
-
if !args.first.nil? && !block.nil?
|
50
|
-
raise ArgumentError.new("Arguments, references, and blocks are mutually exclusive")
|
51
|
-
end
|
52
|
-
|
53
|
-
if @unitClass.instance_methods.include?(method)
|
54
|
-
if args.first.kind_of?(Reference)
|
55
|
-
@messages << { method: method, block: -> (refs) { args.first.call(refs) } }
|
56
|
-
else
|
57
|
-
@messages << { method: method, argument: args.first || true, block: block }
|
58
|
-
end
|
59
|
-
else
|
60
|
-
raise NoMethodError.new("#{@unitClass} instances do not respond to `#{method}'", method, *args)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def is_parameter_method?(*args, &block)
|
65
|
-
args.length <= 1 || (args.length == 0 && !block.nil?)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
data/lib/carry_out/reference.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
module CarryOut
|
2
|
-
class Reference
|
3
|
-
|
4
|
-
def initialize(*args)
|
5
|
-
@key_path = args
|
6
|
-
end
|
7
|
-
|
8
|
-
def call(context)
|
9
|
-
result = context
|
10
|
-
@key_path.each do |key|
|
11
|
-
if context.respond_to?(:has_key?) && context.has_key?(key)
|
12
|
-
result = context[key]
|
13
|
-
else
|
14
|
-
result = nil
|
15
|
-
break
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
result
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|