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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6c749e3c0c387227604f9ca4be639d3cabf06dcc
4
- data.tar.gz: 779b36680827fb53d901fbe44436e679cf703cfd
3
+ metadata.gz: 42f7f2e7dafb202aa6ccef0b4f11972a70ba1206
4
+ data.tar.gz: 920ad2c0c2ef8916faf1696d0c17abb040d5e8b4
5
5
  SHA512:
6
- metadata.gz: 95cb4d600263513ef199f4c578f45569eb62052fcd9806fe3f8195ee707fa8fddbaf3704b136192e66186751febefac90050deb51cfaeb502c39eecc75817ea4
7
- data.tar.gz: c4571461ea2b47e78598fa46ae21273d439ea8c3b06bb1129b54135850197d5fbc80dc583547464b4d1fcbef89333dc1a042d3aba9c5323887c8abcedef6b05b
6
+ metadata.gz: 68ce14ebada98c8a1fb6e0b3f3fc0e0c8cadbc3e487b6661c82f144927c1aef2a8dd8e392217e419f25b47db1f133a1eda2b0440a75004390066a509e5cd40c3
7
+ data.tar.gz: 4238932688704262a554d90483bb3642ee3d84c13b31a7db35e64d6a1a8496db4abb0b95bbdd5c1b84df50d5893579500fb1673268226140d9774795327e1a13
@@ -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.
@@ -1,58 +1,63 @@
1
1
  # Development
2
2
 
3
- - Update documentation.
4
-
5
- ## Core
6
-
7
- ## Command
8
-
9
- - Chaining Methods:
10
- - #chain:
11
- - with a command, sets the command's #result to the last result and calls
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
- - private #build_result
31
-
32
- ### Built In
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
- ### DSL
19
+ #### Cuprum::DSL
39
20
 
40
- - class-level methods
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
- - ::process - shortcut for defining #process
45
- - ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
46
-
47
- ### Hooks
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
- ## Operation
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
- ## Result
49
+ - builder/aggregator for command objects, esp. with shared
50
+ initializers/parameters, e.g. actions for a resource
51
+ - Syntax: |
54
52
 
55
- ## Documentation
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') # an instance of Cuprum::Result
85
- result.value #=> an instance of Book with title 'The Hobbit'
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
- Because Cuprum::Command instances are proper objects, they can be composed like any other object. 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.
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
- class AddCommand < Cuprum::Command
179
- def initialize addend
180
- @addend = addend
181
- end # constructor
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
- private
319
+ table_name_command =
320
+ Cuprum::Command.new(&:name).chain(&:pluralize).chain(&:underscore)
321
+ ```
184
322
 
185
- def process int
186
- int + @addend
187
- end # method process
188
- end # class
323
+ #### Chaining Details
189
324
 
190
- double_and_add_one = MultiplyCommand.new(2).chain(AddCommand.new(1))
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
- result.value #=> 5
327
+ ```ruby
328
+ first_command = Cuprum::Command.new { puts 'First command!' }
329
+ first_command.call #=> Outputs 'First command!' to STDOUT.
194
330
 
195
- For finer control over the returned result, `#chain` can instead be called with a block that yields the most recent result. If the block returns a Cuprum::Result, that result is returned or passed to the next command.
331
+ second_command = first_command.chain { puts 'Second command!' }
332
+ second_command.call #=> Outputs 'First command!' then 'Second command!'.
196
333
 
197
- MultiplyCommand.new(3).
198
- chain { |result| Cuprum::Result.new(result + 1) }.
199
- call(3)
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
- Otherwise, the block is still called but the previous result is returned or passed to the next command in the chain.
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
- AddCommand.new(2).
205
- chain { |result| puts "There are #{result.value} lights!" }.
206
- call(2)
207
- #=> Writes "There are 4 lights!" to STDOUT.
208
- #=> Returns a Cuprum::Result with a value of 4.
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` keyword, with values of `:success` and `:failure` accepted. If `#chain` is called with `:on => :success`, then the chained command or block will **only** be called if the previous result `#success?` returns true. Conversely, if `#chain` is called with `:on => :failure`, then the chained command will only be called if the previous result `#failure?` returns true.
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
- In either case, execution will then pass to the next command in the chain, which may itself be called or not if it was conditionally chained. Calling a conditional command chain will return the result of the last called command.
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
- The methods `#then` and `#else` serve as shortcuts for `#chain` with `:on => :success` and `:on => :failure`, respectively.
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
- class EvenCommand < Cuprum::Command
219
- private
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
- def process int
222
- errors << 'errors.messages.not_even' unless int.even?
425
+ book
426
+ end
427
+ create_command =
428
+ Cuprum::Command.new do |attributes|
429
+ book = Book.new(attributes)
223
430
 
224
- int
225
- end # method process
226
- end # class
431
+ if book.save
432
+ result.success!
433
+ else
434
+ book.errors.full_messages.each { |message| result.errors << message }
435
+ end
227
436
 
228
- # The next step in a Collatz sequence is determined as follows:
229
- # - If the number is even, divide it by 2.
230
- # - If the number is odd, multiply it by 3 and add 1.
231
- collatz_command =
232
- EvenCommand.new.
233
- then(DivideCommand.new(2)).
234
- else(MultiplyCommand.new(3).chain(AddCommand.new(1)))
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
- result = collatz_command.new(5)
237
- result.value #=> 16
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
- result = collatz_command.new(16)
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
- panic_command =
247
- Cuprum::Command.new do |value|
248
- halt!
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
- value
251
- end # command
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
- result =
254
- double_command.
255
- then(panic_command).
256
- then(AddCommand.new(1)). #=> This is never executed.
257
- chain(:on => :always) { |count| puts "There are #{count} lights!" }.
258
- call(2)
259
- #=> Writes "There are 4 lights!" to STDOUT.
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
- result.value #= 4
262
- result.halted? #=> true
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'