cuprum 0.6.0 → 0.7.0.rc.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/DEVELOPMENT.md +45 -42
- data/README.md +469 -62
- data/lib/cuprum.rb +1 -1
- data/lib/cuprum/built_in.rb +1 -1
- data/lib/cuprum/built_in/identity_command.rb +1 -1
- data/lib/cuprum/chaining.rb +300 -92
- data/lib/cuprum/command.rb +36 -18
- data/lib/cuprum/operation.rb +6 -5
- data/lib/cuprum/processing.rb +180 -0
- data/lib/cuprum/result.rb +17 -13
- data/lib/cuprum/result_helpers.rb +113 -0
- data/lib/cuprum/utils/instance_spy.rb +26 -26
- data/lib/cuprum/utils/result_not_empty_warning.rb +65 -0
- data/lib/cuprum/version.rb +3 -3
- metadata +7 -5
- data/lib/cuprum/basic_command.rb +0 -212
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42f7f2e7dafb202aa6ccef0b4f11972a70ba1206
|
4
|
+
data.tar.gz: 920ad2c0c2ef8916faf1696d0c17abb040d5e8b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68ce14ebada98c8a1fb6e0b3f3fc0e0c8cadbc3e487b6661c82f144927c1aef2a8dd8e392217e419f25b47db1f133a1eda2b0440a75004390066a509e5cd40c3
|
7
|
+
data.tar.gz: 4238932688704262a554d90483bb3642ee3d84c13b31a7db35e64d6a1a8496db4abb0b95bbdd5c1b84df50d5893579500fb1673268226140d9774795327e1a13
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.7.0
|
4
|
+
|
5
|
+
The "To Strive, To Seek, To Find, And Not To Yield" Update.
|
6
|
+
|
7
|
+
### Commands
|
8
|
+
|
9
|
+
Refactored the `#chain` method. If given a block, will create an anonymous command. The command will be called with the value of the previous result, and additionally the previous result errors, success/failure status, and halted status will be available in the command.
|
10
|
+
|
11
|
+
Implemented the `#yield_result` method, which takes a block, yields the previous result, and wraps the return value of the block in a result.
|
12
|
+
|
13
|
+
Implemented the `#tap_result` method, which functions as `#yield_result` but always returns the previous result.
|
14
|
+
|
15
|
+
Renamed the `#else` method to `#failure`, and the `#then` method to `#success` to avoid overloading reserved words.
|
16
|
+
|
17
|
+
Implemented the `#arity` method, which returns an indication of the number of arguments accepted by the command.
|
18
|
+
|
19
|
+
Refactored internal logic for returning result objects.
|
20
|
+
|
3
21
|
## 0.6.0
|
4
22
|
|
5
23
|
The "By Your Command" Update.
|
data/DEVELOPMENT.md
CHANGED
@@ -1,58 +1,63 @@
|
|
1
1
|
# Development
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#process with the last result's #value; returns result of calling chained
|
13
|
-
command.
|
14
|
-
- with a block creates anonymous command, then see above
|
15
|
-
- if the command does not accept any arguments (or keywords if #value is a
|
16
|
-
Hash), does not pass #value. Requires #arity, #apply_chained?
|
17
|
-
- one argument (on), values nil (default), :success, :failure, :always
|
18
|
-
- #success - as chain(:success)
|
19
|
-
- #failure - as chain(:failure)
|
20
|
-
- #tap_result:
|
21
|
-
- block form only, block is yielded and returns the last result
|
22
|
-
- same argument as #chain
|
23
|
-
- #yield_result:
|
24
|
-
- block form only, block is yielded the last result, returns the return
|
25
|
-
value of the block (wrapped in a result if needed)
|
26
|
-
- same argument as #chain
|
3
|
+
## Version 1.0.0+
|
4
|
+
|
5
|
+
'The "Look On My Works, Ye Mighty, and Despair" Update'
|
6
|
+
|
7
|
+
- Integration specs.
|
8
|
+
- Configuration option to raise, warn, ignore discarded results.
|
9
|
+
|
10
|
+
### Commands
|
11
|
+
|
27
12
|
- Protected Chaining Methods:
|
28
13
|
- #chain!, #success!, #failure!, #tap_chain!, #yield_result!
|
29
14
|
- adds chained command to current command instead of a clone.
|
30
|
-
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
- MapCommand - wraps a command (or proc) and returns Result with value, errors
|
35
|
-
as array
|
36
|
-
- RetryCommand
|
15
|
+
- Command#to_proc
|
16
|
+
- :clear_errors => true option on #chain
|
17
|
+
- #context object
|
37
18
|
|
38
|
-
|
19
|
+
#### Cuprum::DSL
|
39
20
|
|
40
|
-
-
|
21
|
+
- ::process - shortcut for defining #process
|
22
|
+
- ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
|
23
|
+
- chaining methods:
|
41
24
|
- ::chain (::success, ::failure):
|
42
25
|
on #initialize, chains the given command. Can be given a command class
|
43
26
|
(if ::new takes no arguments) or a block that returns a command.
|
44
|
-
|
45
|
-
-
|
46
|
-
|
47
|
-
|
27
|
+
- constructor methods:
|
28
|
+
- Programmatically generate a constructor method. Raises an error if
|
29
|
+
#initialize is defined. Automatically sets instance variables on initialize,
|
30
|
+
and defines reader methods.
|
31
|
+
- ::arguments - sets all positional arguments in the constructor. Takes 0 or
|
32
|
+
more String or Symbol arguments representing required arguments. Takes an
|
33
|
+
optional hash with String/Symbol keys and arbitrary values, representing
|
34
|
+
optional arguments and their default values.
|
35
|
+
- ::keywords - sets keyword arguments; same arguments as ::arguments.
|
36
|
+
|
37
|
+
#### Hooks
|
48
38
|
|
49
39
|
- :before, :around, :after hooks
|
50
40
|
|
51
|
-
|
41
|
+
### Commands - Built In
|
42
|
+
|
43
|
+
- MapCommand - wraps a command (or proc) and returns Result with value, errors
|
44
|
+
as array
|
45
|
+
- RetryCommand
|
46
|
+
|
47
|
+
### CommandFactory
|
52
48
|
|
53
|
-
|
49
|
+
- builder/aggregator for command objects, esp. with shared
|
50
|
+
initializers/parameters, e.g. actions for a resource
|
51
|
+
- Syntax: |
|
54
52
|
|
55
|
-
|
53
|
+
actions = ResourceCommandFactory.new(Book)
|
54
|
+
command = actions::Build.new #=> returns a book builder command
|
55
|
+
result = command.call(attributes) #=> returns a result with value => a Book
|
56
|
+
# OR
|
57
|
+
result = actions.build(attributes) #=> returns a result with value => a Book
|
58
|
+
book = result.value
|
59
|
+
|
60
|
+
### Documentation
|
56
61
|
|
57
62
|
Chaining Case Study: |
|
58
63
|
|
@@ -67,5 +72,3 @@ Chaining Case Study: |
|
|
67
72
|
Create Content
|
68
73
|
Create ContentVersion
|
69
74
|
Tags.each { FindOrCreate Tag }
|
70
|
-
|
71
|
-
## Testing
|
data/README.md
CHANGED
@@ -17,7 +17,7 @@ Traditional frameworks such as Rails focus on the objects of your application -
|
|
17
17
|
- **Consistency:** Use the same Commands to underlie controller actions, worker processes and test factories.
|
18
18
|
- **Encapsulation:** Each Command is defined and run in isolation, and dependencies must be explicitly provided to the command when it is initialized or run. This makes it easier to reason about the command's behavior and keep it insulated from changes elsewhere in the code.
|
19
19
|
- **Testability:** Because the logic is extracted from unnecessary context, testing its behavior is much cleaner and easier.
|
20
|
-
- **Composability:** Complex logic such as "find the object with this ID, update it with these attributes, and log the transaction to the reporting service" can be extracted into a series of simple Commands and composed together. The [Chaining](#label-Chaining) feature allows for complex control flows.
|
20
|
+
- **Composability:** Complex logic such as "find the object with this ID, update it with these attributes, and log the transaction to the reporting service" can be extracted into a series of simple Commands and composed together. The [Chaining](#label-Chaining+Commands) feature allows for complex control flows.
|
21
21
|
- **Reusability:** Logic common to multiple data models or instances in your code, such as "persist an object to the database" or "find all records with a given user and created in a date range" can be refactored into parameterized commands.
|
22
22
|
|
23
23
|
### Alternatives
|
@@ -81,8 +81,12 @@ class BuildBookCommand < Cuprum::Command
|
|
81
81
|
end # class
|
82
82
|
|
83
83
|
command = BuildPostCommand.new
|
84
|
-
result = command.call(:title => 'The Hobbit')
|
85
|
-
result.
|
84
|
+
result = command.call(:title => 'The Hobbit')
|
85
|
+
result.class #=> Cuprum::Result
|
86
|
+
|
87
|
+
book = result.value
|
88
|
+
book.class #=> Book
|
89
|
+
book.title #=> 'The Hobbit'
|
86
90
|
```
|
87
91
|
|
88
92
|
There are several takeaways from this example. First, we are defining a custom command class that inherits from `Cuprum::Command`. We are defining the `#process` method, which takes a single `attributes` parameter and returns an instance of `Book`. Then, we are creating an instance of the command, and invoking the `#call` method with an attributes hash. These attributes are passed to our `#process` implementation. Invoking `#call` returns a result, and the `#value` of the result is our new Book.
|
@@ -119,8 +123,67 @@ increment_command = Cuprum::Command.new { |int| int + 1 }
|
|
119
123
|
increment_command.call(2).value #=> 3
|
120
124
|
```
|
121
125
|
|
126
|
+
If the command is wrapping a method on the receiver, the syntax is even simpler:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
inspect_command = Cuprum::Command.new { |obj| obj.inspect }
|
130
|
+
inspect_command = Cuprum::Command.new(&:inspect) # Equivalent to above.
|
131
|
+
```
|
132
|
+
|
122
133
|
Commands defined using `Cuprum::Command.new` are quick to use, but more difficult to read and to reuse. Defining your own command class is recommended if a command definition takes up more than one line, or if the command will be used in more than one place.
|
123
134
|
|
135
|
+
#### Result Values
|
136
|
+
|
137
|
+
Calling the `#call` method on a `Cuprum::Command` instance will always return an instance of `Cuprum::Result`. The result's `#value` property is determined by the object returned by the `#process` method (if the command is defined as a class) or the block (if the command is defined by passing a block to `Cuprum::Command.new`).
|
138
|
+
|
139
|
+
The `#value` depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the methods `#to_result`, `#value`, and `#success?` is considered to be a result.
|
140
|
+
|
141
|
+
If the object returned is **not** a result, then the `#value` of the returned result is set to the object.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
command = Cuprum::Command.new { 'Greetings, programs!' }
|
145
|
+
result = command.call
|
146
|
+
result.class #=> Cuprum::Result
|
147
|
+
result.value #=> 'Greetings, programs!'
|
148
|
+
```
|
149
|
+
|
150
|
+
If the object returned is the command's own result object, then the `#value` of the returned result is unchanged. For convenience, methods to set the result status or mark the result as halted will return the result.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
command = Cuprum::Command.new { result.failure! }
|
154
|
+
result = command.call
|
155
|
+
result.class #=> Cuprum::Result
|
156
|
+
result.value #=> nil
|
157
|
+
result.success? #=> false
|
158
|
+
```
|
159
|
+
|
160
|
+
If the object returned is another result or compatible object, then `#call` will call the `#to_result` method on the result and return the resulting object.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
command = Cuprum::Command.new { |value| Cuprum::Result.new(value) }
|
164
|
+
result = command.call('Greetings, starfighter!')
|
165
|
+
result.class #=> Cuprum::Result
|
166
|
+
result.value #=> 'Greetings, starfighter!'
|
167
|
+
```
|
168
|
+
|
169
|
+
In some cases, returning a result directly will discard information on the command's own result object. When this occurs, Cuprum will display a warning.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
command =
|
173
|
+
Cuprum::Command.new do
|
174
|
+
result.errors << 'Oops! We are throwing away this result.'
|
175
|
+
|
176
|
+
Cuprum::Result.new
|
177
|
+
end
|
178
|
+
|
179
|
+
#=> This calls Kernel#warn with a warning message.
|
180
|
+
result = command.call
|
181
|
+
result.class #=> Cuprum::Result
|
182
|
+
result.value #=> nil
|
183
|
+
result.success? #=> true
|
184
|
+
result.errors #=> []
|
185
|
+
```
|
186
|
+
|
124
187
|
#### Success, Failure, and Errors
|
125
188
|
|
126
189
|
Whether defined with a block or in the `#process` method, the Command implementation can access an `#errors` object while in the `#call` method. Any errors added to the errors object will be exposed by the `#errors` method on the result object.
|
@@ -171,95 +234,421 @@ result.value #=> book
|
|
171
234
|
book.published? #=> false
|
172
235
|
```
|
173
236
|
|
237
|
+
#### Composing Commands
|
238
|
+
|
239
|
+
Because Cuprum::Command instances are proper objects, they can be composed like any other object. For example, we could define some basic mathematical operations by composing commands:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
increment_command = Cuprum::Command.new { |i| i + 1 }
|
243
|
+
increment_command.call(1).value #=> 2
|
244
|
+
increment_command.call(2).value #=> 3
|
245
|
+
increment_command.call(3).value #=> 4
|
246
|
+
|
247
|
+
add_command = Cuprum::Command.new do |addend, i|
|
248
|
+
addend.times { i = increment_command(i).value }
|
249
|
+
|
250
|
+
i
|
251
|
+
end # command
|
252
|
+
|
253
|
+
add_command.call(1, 1).value #=> 2
|
254
|
+
add_command.call(1, 2).value #=> 3
|
255
|
+
add_command.call(2, 1).value #=> 3
|
256
|
+
add_command.call(2, 2).value #=> 4
|
257
|
+
```
|
258
|
+
|
259
|
+
This can also be done using command classes.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class IncrementCommand < Cuprum::Command
|
263
|
+
private
|
264
|
+
|
265
|
+
def process i
|
266
|
+
i + 1
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
class AddCommand < Cuprum::Command
|
271
|
+
def initialize addend
|
272
|
+
@addend = addend
|
273
|
+
end
|
274
|
+
|
275
|
+
private
|
276
|
+
|
277
|
+
def process i
|
278
|
+
addend.times { i = IncrementCommand.new.call(i).value }
|
279
|
+
|
280
|
+
i
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
add_two_command = AddCommand.new(2)
|
285
|
+
add_two_command.call(0).value #=> 2
|
286
|
+
add_two_command.call(1).value #=> 3
|
287
|
+
add_two_command.call(8).value #=> 10
|
288
|
+
```
|
289
|
+
|
174
290
|
### Chaining Commands
|
175
291
|
|
176
|
-
|
292
|
+
Cuprum::Command also defines methods for chaining commands together. When a chain of commands is called, each command in the chain is called in sequence and passed the value of the previous command. The result of the last command in the chain is returned from the chained call.
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
name_command = Cuprum::Command.new { |klass| klass.name }
|
296
|
+
pluralize_command = Cuprum::Command.new { |str| str.pluralize }
|
297
|
+
underscore_command = Cuprum::Command.new { |str| str.underscore }
|
298
|
+
|
299
|
+
table_name_command =
|
300
|
+
name_command
|
301
|
+
.chain(pluralize_command)
|
302
|
+
.chain(underscore_command)
|
303
|
+
|
304
|
+
result = table_name_command.call(LibraryCard)
|
305
|
+
result.class #=> Cuprum::Result
|
306
|
+
result.value #=> 'library_cards'
|
307
|
+
```
|
308
|
+
|
309
|
+
When the `table_name_command` is called, the class (in our case `LibraryCard`) is passed to the first command in the chain, which is the `name_command`. This produces a Result with a value of 'LibraryCard'. This value is then passed to `pluralize_command`, which returns a Result with a value of 'LibraryCards'. Finally, `underscore_command` is called and returns a Result with a value of 'library_cards'. Since there are no more commands in the chain, this final result is then returned.
|
177
310
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
311
|
+
Chained commands can also be defined with a block. This creates an anonymous command, equivalent to `Cuprum::Command.new {}`. Thus, the `table_name_command` could have been defined as either of these:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
table_name_command =
|
315
|
+
Cuprum::Command.new { |klass| klass.name }
|
316
|
+
.chain { |str| str.pluralize }
|
317
|
+
.chain { |str| str.underscore }
|
182
318
|
|
183
|
-
|
319
|
+
table_name_command =
|
320
|
+
Cuprum::Command.new(&:name).chain(&:pluralize).chain(&:underscore)
|
321
|
+
```
|
184
322
|
|
185
|
-
|
186
|
-
int + @addend
|
187
|
-
end # method process
|
188
|
-
end # class
|
323
|
+
#### Chaining Details
|
189
324
|
|
190
|
-
|
191
|
-
result = double_and_add_one(5)
|
325
|
+
The `#chain` method is defined for instances of `Cuprum::Command` (or object that extend `Cuprum::Chaining`). Calling `#chain` on a command will always create a copy of the command. The given command is then added to the command chain for the copied command. The original command is unchanged. Thus:
|
192
326
|
|
193
|
-
|
327
|
+
```ruby
|
328
|
+
first_command = Cuprum::Command.new { puts 'First command!' }
|
329
|
+
first_command.call #=> Outputs 'First command!' to STDOUT.
|
194
330
|
|
195
|
-
|
331
|
+
second_command = first_command.chain { puts 'Second command!' }
|
332
|
+
second_command.call #=> Outputs 'First command!' then 'Second command!'.
|
196
333
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
#=> Returns a Cuprum::Result with a value of 10.
|
334
|
+
# The original command is unchanged.
|
335
|
+
first_command.call #=> Outputs 'First command!' to STDOUT.
|
336
|
+
```
|
201
337
|
|
202
|
-
|
338
|
+
When a chained command is called, the original command is called with whatever parameters are passed in to the `#call` method, and the command executes the `#process` method as normal, generating a `Cuprum::Result` and assigning it a value and optionally errors or a status. Rather than returning this result, however, it is passed on to the next command in the chain. In the context of a chained command, this means two things.
|
203
339
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
340
|
+
First, the next command is called. If the next command does not take any arguments, it is not passed any arguments. If the next command takes one or more arguments, it is passed the `#value` of that previous result.
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
double_command = Cuprum::Command.new { |i| 2 * i }
|
344
|
+
increment_command = Cuprum::Command.new { |i| 1 + i }
|
345
|
+
square_command = Cuprum::Command.new { |i| i * i }
|
346
|
+
chained_command =
|
347
|
+
double_command
|
348
|
+
.chain(increment_command)
|
349
|
+
.chain(square_command)
|
350
|
+
|
351
|
+
# First, the double_commmand is called with 2. This returns a Cuprum::Result
|
352
|
+
# with a value of 4.
|
353
|
+
#
|
354
|
+
# Next, the increment_command is called with 4, returning a result with value 5.
|
355
|
+
#
|
356
|
+
# Finally, the square_command is called with 5, returning a result with a value
|
357
|
+
# of 25. This final result is returned by #call.
|
358
|
+
result = chained_command.call(2)
|
359
|
+
result.class #=> Cuprum::Result
|
360
|
+
result.value #=> 25
|
361
|
+
```
|
362
|
+
|
363
|
+
Second, the previous result is set as the result of the next command (before it is evaluated). This makes it available to the next command during execution as the `#result` method, and makes available the value, errors, and status of the previous command (and avoids unnecessary object allocation). The value of the result will be updated to reflect the return value of the next command execution.
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
validate_command =
|
367
|
+
Cuprum::Command.new do |object|
|
368
|
+
result.errors << 'Object is invalid!' unless object.valid?
|
369
|
+
|
370
|
+
object
|
371
|
+
end
|
372
|
+
persist_command =
|
373
|
+
Cuprum::Command.new do |object|
|
374
|
+
object.save if result.success?
|
375
|
+
|
376
|
+
object
|
377
|
+
end
|
378
|
+
chained_command = validate_command.chain(persist_command)
|
379
|
+
|
380
|
+
# First, validate_command is called with a valid object. This creates a result
|
381
|
+
# with no errors and whose value is the valid object.
|
382
|
+
#
|
383
|
+
# Then, persist_command is called with the object, and its result is assigned to
|
384
|
+
# the previous result. Since there are no errors on the result, the object is
|
385
|
+
# saved. Finally, the value of the result is set to the object, and the result
|
386
|
+
# is returned.
|
387
|
+
result = chained_command.call(a_valid_object) #=> Saves the object.
|
388
|
+
result.value #=> a_valid_object
|
389
|
+
result.errors #=> []
|
390
|
+
a_valid_object.persisted? #=> true
|
391
|
+
|
392
|
+
# First, validate_command is called with an invalid object. This creates a
|
393
|
+
# result whose value is the invalid object, and with errors 'Object is
|
394
|
+
# invalid!'.
|
395
|
+
#
|
396
|
+
# Then, persist_command is called with the object, and its result is assigned to
|
397
|
+
# the previous result. Since the result has an error, the object is not saved.
|
398
|
+
# Finally, the value of the result is set to the object, and the result
|
399
|
+
# is returned.
|
400
|
+
result = chained_command.call(an_invalid_object) #=> Does not save the object.
|
401
|
+
result.value #=> an_invalid_object
|
402
|
+
result.errors #=> ['Object is invalid!']
|
403
|
+
an_invalid_object.persisted? #=> false
|
404
|
+
```
|
209
405
|
|
210
406
|
#### Conditional Chaining
|
211
407
|
|
212
|
-
The `#chain` method can be passed an optional `:on
|
408
|
+
The `#chain` method can be passed an optional `:on => value` keyword. This keyword determines whether or not the chained command will execute, based on the previous result status. Possible values are `:success`, `:failure`, `:always`, or `nil`. The default value is `nil`.
|
409
|
+
|
410
|
+
If the `:on` keyword is omitted, the chained command will always be executed after the previous command unless the result is halted (see [Halting A Command Chain](#label-Commands))
|
411
|
+
|
412
|
+
If the command is chained with `:on => :success`, then the chained command will only execute if the previous result is passing, e.g. the `#success?` method returns true and the command is not halted. A result is passing if there are no errors, or if the status is set to `:success`.
|
213
413
|
|
214
|
-
|
414
|
+
If the command is chained with `:on => :failure`, then the chained command will only execute if the previous result is failing, e.g. the `#success?` method returns false and the command is not halted. A result is failing if the errors object is not empty, or if the status is set to `:failure`.
|
215
415
|
|
216
|
-
|
416
|
+
If the command is chained with `:on => always`, then the chained command will always be executed, even if the previous result is halted.
|
217
417
|
|
218
|
-
|
219
|
-
|
418
|
+
```ruby
|
419
|
+
find_command =
|
420
|
+
Cuprum::Command.new do |attributes|
|
421
|
+
book = Book.where(:id => attributes[:id]).first
|
422
|
+
|
423
|
+
result.errors << 'Book not found' unless book
|
220
424
|
|
221
|
-
|
222
|
-
|
425
|
+
book
|
426
|
+
end
|
427
|
+
create_command =
|
428
|
+
Cuprum::Command.new do |attributes|
|
429
|
+
book = Book.new(attributes)
|
223
430
|
|
224
|
-
|
225
|
-
|
226
|
-
|
431
|
+
if book.save
|
432
|
+
result.success!
|
433
|
+
else
|
434
|
+
book.errors.full_messages.each { |message| result.errors << message }
|
435
|
+
end
|
227
436
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
437
|
+
book
|
438
|
+
end
|
439
|
+
|
440
|
+
find_or_create_command = find_command.chain(create_command, :on => :failure)
|
441
|
+
|
442
|
+
# With a book that exists in the database, the find_command is called and
|
443
|
+
# returns a result with no errors and a value of the found book. The
|
444
|
+
# create_command is not called.
|
445
|
+
hsh = { id: 0, title: 'Journey to the West' }
|
446
|
+
result = find_or_create_command.call(hsh)
|
447
|
+
book = result.value
|
448
|
+
book.id #=> 0
|
449
|
+
book.title #=> 'Journey to the West'
|
450
|
+
result.success? #=> true
|
451
|
+
result.errors #=> []
|
235
452
|
|
236
|
-
|
237
|
-
|
453
|
+
# With a book that does not exist but with valid attributes, the find command
|
454
|
+
# returns a failing result with a value of nil. The create_command is called and
|
455
|
+
# creates a new book with the attributes, returning a passing result but
|
456
|
+
# preserving the errors.
|
457
|
+
hsh = { id: 1, title: 'The Ramayana' }
|
458
|
+
result = find_or_create_command.call(hsh)
|
459
|
+
book = result.value
|
460
|
+
book.id #=> 1
|
461
|
+
book.title #=> 'The Ramayana'
|
462
|
+
result.success? #=> true
|
463
|
+
result.errors #=> ['Book not found']
|
464
|
+
|
465
|
+
# With a book that does not exist and with invalid attributes, the find command
|
466
|
+
# returns a failing result with a value of nil. The create_command is called and
|
467
|
+
# is unable to create a new book with the attributes, returning the
|
468
|
+
# (non-persisted) book and adding the validation errors.
|
469
|
+
hsh = { id: 2, title: nil }
|
470
|
+
result = find_or_create_command.call(hsh)
|
471
|
+
book = result.value
|
472
|
+
book.id #=> 2
|
473
|
+
book.title #=> nil
|
474
|
+
result.success? #=> false
|
475
|
+
result.errors #=> ['Book not found', "Title can't be blank"]
|
476
|
+
```
|
238
477
|
|
239
|
-
|
240
|
-
result.value #=> 8
|
478
|
+
The `#success` method can be used as shorthand for `chain(command, :on => :success)`. Likewise, the `#failure` method can be used in place of `chain(command, :on => :failure)`.
|
241
479
|
|
242
480
|
#### Halting A Command Chain
|
243
481
|
|
244
482
|
If the `#halt` method is called as part of a Command block or `#process` method, the command chain is halted. Any subsequent chained commands will not be called unless they were chained with the `:on => :always` option. This allows you to terminate a Command chain early without having to raise and rescue an exception.
|
245
483
|
|
246
|
-
|
247
|
-
|
248
|
-
|
484
|
+
```ruby
|
485
|
+
double_command = Cuprum::Command.new { |i| 2 * i }
|
486
|
+
halt_command = Cuprum::Command.new { |value| value.tap { result.halt! } }
|
487
|
+
add_one_command = Cuprum::Command.new { |i| i + 1 }
|
488
|
+
|
489
|
+
chained_command =
|
490
|
+
double_command
|
491
|
+
.chain(halt_command)
|
492
|
+
.chain(add_one_command)
|
493
|
+
.chain(:on => :always) { |count| "There are #{count} lights!" }
|
494
|
+
|
495
|
+
# First, double_command is called with 2, returning a result with no errors and
|
496
|
+
# a value of 4. Then, halt_command is called, which marks the result as halted.
|
497
|
+
# Because the result is now halted, the add_one_command is not called. Finally,
|
498
|
+
# the last command is called (even though the result is halted, the command is
|
499
|
+
# chained :on => :always), which returns a result with the string value. The
|
500
|
+
# result is passing, but is still halted.
|
501
|
+
result = chained_command.call(2)
|
502
|
+
result.value #=> 'There are 4 lights!'
|
503
|
+
result.success? #=> true
|
504
|
+
result.halted? #=> true
|
505
|
+
```
|
506
|
+
|
507
|
+
### Advanced Chaining
|
249
508
|
|
250
|
-
|
251
|
-
|
509
|
+
The `#tap_result` and `#yield_result` methods provide advanced control over the flow of chained commands.
|
510
|
+
|
511
|
+
#### Tap Result
|
512
|
+
|
513
|
+
The `#tap_result` method allows you to insert arbitrary code into a command chain without affecting later commands. The method takes a block and yields the previous result, which is then returned and passed to the next command, or returned by `#call` if `#tap_result` is the last item in the chain.
|
252
514
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
515
|
+
```ruby
|
516
|
+
command =
|
517
|
+
Cuprum::Command.new do
|
518
|
+
result.errors << 'Example error'
|
519
|
+
|
520
|
+
'Example value'
|
521
|
+
end
|
522
|
+
chained_command =
|
523
|
+
command
|
524
|
+
.tap_result do |result|
|
525
|
+
puts "The result value was #{result.inspect}"
|
526
|
+
end
|
527
|
+
|
528
|
+
# Prints 'The result value was "Example value"' to STDOUT.
|
529
|
+
result = chained_command.call
|
530
|
+
result.class #=> Cuprum::Result
|
531
|
+
result.value #=> 'Example value'
|
532
|
+
result.success? #=> false
|
533
|
+
result.errors #=> ['Example error']
|
534
|
+
```
|
535
|
+
|
536
|
+
Like `#chain`, `#tap_result` can be given an `:on => value` keyword.
|
260
537
|
|
261
|
-
|
262
|
-
|
538
|
+
```ruby
|
539
|
+
find_book_command =
|
540
|
+
Cuprum::Command.new do |book_id|
|
541
|
+
book = Book.where(:id => book_id).first
|
542
|
+
|
543
|
+
result.errors << "Unable to find book with id #{book_id}" unless book
|
544
|
+
|
545
|
+
book
|
546
|
+
end
|
547
|
+
|
548
|
+
chained_command =
|
549
|
+
find_book_command
|
550
|
+
.tap_result(:on => :success) do |result|
|
551
|
+
render :show, :locals => { :book => result.value }
|
552
|
+
end
|
553
|
+
.tap_result(:on => :failure) do
|
554
|
+
redirect_to books_path
|
555
|
+
end
|
556
|
+
|
557
|
+
# Calls find_book_command with the id, which queries for the book and returns a
|
558
|
+
# result with a value of the book and no errors. Then, the first tap_result
|
559
|
+
# block is evaluated, calling the render method and passing it the book via the
|
560
|
+
# result.value method. Because the result is passing, the next block is skipped.
|
561
|
+
# Finally, the result of find_book_command is returned unchanged.
|
562
|
+
result = chained_command.call(valid_id)
|
563
|
+
result.class #=> Cuprum::Result
|
564
|
+
result.value #=> an instance of Book
|
565
|
+
result.success? #=> true
|
566
|
+
result.errors #=> []
|
567
|
+
|
568
|
+
# Calls find_book_command with the id, which queries for the book and returns a
|
569
|
+
# result with a value of nil and the error message. Because the result is
|
570
|
+
# failing, the first block is skipped. The second block is then evaluated,
|
571
|
+
# calling the redirect_to method. Finally, the result of find_book_command is
|
572
|
+
# returned unchanged.
|
573
|
+
result = chained_command.call(invalid_id)
|
574
|
+
result.class #=> Cuprum::Result
|
575
|
+
result.value #=> nil
|
576
|
+
result.success? #=> false
|
577
|
+
result.errors #=> ['Unable to find book with id invalid_id']
|
578
|
+
```
|
579
|
+
|
580
|
+
#### Yield Result
|
581
|
+
|
582
|
+
The `#yield_result` method offers advanced control over a chained command step. The method takes a block and yields the previous result. If the object returned by the block is not a result, a new result is created with a value equal to the returned object. In either case, the result is returned and passed to the next command, or returned by `#call` if `#tap_result` is the last item in the chain.
|
583
|
+
|
584
|
+
Unlike `#chain`, the block in `#yield_result` yields the previous result, not the previous value. In addition, `#yield_result` does not automatically carry over any errors from the previous result, the result status, or whether the result was marked as halted.
|
585
|
+
|
586
|
+
```ruby
|
587
|
+
chained_command =
|
588
|
+
Cuprum::Command.new do
|
589
|
+
'Example value'
|
590
|
+
end
|
591
|
+
.yield_result do |result|
|
592
|
+
result.errors << 'Example error'
|
593
|
+
|
594
|
+
result
|
595
|
+
end
|
596
|
+
.yield_result do |result|
|
597
|
+
"The last result was a #{result.success? ? 'success' : 'failure'}."
|
598
|
+
end
|
599
|
+
|
600
|
+
# The first command creates a passing result with a value of 'Example value'.
|
601
|
+
#
|
602
|
+
# This is passed to the first yield_result block, which adds the 'Example error'
|
603
|
+
# string to the result errors, and then returns the result. Because the
|
604
|
+
# yield_result block returns the previous result, it is then passed to the next
|
605
|
+
# item in the chain.
|
606
|
+
#
|
607
|
+
# Finally, the second yield_result block is called, which checks the status of
|
608
|
+
# the passed result. Since the second block does not return the previous result,
|
609
|
+
# the previous result is discarded and a new result is created with the string
|
610
|
+
# value starting with 'The last result was ...'.
|
611
|
+
result = chained_command.call
|
612
|
+
result.class #=> Cuprum::Result
|
613
|
+
result.value #=> 'The last result was a failure.'
|
614
|
+
result.success? #=> true
|
615
|
+
result.errors #=> []
|
616
|
+
```
|
617
|
+
|
618
|
+
Like `#chain`, `#yield_result` can be given an `:on => value` keyword.
|
619
|
+
|
620
|
+
```ruby
|
621
|
+
# The Collatz conjecture in mathematics concerns a sequence of numbers defined
|
622
|
+
# as follows. Start with any positive integer. If the number is even, then
|
623
|
+
# divide the number by two. If the number is odd, multiply by three and add one.
|
624
|
+
# These steps are repeated until the value is one or the numbers loop.
|
625
|
+
step_command =
|
626
|
+
Cuprum::Command.new do |i|
|
627
|
+
result.failure! unless i.even?
|
628
|
+
|
629
|
+
i
|
630
|
+
end
|
631
|
+
.yield_result(:on => :success) { |result| result.value / 2 }
|
632
|
+
.yield_result(:on => :failure) { |result| 1 + 3 * result.value }
|
633
|
+
|
634
|
+
# Because the value is even, the first command returns a passing result with a
|
635
|
+
# value of 8. The first yield_result block is then called with that result,
|
636
|
+
# returning a new result with a value of 4. The result is passing, so the second
|
637
|
+
# yield_result block is skipped and the result is returned.
|
638
|
+
result = collatz_command.call(8)
|
639
|
+
result.value #=> 4
|
640
|
+
result.success? #=> true
|
641
|
+
|
642
|
+
# Because the value is odd, the first command returns a failing result with a
|
643
|
+
# value of 5. Because the result is failing, the first yield_result block is
|
644
|
+
# skipped. The second yield_result block is then called with the result,
|
645
|
+
# returning a new (passing) result with a value of 16.
|
646
|
+
result = collatz_command.call(5)
|
647
|
+
result.value #=> 16
|
648
|
+
result.success? #=> true
|
649
|
+
```
|
650
|
+
|
651
|
+
Under the hood, both `#chain` and `#tap_result` are implemented on top of `#yield_result`.
|
263
652
|
|
264
653
|
### Results
|
265
654
|
|
@@ -586,6 +975,14 @@ Only available while the Command is being called. If called, marks the result ob
|
|
586
975
|
|
587
976
|
[Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#success!-instance_method)
|
588
977
|
|
978
|
+
#### `#tap_result`
|
979
|
+
|
980
|
+
tap_result(on: nil) { |previous_result| } #=> Cuprum::Result
|
981
|
+
|
982
|
+
Creates a copy of the command, and then chains the block to execute after the command implementation. When #call is executed, each chained block will be yielded the previous result, and the previous result returned or yielded to the next block. The return value of the block is discarded.
|
983
|
+
|
984
|
+
If the `on` parameter is omitted, the block will be called if the last result is not halted. If the `on` parameter is set to `:success`, the block will be called if the last result is successful and not halted. If the `on` parameter is set to `:failure`, the block will be called if the last result is failing and not halted. Finally, if the `on` parameter is set to `:always`, the block will always be called, even if the last result is halted.
|
985
|
+
|
589
986
|
#### `#then`
|
590
987
|
|
591
988
|
then(command) #=> Cuprum::Command
|
@@ -602,6 +999,16 @@ If the block returns a Cuprum::Result (or an object responding to #value and #su
|
|
602
999
|
|
603
1000
|
[Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#then-instance_method)
|
604
1001
|
|
1002
|
+
#### `#yield_result`
|
1003
|
+
|
1004
|
+
yield_result(on: nil) { |previous_result| } #=> Cuprum::Result
|
1005
|
+
|
1006
|
+
Creates a copy of the command, and then chains the block to execute after the command implementation. When #call is executed, each chained block will be yielded the previous result, and the return value wrapped in a result and returned or yielded to the next block.
|
1007
|
+
|
1008
|
+
If the `on` parameter is omitted, the block will be called if the last result is not halted. If the `on` parameter is set to `:success`, the block will be called if the last result is successful and not halted. If the `on` parameter is set to `:failure`, the block will be called if the last result is failing and not halted. Finally, if the `on` parameter is set to `:always`, the block will always be called, even if the last result is halted.
|
1009
|
+
|
1010
|
+
[Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#yield_result-instance_method)
|
1011
|
+
|
605
1012
|
### Cuprum::Operation
|
606
1013
|
|
607
1014
|
require 'cuprum'
|