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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 978f0d178d3f9163e0c712bbb6d9d15a2dc73331
4
- data.tar.gz: 025b60557fca2bc67a66e2ece60d00482ac66dd5
3
+ metadata.gz: 85928342e0fda93fadd48a0609af90372244b406
4
+ data.tar.gz: 50a55f7b851ae3363d1f8ff3a9bccae4720c0963
5
5
  SHA512:
6
- metadata.gz: 060aff17ad5c6426f1d62fdde246e3360416064e582d5dcadb40d2b49f7b5007259880d56bd690b85106c96b634d614f1d8dcf3c83bafae4b102e095ad90fb20
7
- data.tar.gz: e705734c5a0ddcfec23612af8053bc93ebb5f05c6c72a63defe0043ded55379e398acbfef4cc7f3e4dafb29b6fedd802d01b055117ab8d9019f1b294e344c108
6
+ metadata.gz: bb251b2ce082f94ede7d254a57a6973a1f609c775951e1390dc8105fa14d007bb17a54c7849f51f8f540e9cf293e556137a167d6c9f0dc656f20f9d545d6c122
7
+ data.tar.gz: 84f8722a4a273fd1b9ad0e5ce15fb608ba1f66a99681cc2aad81be3c2dee41c5657a313639d3f80fad1ede719c8825219303ce89d3a465453e58987d274a52c5
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CarryOut
2
2
 
3
- CarryOut connects units of logic into workflows. Each unit can extend the DSL with parameter methods. Artifacts and errors are collected as the workflow executes and are returned in a result bundle upon completion.
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 execute(result)
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.will(SayHello)
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.execute
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 execute(result)
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
- .will(SayHello)
64
- .to("Ryan")
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
- .will(SayHello)
70
- .to { "Ryan" }
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 :and, :names
88
+ appending_parameter :and_to, :names
83
89
 
84
- def execute
90
+ def call
85
91
  puts "Hello, #{@names}.join(", ")}!"
86
92
  end
87
93
  end
88
94
 
89
- plan = CarryOut
90
- .will(SayHello)
91
- .to("John")
92
- .and("Jane")
93
- .and("Ryan")
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
- References via `CarryOut#get` or via blocks can be used to pass result artifacts on to subsequent execution units in the plan.
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 execute(result)
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 execut(result)
123
- subtotal = items.inject { |sum, item| sum + item.price }
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
- .will(AddToCart, as: :cart)
131
- .items([ item1, item2, item3])
132
- .then(CalculateSubtotal, as: :invoice)
133
- .items(CarryOut.get(:cart, :contents)
134
- # or .items { |refs| refs[:cart][:contents] }
135
-
136
- plan.execute do |result|
137
- puts "Subtotal: #{result.artifacts[:invoice][:subtotal]}"
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#execute` accepts a hash that will seed the initial result artifacts.
152
+ `Plan#call` accepts a hash that will seed the initial result context.
144
153
 
145
154
  ```ruby
146
- plan = CarryOut
147
- .will(SayHello)
148
- .to(CarryOut.get(:name))
155
+ plan = CarryOut.plan do
156
+ call AddToCart do
157
+ items context(:items)
158
+ end
159
+ end
149
160
 
150
- plan.execute(name: 'John')
161
+ plan.call(items: [ item1, item2, item3 ])
151
162
  ```
152
163
 
153
- ### Wrapping Execution
164
+ ### Altering a returned value
154
165
 
155
- Plan execution can be wrapped for purposes such as ensuring files get closed or to run the plan inside a database transaction. Wrapping also provides an alternative mechanism for injecting initial artifacts into the plan result.
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
- class FileContext
161
- def initialize(file_path)
162
- @file_path = file_path
163
- end
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 strategies.
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.will(SayHello)
182
+ say_hello = CarryOut.plan { call SayHello }
196
183
 
197
- plan = CarryOut
198
- .will(DisplayBanner)
199
- .then(say_hello)
184
+ plan = CarryOut do
185
+ call DisplayBanner
186
+ then_call SayHello
187
+ end
200
188
  ```
201
189
 
202
- Passing a plan to `#then` works similar to passing a `CarryOut::Unit` class or instance. If the `as` option is added, the inner plan's result artifacts will be added to the outer plan's result at the specified key.
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
- **One caveat to be aware of**: There is no way to specify initial artifacts for an embedded plan. If an embedded plan depends on an external context, `CarryOut#within` is sufficient to work around this limitation. However, there is currently no way for an inner plan to access an outer plan's artifacts. This is considered a bug and will be fixed in a future release.
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 `if` or `unless` directive to conditionally execute a unit.
199
+ Use the `only_when` or `except_when` directives to conditionally execute a unit.
209
200
 
210
201
  ```ruby
211
- plan = CarryOut
212
- .will(SayHello)
213
- .if { |refs| refs[:audible] }
214
- # or .if(CarryOut.get(:audible))
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
- .will(SayHello)
220
- .unless { |refs| refs[:silenced] }
221
- # or .unless(CarryOut.get(:silenced))
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
- 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:
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
- ```ruby
239
- CarryOut.defaults = {
240
- search: -> (name) { name.constantize }
241
- }
242
- ```
218
+ ### Magic Unit Methods
243
219
 
244
- #### will\_, then\_, and within\_ Directives
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
- The magic versions of `will`, `then`, and `within` will use the configured search strategy to convert the remaning portion of the directive into a class reference.
222
+ Assuming `MyModule1` contains definitions for units `DisplayBanner` and `SayHello`:
247
223
 
248
- Using the default strategy as configured above:
249
224
  ```ruby
250
- module MyModule1
251
- class SayHello
252
- def execute; puts "Hello!"; end
253
- end
225
+ CarryOut.configure do
226
+ search [ MyModule1 ]
254
227
  end
255
228
 
256
- plan = CarryOut.will_say_hello
257
- ```
258
-
259
- #### returning\_as\_ Directive
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
- #### result\_of\_ Directive
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
- plan = CarryOut
277
- .will_receive_message
278
- .returning_as_message
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
- #### Example using all available magic
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
- While a contrived example, the following illustrates the improved readability of a plan when using the magic directives.
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 what 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. 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.
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 isn't bound in any way to things like ActiveRecord. If those sorts of bindings emerge, I'll provide an add-on gem or an alternate require.
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's workflows don't support asynchronous execution units yet. The workflows can't branch or loop. These are features I hope to provide in the future. Feature requests are welcome.
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,11 @@
1
+ module CarryOut
2
+ class Configurator
3
+ def initialize(options)
4
+ @options = options
5
+ end
6
+
7
+ def search(path)
8
+ @options[:search] = [ path ].flatten(1)
9
+ end
10
+ end
11
+ end
@@ -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,13 @@
1
+ module CarryOut
2
+ module Plan
3
+ class GuardContext
4
+ def initialize(context = {})
5
+ @context = context
6
+ end
7
+
8
+ def context(*args)
9
+ args.inject(@context) { |c, k| c.nil? ? nil : c[k] }
10
+ end
11
+ end
12
+ end
13
+ 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,11 @@
1
+ module CarryOut
2
+ module Plan
3
+ class NodeResult
4
+ attr_reader :value
5
+
6
+ def initialize(value = nil)
7
+ @value = value
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -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 }
@@ -1,7 +1,18 @@
1
1
  module CarryOut
2
2
  class Unit
3
3
 
4
- def execute
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)
@@ -1,3 +1,3 @@
1
1
  module CarryOut
2
- VERSION = "0.3.0"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/carry_out.rb CHANGED
@@ -1,69 +1,40 @@
1
- require "carry_out/version"
1
+ require 'carry_out/version'
2
2
 
3
- require "carry_out/context"
4
- require "carry_out/error"
5
- require "carry_out/plan"
6
- require "carry_out/plan_node"
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
- module CarryOut
13
- MATCH_CONTINUATION_METHOD = /^will_/
14
- MATCH_WITHIN_METHOD = /^within_/
8
+ require 'carry_out/plan_builder'
9
+ require 'carry_out/plan_runner'
15
10
 
16
- class ConfiguredCarryOut
11
+ module CarryOut
12
+ class ConfiguredInstance
17
13
  def initialize(options = {})
18
- @config = options
19
- end
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 will(*args)
36
- create_plan.will(*args)
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.defaults=(options = {})
54
- @default_options = options
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.method_missing(method, *args, &block)
59
- default_carry_out.send(method, *args, &block)
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.default_options
63
- @default_options ||= Hash.new
33
+ def self.configuration
34
+ @configuration ||= {}
64
35
  end
65
36
 
66
- def self.default_carry_out
67
- @default_carry_out ||= ConfiguredCarryOut.new(default_options)
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.3.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-03-27 00:00:00.000000000 Z
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/context.rb
105
+ - lib/carry_out/configurator.rb
92
106
  - lib/carry_out/error.rb
93
- - lib/carry_out/plan.rb
94
- - lib/carry_out/plan_node.rb
95
- - lib/carry_out/reference.rb
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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,9 +0,0 @@
1
- module CarryOut
2
- class UnitError < StandardError
3
- attr_reader :error
4
-
5
- def initialize(error)
6
- @error = error
7
- end
8
- end
9
- end