cuprum 0.9.1 → 0.10.0.rc.0

This diff has not been reviewed by any users.
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
  SHA256:
3
- metadata.gz: f867011719a69bd7080c77d450fedfee736ef6df5be2b8a2d11fe3b1006a3890
4
- data.tar.gz: 48109c5460f817fedb8c6a46b84649a87d9c21e2d2dbadba31b31e32bdf20977
3
+ metadata.gz: 8ca65deb82f58ec6a0b9706a7eb81f6edc191ecf54d22a9e3155a82370aec83e
4
+ data.tar.gz: e259228f7b6128c24a0974666165a0655c4188f1011519c8d9dea498c5d59008
5
5
  SHA512:
6
- metadata.gz: b272e3d32edf7449a83a8513be56f8e68e2474872621d055f5947b6605ec5c29ecbd4acc7e7155d1efd5ee122689dc564eaab312b499ff129d996c6e6599ff90
7
- data.tar.gz: 7ad63c437101991668b8a060fa355633f46b269ad55d609fad81bf5e993b998974f43ae61bc96d2c781a10e29d83178a7b2770ec2540a121f54bf6261530fcce
6
+ metadata.gz: eafd3d6b85ca035cec50fdbbaacf3f3238cfa1a624de85f0663fdfcef4aeb91298f08ee11915631d3f82069222ca641279c243a6d2087a8b29cc0cf1bd01bd01
7
+ data.tar.gz: '080089f9d312a1019e934e7ed84e6df92b566c2bff83c9472671c0bed9f3738762345082d99f298af70eab33f1ec8af6809310ee2a3f90cd65544a618e812fb8'
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10
4
+
5
+ The "One Small Step" Update
6
+
7
+ **Note:** This update may have backwards incompatible changes for versions of Ruby before 2.7 when creating commands whose last parameter is an arguments Hash. See [separation of positional and keyword arguments](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/) for more information.
8
+
9
+ ### Commands
10
+
11
+ Implemented the `#curry` method, which performs partial application of arguments or keywords.
12
+
13
+ #### Chaining
14
+
15
+ Added deprecation warnings to all chaining methods, and `Cuprum::Command` no longer includes `Cuprum::Chaining` by default. The `Cuprum::Chaining` module will be removed in version 1.0.
16
+
17
+ #### Steps
18
+
19
+ Implemented the `#step` method, which extracts the value of the called command (on a success) or halts execution (on a failure).
20
+
21
+ Implemented the `#steps` method, which wraps a series of steps and returns first failing result, or the the last result if all steps are passing.
22
+
3
23
  ## 0.9.1
4
24
 
5
25
  ### Operations
@@ -1,25 +1,13 @@
1
1
  # Development
2
2
 
3
- ## Version 0.9.0
4
-
5
- The "'Tis Not Too Late To Seek A Newer World" Update
6
-
7
- ## Version 0.10.0
8
-
9
- The "One Small Step" Update
3
+ ## Version 1.0.0
10
4
 
11
- ### Commands
5
+ The "Look On My Works, Ye Mighty, and Despair" Update
12
6
 
13
- - Implement #<<, #>> composition methods.
14
- - Calls commands in order passing values.
15
- - Return Result early on Failure (or not Success), otherwise final Result.
16
- - Implement #step method (used in #process).
17
- - Called with command (block? method?) that returns a Result.
18
- - Raise (and catch) exception on non-success Result (test custom status?)
19
- - Otherwise return Result#value.
20
- - Deprecate #chain and its related methods
21
-
22
- ### Documentation
7
+ - Integration specs.
8
+ - Configuration option to raise, warn, ignore discarded results.
9
+ - Code cleanup: Hash syntax, remove end comments, remove file headers
10
+ - Status Badges!
23
11
 
24
12
  Steps Case Study: |
25
13
 
@@ -35,44 +23,19 @@ Steps Case Study: |
35
23
  Create ContentVersion
36
24
  Tags.each { FindOrCreate Tag }
37
25
 
38
- ### Matcher
39
-
40
- - Handle success(), failure(), failure(SomeError) cases.
41
- - Custom matcher to handle additional cases - halted, pending, etc?
42
-
43
- ### RSpec
44
-
45
- - be_callable matcher - delegates to respond_to(), but check arguments of
46
- private #process method
47
- - call_command_step matcher
48
- - (optionally) alias be_a_result family as have_result for operations
49
-
50
- ## Version 1.0.0
51
-
52
- 'The "Look On My Works, Ye Mighty, and Despair" Update'
53
-
54
- - Integration specs.
55
- - Configuration option to raise, warn, ignore discarded results.
56
- - Code cleanup: Hash syntax, remove end comments, remove file headers
57
-
58
26
  ### Commands
59
27
 
60
28
  - Command#to_proc
61
- - Remove #chain and its related methods
62
-
63
- ### Commands - Built In
64
-
65
- - MapCommand - wraps a command (or proc) and returns Result with value, errors
66
- as array
67
- - RetryCommand
68
29
 
69
30
  ## Future Versions
70
31
 
71
32
  ### Commands
72
33
 
73
- - command currying
34
+ - Implement #<<, #>> composition methods.
35
+ - Calls commands in order passing values.
36
+ - Return Result early on Failure (or not Success), otherwise final Result.
74
37
 
75
- #### Cuprum::DSL
38
+ #### DSL
76
39
 
77
40
  - ::process - shortcut for defining #process
78
41
  - ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
@@ -89,3 +52,46 @@ Steps Case Study: |
89
52
  #### Dependency Injection
90
53
 
91
54
  - shorthand for referencing a sequence of operations
55
+
56
+ ### Commands - Built In
57
+
58
+ - MapCommand - wraps a command (or proc) and returns Result with value, errors
59
+ as array
60
+ - RetryCommand - takes command, retry count
61
+ - optional only:, except: - restrict what errors are retried
62
+
63
+ ### Matcher
64
+
65
+ - Handle success(), failure(), failure(SomeError) cases.
66
+ - Custom matcher to handle additional cases - halted, pending, etc?
67
+
68
+ ### Middleware
69
+
70
+ - Implement Command.subclass
71
+ - Curries constructor arguments
72
+ - Implement Cuprum::Middleware
73
+ - #process takes next command, \*args, \*\*kwargs
74
+ - calls next command with \*args, \*\*kwargs
75
+ - .apply takes middleware: array, root: command
76
+ - Implement Cuprum::AppliedMiddleware < Cuprum::Command
77
+ - has readers #root (Class), #middleware (Array<Class>)
78
+ - #initialize
79
+ - initializes root command (passing constructor parameters)
80
+ - initializes each middleware command
81
+ - if Class defining .instance, call .instance
82
+ - if Class, call .new
83
+ - if Proc, call #call with constructor parameters
84
+ - calls Middleware.apply and caches as private #applied
85
+ - #call
86
+ - delegates to #applied
87
+
88
+ ### RSpec
89
+
90
+ - be_callable matcher - delegates to respond_to(), but check arguments of
91
+ private #process method
92
+ - call_command_step matcher
93
+ - (optionally) alias be_a_result family as have_result for operations
94
+
95
+ ### Steps::Strict
96
+
97
+ - #step raises exception unless block or method returns a result
data/README.md CHANGED
@@ -23,12 +23,12 @@ Traditional frameworks such as Rails focus on the objects of your application -
23
23
 
24
24
  ### Alternatives
25
25
 
26
- If you want to extract your logic but Cuprum is not the right solution for you, here are several alternatives:
27
-
28
- - Service objects. A common pattern used when first refactoring an application that has outgrown its abstractions. Service objects are simple and let you group related functionality, but they are harder to compose and require firm conventions to tame.
29
- - The [Interactor](https://github.com/collectiveidea/interactor) gem. Provides an `Action` module to implement logic and an `Organizer` module to manage control flow. Supports before, around, and after hooks.
30
- - The [Waterfall](https://github.com/apneadiving/waterfall) gem. Focused more on control flow.
31
- - [Trailblazer](http://trailblazer.to/) Operations. A pipeline-based approach to control flow, and can integrate tightly with other Trailblazer elements.
26
+ If you want to extract your logic but Cuprum is not the right solution for you, there are a number of alternatives, including
27
+ [ActiveInteraction](https://github.com/AaronLasseigne/active_interaction),
28
+ [Dry::Monads](https://dry-rb.org/gems/dry-monads/),
29
+ [Interactor](https://github.com/collectiveidea/interactor),
30
+ [Trailblazer](http://trailblazer.to/) Operations,
31
+ and [Waterfall](https://github.com/apneadiving/waterfall).
32
32
 
33
33
  ### Compatibility
34
34
 
@@ -213,7 +213,61 @@ result.value #=> book
213
213
  book.published? #=> false
214
214
  ```
215
215
 
216
- #### Composing Commands
216
+ #### Command Currying
217
+
218
+ Cuprum::Command defines the `#curry` method, which allows for partial application of command objects. Partial application (more commonly referred to, if imprecisely, as currying) refers to fixing some number of arguments to a function, resulting in a function with a smaller number of arguments.
219
+
220
+ In Cuprum's case, a curried (partially applied) command takes an original command and pre-defines some of its arguments. When the curried command is called, the predefined arguments and/or keywords will be combined with the arguments passed to #call.
221
+
222
+ ##### Currying Arguments
223
+
224
+ We start by defining the base command. In this case, our base command takes two string arguments - a greeting and a person to be greeted.
225
+
226
+ ```ruby
227
+ say_command = Cuprum::Command.new do |greeting, person|
228
+ "#{greeting}, #{person}!"
229
+ end
230
+ say_command.call('Hello', 'world')
231
+ #=> returns a result with value 'Hello, world!'
232
+ ```
233
+
234
+ Next, we create a curried command. Here, we pass in one argument. This will set the first argument to always be "Greetings"; therefore, our curried command only takes one argument, the name of the person being greeted.
235
+
236
+ ```ruby
237
+ greet_command = say_command.curry('Greetings')
238
+ greet_command.call('programs')
239
+ #=> returns a result with value 'Greetings, programs!'
240
+ ```
241
+
242
+ Alternatively, we could pass both arguments to `#curry`. In this case, our curried argument does not take any arguments, and will always return the same string.
243
+
244
+ ```ruby
245
+ recruit_command = say_command.curry('Greetings', 'starfighter')
246
+ recruit_command.call
247
+ #=> returns a result with value 'Greetings, starfighter!'
248
+ ```
249
+
250
+ ##### Currying Keywords
251
+
252
+ We can also pass keywords to `#curry`. Again, we start by defining our base command. In this case, our base command takes a mathematical operation (addition, subtraction, multiplication, etc) and a list of operands.
253
+
254
+ ```ruby
255
+ math_command = Cuprum::Command.new do |operands:, operation:|
256
+ operations.reduce(&operation)
257
+ end
258
+ math_command.call(operands: [2, 2], operation: :+)
259
+ #=> returns a result with value 4
260
+ ```
261
+
262
+ Our curried command still takes two keywords, but now the operation keyword is optional. It now defaults to :\*, for multiplication.
263
+
264
+ ```ruby
265
+ multiply_command = math_command.curry(operation: :*)
266
+ multiply_command.call(operands: [3, 3])
267
+ #=> returns a result with value 9
268
+ ```
269
+
270
+ ### Composing Commands
217
271
 
218
272
  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:
219
273
 
@@ -224,6 +278,8 @@ increment_command.call(2).value #=> 3
224
278
  increment_command.call(3).value #=> 4
225
279
 
226
280
  add_command = Cuprum::Command.new do |addend, i|
281
+ # Here, we are composing commands together by calling the increment_command
282
+ # instance from inside the add_command definition.
227
283
  addend.times { i = increment_command(i).value }
228
284
 
229
285
  i
@@ -270,289 +326,305 @@ add_two_command.call(1).value #=> 3
270
326
  add_two_command.call(8).value #=> 10
271
327
  ```
272
328
 
273
- ### Chaining Commands
329
+ You can achieve even more powerful composition by passing in a command as an argument to a method, or by creating a method that returns a command.
330
+
331
+ #### Commands As Arguments
274
332
 
275
- 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.
333
+ Since commands are objects, they can be passed in as arguments to a method or to another command. For example, consider a command that calls another command a given number of times:
276
334
 
277
335
  ```ruby
278
- name_command = Cuprum::Command.new { |klass| klass.name }
279
- pluralize_command = Cuprum::Command.new { |str| str.pluralize }
280
- underscore_command = Cuprum::Command.new { |str| str.underscore }
336
+ class RepeatCommand
337
+ def initialize(count)
338
+ @count = count
339
+ end
340
+
341
+ private
281
342
 
282
- table_name_command =
283
- name_command
284
- .chain(pluralize_command)
285
- .chain(underscore_command)
343
+ def process(command)
344
+ @count.times { command.call }
345
+ end
346
+ end
286
347
 
287
- result = table_name_command.call(LibraryCard)
288
- result.class #=> Cuprum::Result
289
- result.value #=> 'library_cards'
348
+ greet_command = Cuprum::Command.new { puts 'Greetings, programs!' }
349
+ repeat_command = RepeatCommand.new(3)
350
+ repeat_command.call(greet_command) #=> prints 'Greetings, programs!' 3 times
290
351
  ```
291
352
 
292
- 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.
353
+ This is an implementation of the Strategy pattern, which allows us to customize the behavior of a part of our system by passing in implementation code rather than burying conditionals in our logic.
293
354
 
294
- 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:
355
+ Consider a more concrete example. Suppose we are running an online bookstore that sells both physuical and electronic books, and serves both domestic and international customers. Depending on what the customer ordered and where they live, our business logic for fulfilling an order will have different shipping instructions.
356
+
357
+ Traditionally this would be handled with a conditional inside the order fulfillment code, which adds complexity. However, we can use the Strategy pattern and pass in our shipping code as a command.
295
358
 
296
359
  ```ruby
297
- table_name_command =
298
- Cuprum::Command.new { |klass| klass.name }
299
- .chain { |str| str.pluralize }
300
- .chain { |str| str.underscore }
360
+ class DeliverEbook < Cuprum::Command; end
361
+
362
+ class ShipDomestic < Cuprum::Command; end
363
+
364
+ class ShipInternational < Cuprum::Command; end
365
+
366
+ class FulfillOrder < Cuprum::Command
367
+ def initialize(delivery_command)
368
+ @delivery_command = delivery_command
369
+ end
301
370
 
302
- table_name_command =
303
- Cuprum::Command.new(&:name).chain(&:pluralize).chain(&:underscore)
371
+ private
372
+
373
+ def process(book:, user:)
374
+ # Here we will check inventory, process payments, and so on. The final step
375
+ # is actually delivering the book to the user:
376
+ delivery_command.call(book: book, user: user)
377
+ end
378
+ end
304
379
  ```
305
380
 
306
- #### Chaining Details
381
+ This pattern is also useful for testing. When writing specs for the FulfillOrder command, simply pass in a mock double as the delivery command. This removes any need to stub out the implementation of whatever shipping method is used (or worse, calls to external services).
307
382
 
308
- 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:
383
+ #### Commands As Returned Values
384
+
385
+ We can also return commands as an object from a method call or from another command. One use case for this is the Abstract Factory pattern.
386
+
387
+ Consider our shipping example, above. The traditional way to generate a shipping command is to use an `if-then-else` or `case` construct, which would be embedded in whatever code is calling `FulfillOrder`. This adds complexity and increases the testing burden.
388
+
389
+ Instead, let's create a factory command. This command will take a user and a book, and will return the command used to ship that item.
309
390
 
310
391
  ```ruby
311
- first_command = Cuprum::Command.new { puts 'First command!' }
312
- first_command.call #=> Outputs 'First command!' to STDOUT.
392
+ class ShippingMethod < Cuprum::Command
393
+ private
313
394
 
314
- second_command = first_command.chain { puts 'Second command!' }
315
- second_command.call #=> Outputs 'First command!' then 'Second command!'.
395
+ def process(book:, user:)
396
+ return DeliverEbook.new(user.email) if book.ebook?
316
397
 
317
- # The original command is unchanged.
318
- first_command.call #=> Outputs 'First command!' to STDOUT.
319
- ```
398
+ return ShipDomestic.new(user.address) if user.address&.domestic?
320
399
 
321
- 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. Rather than returning the result, however, the next command in the chain 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. When the final command in the chain is called, that result is returned.
400
+ return ShipInternational.new(user.address) if user.address&.international?
322
401
 
323
- ```ruby
324
- double_command = Cuprum::Command.new { |i| 2 * i }
325
- increment_command = Cuprum::Command.new { |i| 1 + i }
326
- square_command = Cuprum::Command.new { |i| i * i }
327
- chained_command =
328
- double_command
329
- .chain(increment_command)
330
- .chain(square_command)
331
-
332
- # First, the double_commmand is called with 2. This returns a Cuprum::Result
333
- # with a value of 4.
334
- #
335
- # Next, the increment_command is called with 4, returning a result with value 5.
336
- #
337
- # Finally, the square_command is called with 5, returning a result with a value
338
- # of 25. This final result is returned by #call.
339
- result = chained_command.call(2)
340
- result.class #=> Cuprum::Result
341
- result.value #=> 25
402
+ err = Cuprum::Error.new(message: 'user does not have a valid address')
403
+
404
+ failure(err)
405
+ end
406
+ end
342
407
  ```
343
408
 
344
- #### Conditional Chaining
409
+ Notice that our factory includes error handling - if the user does not have a valid address, that is handled immediately rather than when trying to ship the item.
345
410
 
346
- 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`.
411
+ The [Command Factory](#label-Command+Factories) defined by Cuprum is another example of using the Abstract Factory pattern to return command instances. One use case for a command factory would be defining CRUD operations for data records. Depending on the class or the type of record passed in, the factory could return a generic command or a specific command tied to that specific record type.
347
412
 
348
- If the command is chained with `on: :always`, or if the `:on` keyword is omitted, the chained command will always be executed after the previous command.
413
+ ### Command Steps
349
414
 
350
- 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. A result is passing if there is no `#error`, or if the status is set to `:success`.
415
+ Separating out business logic into commands is a powerful tool, but it does come with some overhead, particularly when checking whether a result is passing, or when converting between results and values. When a process has many steps, each of which can fail or return a value, this can result in a lot of boilerplate.
351
416
 
352
- 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. A result is failing if there is an `#error`, or if the status is set to `:failure`.
417
+ The solution Cuprum provides is the `#step` method, which calls either a named method or a given block. If the result of the block or method is passing, then the `#step` method returns the value of the result.
353
418
 
354
419
  ```ruby
355
- find_command =
356
- Cuprum::Command.new do |attributes|
357
- book = Book.where(id: attributes[:id]).first
358
-
359
- return book if book
360
-
361
- Cuprum::Result.new(error: 'Book not found')
362
- end
363
- create_command =
364
- Cuprum::Command.new do |attributes|
365
- book = Book.new(attributes)
420
+ triple_command = Cuprum::Command.new { |i| success(3 * i) }
366
421
 
367
- return book if book.save
422
+ int = 2
423
+ int = step { triple_command.call(int) } #=> returns 6
424
+ int = step { triple_command.call(int) } #=> returns 18
425
+ ```
368
426
 
369
- Cuprum::Result.new(error: book.errors.full_messages)
370
- end
427
+ Notice that in each step, we are returning the *value* of the result from `#step`, not the result itself. This means we do not need explicit calls to the `#value` method.
371
428
 
372
- find_or_create_command = find_command.chain(create_command, on: :failure)
429
+ Of course, not all commands return a passing result. If the result of the block or method is failing, then `#step` will throw `:cuprum_failed_result` and the result, immediately halting the execution chain. If the `#step` method is used inside a command definition (or inside a `#steps` block; [see below](#label-Using+Steps+Outside+Of+Commands)), that symbol will be caught and the failing result returned by `#call`.
373
430
 
374
- # With a book that exists in the database, the find_command is called and
375
- # returns a result with no error and a value of the found book. The
376
- # create_command is not called.
377
- hsh = { id: 0, title: 'Journey to the West' }
378
- result = find_or_create_command.call(hsh)
379
- book = result.value
380
- book.id #=> 0
381
- book.title #=> 'Journey to the West'
382
- result.success? #=> true
383
- result.error #=> nil
431
+ ```ruby
432
+ divide_command = Cuprum::Command.new do |dividend, divisor|
433
+ return failure('divide by zero') if divisor.zero?
384
434
 
385
- # With a book that does not exist but with valid attributes, the find command
386
- # returns a failing result with a value of nil. The create_command is called and
387
- # creates a new book with the attributes, returning a passing result.
388
- hsh = { id: 1, title: 'The Ramayana' }
389
- result = find_or_create_command.call(hsh)
390
- book = result.value
391
- book.id #=> 1
392
- book.title #=> 'The Ramayana'
393
- result.success? #=> true
394
- result.error #=> nil
435
+ success(dividend / divisor)
436
+ end
395
437
 
396
- # With a book that does not exist and with invalid attributes, the find command
397
- # returns a failing result with a value of nil. The create_command is called and
398
- # is unable to create a new book with the attributes, returning the
399
- # (non-persisted) book and adding the validation errors.
400
- hsh = { id: 2, title: nil }
401
- result = find_or_create_command.call(hsh)
402
- book = result.value
403
- book.id #=> 2
404
- book.title #=> nil
405
- result.success? #=> false
406
- result.error #=> ["Title can't be blank"]
438
+ value = step { divide_command.call(10, 5) } #=> returns 2
439
+ value = step { divide_command.call(2, 0) } #=> throws :cuprum_failed_result
407
440
  ```
408
441
 
409
- ### Advanced Chaining
442
+ Here, the `divide_command` can either return a passing result (if the divisor is not zero) or a failing result (if the divisor is zero). When wrapped in a `#step`, the failing result is then thrown, halting execution.
410
443
 
411
- The `#tap_result` and `#yield_result` methods provide advanced control over the flow of chained commands.
444
+ This is important when using a sequence of steps. Let's consider a case study - reserving a book from the library. This entails several steps, each of which could potentially fail:
412
445
 
413
- #### Tap Result
446
+ - Validating that the user can reserve books. Maybe the user has too many unpaid fines.
447
+ - Finding the requested book in the library system. Maybe the requested title isn't in the system.
448
+ - Placing a reservation on the book. Maybe there are no copies of the book available to reserve.
414
449
 
415
- 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.
450
+ Using `#step`, as soon as one of the subtasks fails then the command will immediately return the failed value. This prevents us from hitting later subtasks with invalid data, it returns the actual failing result for analytics and for displaying a useful error message to the user, and it avoids the overhead (and the boilerplate) of exception-based failure handling.
416
451
 
417
452
  ```ruby
418
- command =
419
- Cuprum::Command.new do
420
- Cuprum::Result.new(value: 'Example value', error: 'Example error')
453
+ class CheckUserStatus < Cuprum::Command; end
454
+
455
+ class CreateBookReservation < Cuprum::Command; end
456
+
457
+ class FindBookByTitle < Cuprum::Command; end
458
+
459
+ class ReserveBookByTitle < Cuprum::Command
460
+ private
461
+
462
+ def process(title:, user:)
463
+ # If CheckUserStatus fails, #process will immediately return that result.
464
+ # For this step, we already have the user, so we don't need to use the
465
+ # result value.
466
+ step { CheckUserStatus.new.call(user) }
467
+
468
+ # Here, we are looking up the requested title. In this case, we will need
469
+ # the book object, so we save it as a variable. Notice that we don't need
470
+ # an explicit #value call - #step handles that for us.
471
+ book = step { FindBookByTitle.new.call(title) }
472
+
473
+ # Finally, we want to reserve the book. Since this is the last subtask, we
474
+ # don't strictly need to use #step. However, it's good practice, especially
475
+ # if we might need to add more steps to the command in the future.
476
+ step { CreateBookReservation.new.call(book: book, user: user) }
421
477
  end
422
- chained_command =
423
- command
424
- .tap_result do |result|
425
- puts "The result value was #{result.inspect}"
426
- end
478
+ end
479
+ ```
427
480
 
428
- # Prints 'The result value was "Example value"' to STDOUT.
429
- result = chained_command.call
481
+ First, our user may not have borrowing privileges. In this case, `CheckUserStatus` will fail, and neither of the subsequent steps will be called. The `#call` method will return the failing result from `CheckUserStatus`.
482
+
483
+ ```ruby
484
+ result = ReserveBookByTitle.new.call(
485
+ title: 'The C Programming Language',
486
+ user: 'Ed Dillinger'
487
+ )
430
488
  result.class #=> Cuprum::Result
431
- result.value #=> 'Example value'
432
489
  result.success? #=> false
433
- result.error #=> 'Example error'
490
+ result.error #=> 'not authorized to reserve book'
434
491
  ```
435
492
 
436
- Like `#chain`, `#tap_result` can be given an `on: value` keyword.
493
+ Second, our user may be valid but our requested title may not exist in the system. In this case, `FindBookByTitle` will fail, and the final step will not be called. The `#call` method will return the failing result from `FindBookByTitle`.
437
494
 
438
495
  ```ruby
439
- find_book_command =
440
- Cuprum::Command.new do |book_id|
441
- book = Book.where(id: book_id).first
496
+ result = ReserveBookByTitle.new.call(
497
+ title: 'Using GOTO For Fun And Profit',
498
+ user: 'Alan Bradley'
499
+ )
500
+ result.class #=> Cuprum::Result
501
+ result.success? #=> false
502
+ result.error #=> 'title not found'
503
+ ```
442
504
 
443
- return book if book
505
+ Third, our user and book may be valid, but all of the copies are checked out. In this case, each of the steps will be called, and the `#call` method will return the failing result from `CreateBookReservation`.
444
506
 
445
- Cuprum::Result.new(error: "Unable to find book with id #{book_id}")
446
- end
507
+ ```ruby
508
+ result = ReserveBookByTitle.new.call(
509
+ title: 'Design Patterns: Elements of Reusable Object-Oriented Software',
510
+ user: 'Alan Bradley'
511
+ )
512
+ result.class #=> Cuprum::Result
513
+ result.success? #=> false
514
+ result.error #=> 'no copies available'
515
+ ```
447
516
 
448
- chained_command =
449
- find_book_command
450
- .tap_result(on: :success) do |result|
451
- render :show, locals: { book: result.value }
452
- end
453
- .tap_result(on: :failure) do
454
- redirect_to books_path
455
- end
517
+ Finally, if each of the steps succeeds, the `#call` method will return the result of the final step.
456
518
 
457
- # Calls find_book_command with the id, which queries for the book and returns a
458
- # result with a value of the book and no error. Then, the first tap_result
459
- # block is evaluated, calling the render method and passing it the book via the
460
- # result.value method. Because the result is passing, the next block is skipped.
461
- # Finally, the result of find_book_command is returned unchanged.
462
- result = chained_command.call(valid_id)
519
+ ```ruby
520
+ result = ReserveBookByTitle.new.call(
521
+ title: 'The C Programming Language',
522
+ user: 'Alan Bradley'
523
+ )
463
524
  result.class #=> Cuprum::Result
464
- result.value #=> an instance of Book
465
525
  result.success? #=> true
466
- result.error #=> nil
467
-
468
- # Calls find_book_command with the id, which queries for the book and returns a
469
- # result with a value of nil and the error message. Because the result is
470
- # failing, the first block is skipped. The second block is then evaluated,
471
- # calling the redirect_to method. Finally, the result of find_book_command is
472
- # returned unchanged.
473
- result = chained_command.call(invalid_id)
474
- result.class #=> Cuprum::Result
475
- result.value #=> nil
476
- result.success? #=> false
477
- result.error #=> ['Unable to find book with id invalid_id']
526
+ result.value #=> an instance of BookReservation
478
527
  ```
479
528
 
480
- #### Yield Result
529
+ #### Using Methods As Steps
530
+
531
+ Steps can also be defined as method calls. Instead of providing a block to `#step`, provide the name of the method as the first argument, either as a symbol or as a string. Any subsequent arguments, keywords, or a block is passed to the method when it is called.
481
532
 
482
- 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.
533
+ A step defined with a method behaves the same as a step defined with a block. If the method returns a successful result, then `#step` will return the value of the result. If the method returns a failing result, then `#step` will throw `:cuprum_failed_result` and the result, to be caught by the `#process` method or the containing `#steps` block.
483
534
 
484
- Unlike `#chain`, the block in `#yield_result` yields the previous result, not the previous value.
535
+ We can use this to rewrite our `ReserveBookByTitle` command to use methods:
485
536
 
486
537
  ```ruby
487
- chained_command =
488
- Cuprum::Command.new do
489
- 'Example value'
538
+ class ReserveBookByTitle < Cuprum::Result
539
+ private
540
+
541
+ def check_user_status(user)
542
+ CheckUserStatus.new(user)
490
543
  end
491
- .yield_result do |result|
492
- Cuprum::Result.new(error: 'Example error')
544
+
545
+ def create_book_reservation(book:, user:)
546
+ CreateBookReservation.new(book: book, user: user)
493
547
  end
494
- .yield_result do |result|
495
- "The last result was a #{result.success? ? 'success' : 'failure'}."
548
+
549
+ def find_book_by_title(title)
550
+ FindBookByTitle.new.call(title)
496
551
  end
497
552
 
498
- # The first command creates a passing result with a value of 'Example value'.
499
- #
500
- # This is passed to the first yield_result block, which creates a new result
501
- # with the same value and the string 'Example error' as the error object. It is
502
- # then passed to the next command in the chain.
503
- #
504
- # Finally, the second yield_result block is called, which checks the status of
505
- # the passed result. Since the final block does not return the previous result,
506
- # the previous result is discarded and a new result is created with the string
507
- # value starting with 'The last result was ...'.
508
- result = chained_command.call
509
- result.class #=> Cuprum::Result
510
- result.value #=> 'The last result was a failure.'
511
- result.success? #=> true
512
- result.error #=> nil
553
+ def process(title:, user:)
554
+ step :check_user_status, user
555
+
556
+ book = step :find_book_by_title, title
557
+
558
+ create_book_reservation, book: book, user: user
559
+ end
560
+ end
513
561
  ```
514
562
 
515
- Like `#chain`, `#yield_result` can be given an `on: value` keyword.
563
+ In this case, our methods simply delegate to our previously defined commands. However, a more complex example could include other logic in each method, or even a sequence of steps defining subtasks for the method. The only requirement is that the method returns a result. You can use the `#success` helpers to wrap a non-result value, or the `#failure` helper to generate a failing result.
516
564
 
517
- Under the hood, both `#chain` and `#tap_result` are implemented on top of `#yield_result`.
565
+ #### Using Steps Outside Of Commands
518
566
 
519
- #### Protected Chaining Methods
567
+ Steps can also be used outside of a command. For example, a controller action might define a sequence of steps to run when the corresponding endpoint is called.
520
568
 
521
- Each Command also defines the `#chain!`, `#tap_result!`, and `#yield_result!` methods - note the imperative `!`. These methods behave identically to their non-imperative counterparts, but they modify the current command directly instead of creating a clone. They are also protected methods, so they cannot be called from outside the command itself. These methods are designed for use when defining commands.
569
+ To use steps outside of a command, include the `Cuprum::Steps` module. Then, each sequence of steps should be wrapped in a `#steps` block as follows:
522
570
 
523
571
  ```ruby
524
- # We subclass the build command, which will be executed first.
525
- class CreateCommentCommand < BuildCommentCommand
526
- include Cuprum::Chaining
527
- include Cuprum::Processing
528
- #
529
- def initialize
530
- # After the build step is run, we validate the comment.
531
- chain!(ValidateCommentCommand.new)
532
- #
533
- # If the validation passes, we then save the comment.
534
- chain!(SaveCommentCommand.new, on: :success)
535
- end
572
+ steps do
573
+ step { check_something }
574
+
575
+ obj = step { find_something }
576
+
577
+ step :do_something, with: obj
536
578
  end
579
+ ```
537
580
 
538
- Comment.count #=> 0
581
+ Each step will be executed in sequence until a failing result is returned by the block or method. The `#steps` block will return that failing result. If no step returns a failing result, then the return value of the block will be wrapped in a result and returned by `#steps`.
539
582
 
540
- body = 'Why do hot dogs come in packages of ten, and hot dog buns come in ' \
541
- 'packages of eight?'
542
- result = CreateCommentCommand.new.call({ user_id: '12345', body: body })
583
+ Let's consider the example of a controller action for creating a new resource. This would have several steps, each of which can fail:
543
584
 
544
- result.value #=> an instance of Comment with the given user_id and body.
545
- result.success? #=> true
546
- Comment.count #=> 1 - the comment was added to the database
585
+ - First, we build a new instance of the resource with the provided attributes. This can fail if the attributes are incompatible with the resource, e.g. with extra attributes not included in the resource's table columns.
586
+ - Second, we run validations on the resource itself. This can fail if the attributes do not match the expected format.
587
+ - Finally, we persist the resource to the database. This can fail if the record violates any database constraints, or if the database itself is unavailable.
588
+
589
+ ```ruby
590
+ class BooksController
591
+ include Cuprum::Steps
547
592
 
548
- result = CreateCommentCommand.new.call({ user_id: nil, body: body })
593
+ def create
594
+ attributes = params[:books]
595
+ result = steps do
596
+ @book = step :build_book, attributes
549
597
 
550
- result.value #=> an instance of Comment with the given user_id and body.
551
- result.success? #=> false
552
- result.error #=> ["User id can't be blank"]
553
- Comment.count #=> 1 - the comment was not added to the database
598
+ step :run_validations, @book
599
+
600
+ step :persist_book, book
601
+ end
602
+
603
+ result.success ? redirect_to(@book) : render(:edit)
604
+ end
605
+
606
+ private
607
+
608
+ def build_book(attributes)
609
+ success(Book.new(attributes))
610
+ rescue InvalidAttributes
611
+ failure('attributes are invalid')
612
+ end
613
+
614
+ def persist_book(book)
615
+ book.save ? success(book) : failure('unable to persist book')
616
+ end
617
+
618
+ def run_validations(book)
619
+ book.valid? ? success : failure('book is invalid')
620
+ end
621
+ end
554
622
  ```
555
623
 
624
+ A few things to note about this example. First, we have a couple of examples of wrapping existing code in a result, both by rescuing exceptions (in `#build_book`) or by checking a returned status (in `#persist_book`). Second, note that each of our helper methods can be reused in other controller actions. For even more encapsulation and reusability, the next step might be to convert those methods to commands of their own.
625
+
626
+ You can define even more complex logic by defining multiple `#steps` blocks. Each block represents a series of tasks that will terminate on the first failure. Steps blocks can even be nested in one another, or inside a `#process` method.
627
+
556
628
  ### Results
557
629
 
558
630
  require 'cuprum'
@@ -1073,276 +1145,4 @@ operation.success? #=> true
1073
1145
 
1074
1146
  ## Reference
1075
1147
 
1076
- ### Cuprum::BuiltIn::IdentityCommand
1077
-
1078
- require 'cuprum/built_in/identity_command'
1079
-
1080
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
1081
-
1082
- Cuprum::BuiltIn::IdentityCommand defines the following methods:
1083
-
1084
- #### `#call`
1085
-
1086
- call(value) #=> Cuprum::Result
1087
-
1088
- Returns a result, whose `#value` is equal to the given value.
1089
-
1090
- ### Cuprum::BuiltIn::IdentityOperation
1091
-
1092
- require 'cuprum/built_in/identity_operation'
1093
-
1094
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
1095
-
1096
- Cuprum::BuiltIn::IdentityOperation defines the following methods:
1097
-
1098
- #### `#call`
1099
-
1100
- call(value) #=> Cuprum::BuiltIn::IdentityOperation
1101
-
1102
- Sets the last result to a new result, whose `#value` is equal to the given value.
1103
-
1104
- ### Cuprum::BuiltIn::NullCommand
1105
-
1106
- require 'cuprum/built_in/null_command'
1107
-
1108
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
1109
-
1110
- Cuprum::BuiltIn::NullCommand defines the following methods:
1111
-
1112
- #### `#call`
1113
-
1114
- call(*args, **keywords) { ... } #=> Cuprum::Result
1115
-
1116
- Returns a result with nil value. Any arguments or keywords are ignored.
1117
-
1118
- ### Cuprum::BuiltIn::NullOperation
1119
-
1120
- require 'cuprum/built_in/null_operation'
1121
-
1122
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
1123
-
1124
- Cuprum::BuiltIn::NullOperation defines the following methods:
1125
-
1126
- #### `#call`
1127
-
1128
- call(*args, **keywords) { ... } #=> Cuprum::BuiltIn::NullOperation
1129
-
1130
- Sets the last result to a result with nil value. Any arguments or keywords are ignored.
1131
-
1132
- ### Cuprum::Command
1133
-
1134
- require 'cuprum'
1135
-
1136
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommand)
1137
-
1138
- A Cuprum::Command defines the following methods:
1139
-
1140
- #### `#initialize`
1141
-
1142
- initialize { |*arguments, **keywords, &block| ... } #=> Cuprum::Command
1143
-
1144
- Returns a new instance of Cuprum::Command. If a block is given, the `#call` method will wrap the block and set the result `#value` to the return value of the block. This overrides the implementation in `#process`, if any.
1145
-
1146
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#initialize-instance_method)
1147
-
1148
- #### `#call`
1149
-
1150
- call(*arguments, **keywords) { ... } #=> Cuprum::Result
1151
-
1152
- Executes the logic encoded in the constructor block, or the #process method if no block was passed to the constructor.
1153
-
1154
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#call-instance_method)
1155
-
1156
- #### `#chain`
1157
-
1158
- chain(on: nil) { |result| ... } #=> Cuprum::Command
1159
-
1160
- Registers a command or block to run after the current command, or after the last chained command if the current command already has one or more chained command(s). This creates and modifies a copy of the current command. See Chaining Commands, below.
1161
-
1162
- chain(command, on: nil) #=> Cuprum::Command
1163
-
1164
- The command will be passed the `#value` of the previous command result as its parameter, and the result of the chained command will be returned (or passed to the next chained command, if any).
1165
-
1166
- The block will be passed the #result of the previous command as its parameter.
1167
-
1168
- If the block returns a Cuprum::Result (or an object responding to `#to_cuprum_result`), the block result will be returned (or passed to the next chained command, if any). If the block returns any other value (including `nil`), the `#result` of the previous command will be returned or passed to the next command.
1169
-
1170
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#chain-instance_method)
1171
-
1172
- #### `#chain!`
1173
-
1174
- *(Protected Method)*
1175
-
1176
- chain!(on: nil) { |result| ... } #=> Cuprum::Command
1177
-
1178
- chain!(command, on: nil) #=> Cuprum::Command
1179
-
1180
- As `#chain`, but modifies the current command instead of creating a clone.
1181
-
1182
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#chain!-instance_method)
1183
-
1184
- #### `#tap_result`
1185
-
1186
- tap_result(on: nil) { |previous_result| } #=> Cuprum::Result
1187
-
1188
- 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.
1189
-
1190
- If the `on` parameter is set to `:success`, the block will be called if the last result is successful. If the `on` parameter is set to `:failure`, the block will be called if the last result is failing. Finally, if the `on` parameter is set to `:always` or to `nil`, the block will always be called.
1191
-
1192
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#tap_result-instance_method)
1193
-
1194
- #### `#tap_result!`
1195
-
1196
- *(Protected Method)*
1197
-
1198
- tap_result!(on: nil) { |previous_result| } #=> Cuprum::Result
1199
-
1200
- As `#tap_result`, but modifies the current command instead of creating a clone.
1201
-
1202
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#tap_result!-instance_method)
1203
-
1204
- #### `#yield_result`
1205
-
1206
- yield_result(on: nil) { |previous_result| } #=> Cuprum::Result
1207
-
1208
- 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.
1209
-
1210
- If the `on` parameter is set to `:success`, the block will be called if the last result is successful. If the `on` parameter is set to `:failure`, the block will be called if the last result is failing. Finally, if the `on` parameter is set to `:always` or to `nil`, the block will always be called.
1211
-
1212
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#yield_result-instance_method)
1213
-
1214
- #### `#yield_result!`
1215
-
1216
- *(Protected Method)*
1217
-
1218
- yield_result!(on: nil) { |previous_result| } #=> Cuprum::Result
1219
-
1220
- As `#yield_result`, but modifies the current command instead of creating a clone.
1221
-
1222
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#yield_result!-instance_method)
1223
-
1224
- ### Cuprum::Operation
1225
-
1226
- require 'cuprum'
1227
-
1228
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
1229
-
1230
- A Cuprum::Operation inherits the methods from Cuprum::Command (see above), and defines the following additional methods:
1231
-
1232
- #### `#called?`
1233
-
1234
- called?() #=> true, false
1235
-
1236
- True if the operation has been called and there is a result available by calling `#result` or one of the delegated methods, otherwise false.
1237
-
1238
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#called%3F-instance_method)
1239
-
1240
- #### `#reset!`
1241
-
1242
- reset!()
1243
-
1244
- Clears the most recent result and resets `#called?` to false. This frees the result and any linked data for garbage collection. It also clears any internal state from the operation.
1245
-
1246
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#reset!-instance_method)
1247
-
1248
- #### `#result`
1249
-
1250
- result() #=> Cuprum::Result
1251
-
1252
- The most recent result, from the previous time `#call` was executed for the operation.
1253
-
1254
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#result-instance_method)
1255
-
1256
- ### Cuprum::Result
1257
-
1258
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
1259
-
1260
- A Cuprum::Result defines the following methods:
1261
-
1262
- #### `#==`
1263
-
1264
- ==(other) #=> true, false
1265
-
1266
- Performs a fuzzy comparison with the other object. At a minimum, the other object must respond to `#value`, `#success?`, `#error` and the values of `other.value`, `other.success?`, and `other.error` must be equal to the corresponding value on the result. Returns true if all values match, otherwise returns false.
1267
-
1268
- #### `#error`
1269
-
1270
- error() #=> nil
1271
-
1272
- The error generated by the command, or `nil` if no error was generated.
1273
-
1274
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#error-instance_method)
1275
-
1276
- #### `#failure?`
1277
-
1278
- failure?() #=> true, false
1279
-
1280
- True if the command generated an error or was marked as failing. Otherwise false.
1281
-
1282
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#failure%3F-instance_method)
1283
-
1284
- #### `#success?`
1285
-
1286
- success?() #=> true, false
1287
-
1288
- True if the command did not generate an error, or the result has an error but was marked as passing. Otherwise false.
1289
-
1290
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#success%3F-instance_method)
1291
-
1292
- #### `#value`
1293
-
1294
- value() #=> Object
1295
-
1296
- The value returned by the command. For example, for an increment command that added 1 to a given integer, the `#value` of the result object would be the incremented integer.
1297
-
1298
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#value-instance_method)
1299
-
1300
- ### Cuprum::Utilities::InstanceSpy
1301
-
1302
- require 'cuprum/utils/instance_spy'
1303
-
1304
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FUtils%2FInstanceSpy)
1305
-
1306
- Utility module for instrumenting calls to the #call method of any instance of a command class. This can be used to unobtrusively test the functionality of code that calls a command without providing a reference to the command instance, such as chained commands or methods that create and call a command instance.
1307
-
1308
- #### `::clear_spies`
1309
-
1310
- clear_spies() #=> nil
1311
-
1312
- Retires all spies. Subsequent calls to the #call method on command instances will not be mirrored to existing spy objects. Calling this method after each test or example that uses an instance spy is recommended.
1313
-
1314
- after(:example) { Cuprum::Utils::InstanceSpy.clear_spies }
1315
-
1316
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#clear_spies%3F-instance_method)
1317
-
1318
- #### `::spy_on`
1319
-
1320
- spy_on(command_class) #=> InstanceSpy
1321
- spy_on(command_class) { |spy| ... } #=> nil
1322
-
1323
- Finds or creates a spy object for the given module or class. Each time that the #call method is called for an object of the given type, the spy's #call method will be invoked with the same arguments and block. If `#spy_on` is called with a block, the instance spy will be yielded to the block; otherwise, the spy will be returned.
1324
-
1325
- # Observing calls to instances of a command.
1326
- spy = Cuprum::Utils::InstanceSpy.spy_on(CustomCommand)
1327
-
1328
- expect(spy).to receive(:call).with(1, 2, 3, four: '4')
1329
-
1330
- CustomCommand.new.call(1, 2, 3, four: '4')
1331
-
1332
- # Observing calls to a chained command.
1333
- spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedCommand)
1334
-
1335
- expect(spy).to receive(:call)
1336
-
1337
- Cuprum::Command.new {}.
1338
- chain { |result| ChainedCommand.new.call(result) }.
1339
- call
1340
-
1341
- # Block syntax
1342
- Cuprum::Utils::InstanceSpy.spy_on(CustomCommand) do |spy|
1343
- expect(spy).to receive(:call)
1344
-
1345
- CustomCommand.new.call
1346
- end
1347
-
1348
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#spy_on%3F-instance_method)
1148
+ Method and class documentation is available courtesy of [RubyDoc](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master).