cuprum 0.9.1 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/DEVELOPMENT.md +42 -53
  4. data/README.md +728 -536
  5. data/lib/cuprum.rb +12 -6
  6. data/lib/cuprum/built_in.rb +3 -1
  7. data/lib/cuprum/built_in/identity_command.rb +6 -4
  8. data/lib/cuprum/built_in/identity_operation.rb +4 -2
  9. data/lib/cuprum/built_in/null_command.rb +5 -3
  10. data/lib/cuprum/built_in/null_operation.rb +4 -2
  11. data/lib/cuprum/command.rb +37 -59
  12. data/lib/cuprum/command_factory.rb +50 -24
  13. data/lib/cuprum/currying.rb +79 -0
  14. data/lib/cuprum/currying/curried_command.rb +116 -0
  15. data/lib/cuprum/error.rb +44 -10
  16. data/lib/cuprum/errors.rb +2 -0
  17. data/lib/cuprum/errors/command_not_implemented.rb +6 -3
  18. data/lib/cuprum/errors/operation_not_called.rb +6 -6
  19. data/lib/cuprum/errors/uncaught_exception.rb +55 -0
  20. data/lib/cuprum/exception_handling.rb +50 -0
  21. data/lib/cuprum/matcher.rb +90 -0
  22. data/lib/cuprum/matcher_list.rb +150 -0
  23. data/lib/cuprum/matching.rb +232 -0
  24. data/lib/cuprum/matching/match_clause.rb +65 -0
  25. data/lib/cuprum/middleware.rb +210 -0
  26. data/lib/cuprum/operation.rb +17 -15
  27. data/lib/cuprum/processing.rb +10 -14
  28. data/lib/cuprum/result.rb +2 -4
  29. data/lib/cuprum/result_helpers.rb +22 -0
  30. data/lib/cuprum/rspec/be_a_result.rb +10 -1
  31. data/lib/cuprum/rspec/be_a_result_matcher.rb +5 -7
  32. data/lib/cuprum/rspec/be_callable.rb +14 -0
  33. data/lib/cuprum/steps.rb +233 -0
  34. data/lib/cuprum/utils.rb +3 -1
  35. data/lib/cuprum/utils/instance_spy.rb +37 -30
  36. data/lib/cuprum/version.rb +13 -10
  37. metadata +34 -19
  38. data/lib/cuprum/chaining.rb +0 -420
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f867011719a69bd7080c77d450fedfee736ef6df5be2b8a2d11fe3b1006a3890
4
- data.tar.gz: 48109c5460f817fedb8c6a46b84649a87d9c21e2d2dbadba31b31e32bdf20977
3
+ metadata.gz: 7f8976c5a3b4ebc7fd1704d8f796391ec961834492a79cbb65bcef01e0bca9e4
4
+ data.tar.gz: b01921b268068f191e2d96c43fb6b0a6c6fe924277f2ba696e66665079f4ed6b
5
5
  SHA512:
6
- metadata.gz: b272e3d32edf7449a83a8513be56f8e68e2474872621d055f5947b6605ec5c29ecbd4acc7e7155d1efd5ee122689dc564eaab312b499ff129d996c6e6599ff90
7
- data.tar.gz: 7ad63c437101991668b8a060fa355633f46b269ad55d609fad81bf5e993b998974f43ae61bc96d2c781a10e29d83178a7b2770ec2540a121f54bf6261530fcce
6
+ metadata.gz: 600cf6c8df2e8b78047e82049ed27e471f9f51946ae126234c091949657982a019cfb6c514840c2375e689a0a7dc4463e8b9157b1e18be0f03c2a69f4b50a2b6
7
+ data.tar.gz: 40cb494bae1201111b8042eb6e181bc63a82c520cadb2dfed53d9896041ee6934500e07eace1f386c31bc31a9e586065364c93f5fae0a43908d4bec51a50c321
data/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ The "One Giant Leap" Update
6
+
7
+ **Note:** This will be the last feature update before 1.0.
8
+
9
+ ### Commands
10
+
11
+ Implemented the `#to_proc` method, which allows for constructs such as `array.map(&command)`.
12
+
13
+ Removed the deprecated chaining mechanic.
14
+
15
+ #### Currying
16
+
17
+ Added support for currying block parameters.
18
+
19
+ #### Exception Handling
20
+
21
+ Defined `Cuprum::ExceptionHandling` to rescue uncaught errors in commands.
22
+
23
+ Exception handling is *not* included by default - add `include Cuprum::ExceptionHandling` to your command classes to use this feature.
24
+
25
+ #### Middleware
26
+
27
+ Defined `Cuprum::Middleware` to define a wrapper that calls other commands.
28
+
29
+ #### Steps
30
+
31
+ Deprecated calling `#step` with a method name.
32
+
33
+ The error type and message when calling `#steps` without a block has changed.
34
+
35
+ ### Errors
36
+
37
+ Errors can now define their comparable properties by passing additional keywords to the constructor (or `super` for error subclasses).
38
+
39
+ Added the `#type` method and property.
40
+
41
+ Added serialization via the `#as_json` method.
42
+
43
+ ### Matchers
44
+
45
+ Implemented `Cuprum::Matcher`, which provides a way to handle different result cases.
46
+
47
+ ### RSpec
48
+
49
+ Added the `#be_callable` macro, which is a wrapper for `#respond_to` that references the `#process` method.
50
+
51
+ RSpec matchers are no longer automatically included when the macro is required. To use the Cuprum matchers, add `config.include Cuprum::RSpec::Matchers` to your RSpec configuration, or add `include Cuprum::RSpec::Matchers` to your example groups.
52
+
53
+ ## 0.10.0
54
+
55
+ The "One Small Step" Update
56
+
57
+ **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.
58
+
59
+ ### Commands
60
+
61
+ Implemented the `#curry` method, which performs partial application of arguments or keywords.
62
+
63
+ #### Chaining
64
+
65
+ 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 0.11.
66
+
67
+ #### Steps
68
+
69
+ Implemented the `#step` method, which extracts the value of the called command (on a success) or halts execution (on a failure).
70
+
71
+ 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.
72
+
3
73
  ## 0.9.1
4
74
 
5
75
  ### Operations
data/DEVELOPMENT.md CHANGED
@@ -1,78 +1,35 @@
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
3
+ ## Version 1.0.0
8
4
 
9
- The "One Small Step" Update
5
+ The "Look On My Works, Ye Mighty, and Despair" Update
10
6
 
11
- ### Commands
12
-
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
+ - Documentation pass.
23
8
 
24
9
  Steps Case Study: |
25
10
 
26
11
  CMS application - creating a new post.
27
12
  Directory has many Posts
28
13
  Post has a Content
29
- Content has many ContentVersions
30
14
  Post has many Tags
31
15
 
32
16
  Find Directory
33
17
  Create Post
34
18
  Create Content
35
- Create ContentVersion
36
19
  Tags.each { FindOrCreate Tag }
37
-
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
- ### Commands
59
-
60
- - 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
20
+ Publish Post # Requires that post have content
68
21
 
69
22
  ## Future Versions
70
23
 
24
+ Add `.rbs` files
25
+
71
26
  ### Commands
72
27
 
73
- - command currying
28
+ - Implement #<<, #>> composition methods.
29
+ - Calls commands in order passing values.
30
+ - Return Result early on Failure (or not Success), otherwise final Result.
74
31
 
75
- #### Cuprum::DSL
32
+ #### DSL
76
33
 
77
34
  - ::process - shortcut for defining #process
78
35
  - ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
@@ -89,3 +46,35 @@ Steps Case Study: |
89
46
  #### Dependency Injection
90
47
 
91
48
  - shorthand for referencing a sequence of operations
49
+
50
+ ### Commands - Built In
51
+
52
+ - MapCommand - wraps a command (or proc) and returns Result with value, errors
53
+ as array
54
+ - RetryCommand - takes command, retry count
55
+ - optional only:, except: - restrict what errors are retried
56
+
57
+ ### Middleware
58
+
59
+ - Implement Command.subclass
60
+ - Curries constructor arguments
61
+ - Implement Cuprum::AppliedMiddleware < Cuprum::Command
62
+ - has readers #root (Class), #middleware (Array<Class>)
63
+ - #initialize
64
+ - initializes root command (passing constructor parameters)
65
+ - initializes each middleware command
66
+ - if Class defining .instance, call .instance
67
+ - if Class, call .new
68
+ - if Proc, call #call with constructor parameters
69
+ - calls Middleware.apply and caches as private #applied
70
+ - #call
71
+ - delegates to #applied
72
+
73
+ ### RSpec
74
+
75
+ - call_command_step matcher
76
+ - (optionally) alias be_a_result family as have_result for operations
77
+
78
+ ### Steps::Strict
79
+
80
+ - #step raises exception unless block or method returns a result
data/README.md CHANGED
@@ -4,10 +4,11 @@ An opinionated implementation of the Command pattern for Ruby applications. Cupr
4
4
 
5
5
  It defines the following concepts:
6
6
 
7
- - [Commands](#label-Commands) - A function-like object that responds to `#call` and returns a `Result`.
8
- - [Operations](#label-Operations) - A stateful `Command` that wraps and delegates to its most recent `Result`.
9
- - [Results](#label-Results) - An immutable data object with a status (either `:success` or `:failure`), and either a `#value` or an `#error` object.
10
- - [Errors](#label-Errors) - Encapsulates a failure state of a command.
7
+ - [Commands](#Commands) - A function-like object that responds to `#call` and returns a `Result`.
8
+ - [Operations](#Operations) - A stateful `Command` that wraps and delegates to its most recent `Result`.
9
+ - [Results](#Results) - An immutable data object with a status (either `:success` or `:failure`), and optional `#value` and/or `#error` objects.
10
+ - [Errors](#Errors) - Encapsulates a failure state of a command.
11
+ - [Matchers](#Matchers) - Define handling for results based on status, error, and value.
11
12
 
12
13
  ## About
13
14
 
@@ -18,26 +19,24 @@ Traditional frameworks such as Rails focus on the objects of your application -
18
19
  - **Consistency:** Use the same Commands to underlie controller actions, worker processes and test factories.
19
20
  - **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.
20
21
  - **Testability:** Because the logic is extracted from unnecessary context, testing its behavior is much cleaner and easier.
21
- - **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.
22
+ - **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 [step](#label-Command+Steps) feature allows for complex control flows.
22
23
  - **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.
23
24
 
24
25
  ### Alternatives
25
26
 
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.
27
+ If you want to extract your logic but Cuprum is not the right solution for you, there are a number of alternatives, including
28
+ [ActiveInteraction](https://github.com/AaronLasseigne/active_interaction),
29
+ [Dry::Monads](https://dry-rb.org/gems/dry-monads/),
30
+ [Interactor](https://github.com/collectiveidea/interactor),
31
+ [Trailblazer](http://trailblazer.to/) Operations,
32
+ and [Waterfall](https://github.com/apneadiving/waterfall).
32
33
 
33
34
  ### Compatibility
34
35
 
35
- Cuprum is tested against Ruby (MRI) 2.3 through 2.5.
36
+ Cuprum is tested against Ruby (MRI) 2.6 through 3.0.
36
37
 
37
38
  ### Documentation
38
39
 
39
- Method and class documentation is available courtesy of [RubyDoc](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master).
40
-
41
40
  Documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.
42
41
 
43
42
  ### License
@@ -54,13 +53,9 @@ To report a bug or submit a feature request, please use the [Issue Tracker](http
54
53
 
55
54
  To contribute code, please fork the repository, make the desired updates, and then provide a [Pull Request](https://github.com/sleepingkingstudios/cuprum/pulls). Pull requests must include appropriate tests for consideration, and all code must be properly formatted.
56
55
 
57
- ### Credits
58
-
59
- Hi, I'm Rob Smith, a Ruby Engineer and the developer of this library. I use these tools every day, but they're not just written for me. If you find this project helpful in your own work, or if you have any questions, suggestions or critiques, please feel free to get in touch! I can be reached [on GitHub](https://github.com/sleepingkingstudios/cuprum) or [via email](mailto:merlin@sleepingkingstudios.com). I look forward to hearing from you!
60
-
61
- ## Concepts
56
+ <a id="Commands"></a>
62
57
 
63
- ### Commands
58
+ ## Commands
64
59
 
65
60
  require 'cuprum'
66
61
 
@@ -68,9 +63,7 @@ Commands are the core feature of Cuprum. In a nutshell, each `Cuprum::Command` i
68
63
 
69
64
  Each Command implements a `#call` method that wraps your defined business logic and returns an instance of `Cuprum::Result`. The result has a status (either `:success` or `:failure`), and may have a `#value` and/or an `#error` object. For more details about Cuprum::Result, [see below](#label-Results).
70
65
 
71
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommand)
72
-
73
- #### Defining Commands
66
+ ### Defining Commands
74
67
 
75
68
  The recommended way to define commands is to create a subclass of `Cuprum::Command` and override the `#process` method.
76
69
 
@@ -141,7 +134,7 @@ inspect_command = Cuprum::Command.new(&:inspect) # Equivalent to above.
141
134
 
142
135
  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.
143
136
 
144
- #### Result Values
137
+ ### Result Values
145
138
 
146
139
  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`).
147
140
 
@@ -165,7 +158,7 @@ result.class #=> Cuprum::Result
165
158
  result.value #=> 'Greetings, programs!'
166
159
  ```
167
160
 
168
- #### Success, Failure, and Errors
161
+ ### Success, Failure, and Errors
169
162
 
170
163
  Each Result has a status, either `:success` or `:failure`. A Result will have a status of `:failure` when it was created with an error object. Otherwise, a Result will have a status of `:success`. Returning a failing Result from a Command indicates that something went wrong while executing the Command.
171
164
 
@@ -213,7 +206,61 @@ result.value #=> book
213
206
  book.published? #=> false
214
207
  ```
215
208
 
216
- #### Composing Commands
209
+ ### Command Currying
210
+
211
+ 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.
212
+
213
+ 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.
214
+
215
+ #### Currying Arguments
216
+
217
+ 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.
218
+
219
+ ```ruby
220
+ say_command = Cuprum::Command.new do |greeting, person|
221
+ "#{greeting}, #{person}!"
222
+ end
223
+ say_command.call('Hello', 'world')
224
+ #=> returns a result with value 'Hello, world!'
225
+ ```
226
+
227
+ 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.
228
+
229
+ ```ruby
230
+ greet_command = say_command.curry('Greetings')
231
+ greet_command.call('programs')
232
+ #=> returns a result with value 'Greetings, programs!'
233
+ ```
234
+
235
+ 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.
236
+
237
+ ```ruby
238
+ recruit_command = say_command.curry('Greetings', 'starfighter')
239
+ recruit_command.call
240
+ #=> returns a result with value 'Greetings, starfighter!'
241
+ ```
242
+
243
+ #### Currying Keywords
244
+
245
+ 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.
246
+
247
+ ```ruby
248
+ math_command = Cuprum::Command.new do |operands:, operation:|
249
+ operations.reduce(&operation)
250
+ end
251
+ math_command.call(operands: [2, 2], operation: :+)
252
+ #=> returns a result with value 4
253
+ ```
254
+
255
+ Our curried command still takes two keywords, but now the operation keyword is optional. It now defaults to :\*, for multiplication.
256
+
257
+ ```ruby
258
+ multiply_command = math_command.curry(operation: :*)
259
+ multiply_command.call(operands: [3, 3])
260
+ #=> returns a result with value 9
261
+ ```
262
+
263
+ ### Composing Commands
217
264
 
218
265
  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
266
 
@@ -224,6 +271,8 @@ increment_command.call(2).value #=> 3
224
271
  increment_command.call(3).value #=> 4
225
272
 
226
273
  add_command = Cuprum::Command.new do |addend, i|
274
+ # Here, we are composing commands together by calling the increment_command
275
+ # instance from inside the add_command definition.
227
276
  addend.times { i = increment_command(i).value }
228
277
 
229
278
  i
@@ -270,295 +319,308 @@ add_two_command.call(1).value #=> 3
270
319
  add_two_command.call(8).value #=> 10
271
320
  ```
272
321
 
273
- ### Chaining Commands
322
+ 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.
323
+
324
+ #### Commands As Arguments
274
325
 
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.
326
+ 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
327
 
277
328
  ```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 }
329
+ class RepeatCommand
330
+ def initialize(count)
331
+ @count = count
332
+ end
281
333
 
282
- table_name_command =
283
- name_command
284
- .chain(pluralize_command)
285
- .chain(underscore_command)
334
+ private
286
335
 
287
- result = table_name_command.call(LibraryCard)
288
- result.class #=> Cuprum::Result
289
- result.value #=> 'library_cards'
336
+ def process(command)
337
+ @count.times { command.call }
338
+ end
339
+ end
340
+
341
+ greet_command = Cuprum::Command.new { puts 'Greetings, programs!' }
342
+ repeat_command = RepeatCommand.new(3)
343
+ repeat_command.call(greet_command) #=> prints 'Greetings, programs!' 3 times
290
344
  ```
291
345
 
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.
346
+ 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
347
 
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:
348
+ 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.
349
+
350
+ 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
351
 
296
352
  ```ruby
297
- table_name_command =
298
- Cuprum::Command.new { |klass| klass.name }
299
- .chain { |str| str.pluralize }
300
- .chain { |str| str.underscore }
353
+ class DeliverEbook < Cuprum::Command; end
354
+
355
+ class ShipDomestic < Cuprum::Command; end
356
+
357
+ class ShipInternational < Cuprum::Command; end
358
+
359
+ class FulfillOrder < Cuprum::Command
360
+ def initialize(delivery_command)
361
+ @delivery_command = delivery_command
362
+ end
301
363
 
302
- table_name_command =
303
- Cuprum::Command.new(&:name).chain(&:pluralize).chain(&:underscore)
364
+ private
365
+
366
+ def process(book:, user:)
367
+ # Here we will check inventory, process payments, and so on. The final step
368
+ # is actually delivering the book to the user:
369
+ delivery_command.call(book: book, user: user)
370
+ end
371
+ end
304
372
  ```
305
373
 
306
- #### Chaining Details
374
+ 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).
375
+
376
+ #### Commands As Returned Values
377
+
378
+ 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.
379
+
380
+ 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.
307
381
 
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:
382
+ 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
383
 
310
384
  ```ruby
311
- first_command = Cuprum::Command.new { puts 'First command!' }
312
- first_command.call #=> Outputs 'First command!' to STDOUT.
385
+ class ShippingMethod < Cuprum::Command
386
+ private
313
387
 
314
- second_command = first_command.chain { puts 'Second command!' }
315
- second_command.call #=> Outputs 'First command!' then 'Second command!'.
388
+ def process(book:, user:)
389
+ return DeliverEbook.new(user.email) if book.ebook?
316
390
 
317
- # The original command is unchanged.
318
- first_command.call #=> Outputs 'First command!' to STDOUT.
319
- ```
391
+ return ShipDomestic.new(user.address) if user.address&.domestic?
320
392
 
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.
393
+ return ShipInternational.new(user.address) if user.address&.international?
322
394
 
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)
395
+ err = Cuprum::Error.new(message: 'user does not have a valid address')
331
396
 
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
397
+ failure(err)
398
+ end
399
+ end
342
400
  ```
343
401
 
344
- #### Conditional Chaining
402
+ 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
403
 
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`.
404
+ 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
405
 
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.
406
+ ### Command Steps
349
407
 
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`.
408
+ 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
409
 
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`.
410
+ 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
411
 
354
412
  ```ruby
355
- find_command =
356
- Cuprum::Command.new do |attributes|
357
- book = Book.where(id: attributes[:id]).first
358
-
359
- return book if book
413
+ triple_command = Cuprum::Command.new { |i| success(3 * i) }
360
414
 
361
- Cuprum::Result.new(error: 'Book not found')
362
- end
363
- create_command =
364
- Cuprum::Command.new do |attributes|
365
- book = Book.new(attributes)
366
-
367
- return book if book.save
415
+ int = 2
416
+ int = step { triple_command.call(int) } #=> returns 6
417
+ int = step { triple_command.call(int) } #=> returns 18
418
+ ```
368
419
 
369
- Cuprum::Result.new(error: book.errors.full_messages)
370
- end
420
+ 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
421
 
372
- find_or_create_command = find_command.chain(create_command, on: :failure)
422
+ 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
423
 
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
424
+ ```ruby
425
+ divide_command = Cuprum::Command.new do |dividend, divisor|
426
+ return failure('divide by zero') if divisor.zero?
384
427
 
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
428
+ success(dividend / divisor)
429
+ end
395
430
 
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"]
431
+ value = step { divide_command.call(10, 5) } #=> returns 2
432
+ value = step { divide_command.call(2, 0) } #=> throws :cuprum_failed_result
407
433
  ```
408
434
 
409
- ### Advanced Chaining
435
+ 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
436
 
411
- The `#tap_result` and `#yield_result` methods provide advanced control over the flow of chained commands.
437
+ 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
438
 
413
- #### Tap Result
439
+ - Validating that the user can reserve books. Maybe the user has too many unpaid fines.
440
+ - Finding the requested book in the library system. Maybe the requested title isn't in the system.
441
+ - Placing a reservation on the book. Maybe there are no copies of the book available to reserve.
414
442
 
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.
443
+ 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
444
 
417
445
  ```ruby
418
- command =
419
- Cuprum::Command.new do
420
- Cuprum::Result.new(value: 'Example value', error: 'Example error')
446
+ class CheckUserStatus < Cuprum::Command; end
447
+
448
+ class CreateBookReservation < Cuprum::Command; end
449
+
450
+ class FindBookByTitle < Cuprum::Command; end
451
+
452
+ class ReserveBookByTitle < Cuprum::Command
453
+ private
454
+
455
+ def process(title:, user:)
456
+ # If CheckUserStatus fails, #process will immediately return that result.
457
+ # For this step, we already have the user, so we don't need to use the
458
+ # result value.
459
+ step { CheckUserStatus.new.call(user) }
460
+
461
+ # Here, we are looking up the requested title. In this case, we will need
462
+ # the book object, so we save it as a variable. Notice that we don't need
463
+ # an explicit #value call - #step handles that for us.
464
+ book = step { FindBookByTitle.new.call(title) }
465
+
466
+ # Finally, we want to reserve the book. Since this is the last subtask, we
467
+ # don't strictly need to use #step. However, it's good practice, especially
468
+ # if we might need to add more steps to the command in the future.
469
+ step { CreateBookReservation.new.call(book: book, user: user) }
421
470
  end
422
- chained_command =
423
- command
424
- .tap_result do |result|
425
- puts "The result value was #{result.inspect}"
426
- end
471
+ end
472
+ ```
427
473
 
428
- # Prints 'The result value was "Example value"' to STDOUT.
429
- result = chained_command.call
474
+ 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`.
475
+
476
+ ```ruby
477
+ result = ReserveBookByTitle.new.call(
478
+ title: 'The C Programming Language',
479
+ user: 'Ed Dillinger'
480
+ )
430
481
  result.class #=> Cuprum::Result
431
- result.value #=> 'Example value'
432
482
  result.success? #=> false
433
- result.error #=> 'Example error'
483
+ result.error #=> 'not authorized to reserve book'
434
484
  ```
435
485
 
436
- Like `#chain`, `#tap_result` can be given an `on: value` keyword.
486
+ 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
487
 
438
488
  ```ruby
439
- find_book_command =
440
- Cuprum::Command.new do |book_id|
441
- book = Book.where(id: book_id).first
489
+ result = ReserveBookByTitle.new.call(
490
+ title: 'Using GOTO For Fun And Profit',
491
+ user: 'Alan Bradley'
492
+ )
493
+ result.class #=> Cuprum::Result
494
+ result.success? #=> false
495
+ result.error #=> 'title not found'
496
+ ```
442
497
 
443
- return book if book
498
+ 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
499
 
445
- Cuprum::Result.new(error: "Unable to find book with id #{book_id}")
446
- end
500
+ ```ruby
501
+ result = ReserveBookByTitle.new.call(
502
+ title: 'Design Patterns: Elements of Reusable Object-Oriented Software',
503
+ user: 'Alan Bradley'
504
+ )
505
+ result.class #=> Cuprum::Result
506
+ result.success? #=> false
507
+ result.error #=> 'no copies available'
508
+ ```
447
509
 
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
510
+ Finally, if each of the steps succeeds, the `#call` method will return the result of the final step.
456
511
 
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)
512
+ ```ruby
513
+ result = ReserveBookByTitle.new.call(
514
+ title: 'The C Programming Language',
515
+ user: 'Alan Bradley'
516
+ )
463
517
  result.class #=> Cuprum::Result
464
- result.value #=> an instance of Book
465
518
  result.success? #=> true
466
- result.error #=> nil
519
+ result.value #=> an instance of BookReservation
520
+ ```
467
521
 
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']
522
+ #### Using Steps Outside Of Commands
523
+
524
+ 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.
525
+
526
+ 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:
527
+
528
+ ```ruby
529
+ steps do
530
+ step { check_something }
531
+
532
+ obj = step { find_something }
533
+
534
+ step :do_something, with: obj
535
+ end
478
536
  ```
479
537
 
480
- #### Yield Result
538
+ 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`.
481
539
 
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.
540
+ Let's consider the example of a controller action for creating a new resource. This would have several steps, each of which can fail:
483
541
 
484
- Unlike `#chain`, the block in `#yield_result` yields the previous result, not the previous value.
542
+ - 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.
543
+ - Second, we run validations on the resource itself. This can fail if the attributes do not match the expected format.
544
+ - 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.
485
545
 
486
546
  ```ruby
487
- chained_command =
488
- Cuprum::Command.new do
489
- 'Example value'
547
+ class BooksController
548
+ include Cuprum::Steps
549
+
550
+ def create
551
+ attributes = params[:books]
552
+ result = steps do
553
+ @book = step :build_book, attributes
554
+
555
+ step :run_validations, @book
556
+
557
+ step :persist_book, book
558
+ end
559
+
560
+ result.success ? redirect_to(@book) : render(:edit)
490
561
  end
491
- .yield_result do |result|
492
- Cuprum::Result.new(error: 'Example error')
562
+
563
+ private
564
+
565
+ def build_book(attributes)
566
+ success(Book.new(attributes))
567
+ rescue InvalidAttributes
568
+ failure('attributes are invalid')
493
569
  end
494
- .yield_result do |result|
495
- "The last result was a #{result.success? ? 'success' : 'failure'}."
570
+
571
+ def persist_book(book)
572
+ book.save ? success(book) : failure('unable to persist book')
496
573
  end
497
574
 
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
575
+ def run_validations(book)
576
+ book.valid? ? success : failure('book is invalid')
577
+ end
578
+ end
513
579
  ```
514
580
 
515
- Like `#chain`, `#yield_result` can be given an `on: value` keyword.
581
+ 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.
516
582
 
517
- Under the hood, both `#chain` and `#tap_result` are implemented on top of `#yield_result`.
583
+ 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.
518
584
 
519
- #### Protected Chaining Methods
585
+ ### Handling Exceptions
520
586
 
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.
587
+ require 'cuprum/exception_handling'
588
+
589
+ Cuprum defines a utility module to rescue uncaught exceptions when calling a command.
522
590
 
523
591
  ```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)
592
+ class UnsafeCommand < Cuprum::Command
593
+ private
594
+
595
+ def process
596
+ raise 'Something went wrong.'
535
597
  end
536
598
  end
537
599
 
538
- Comment.count #=> 0
600
+ class SafeCommand < UnsafeCommand
601
+ include Cuprum::ExceptionHandling
602
+ end
539
603
 
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 })
604
+ UnsafeCommand.new.call
605
+ #=> raises a StandardError
543
606
 
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
607
+ result = SafeCommand.new.call
608
+ #=> a Cuprum::Result
609
+ result.error
610
+ #=> a Cuprum::Errors::UncaughtException error.
611
+ result.error.message
612
+ #=> 'uncaught exception in SafeCommand -' \
613
+ ' StandardError: Something went wrong.'
614
+ ```
547
615
 
548
- result = CreateCommentCommand.new.call({ user_id: nil, body: body })
616
+ Exception handling is *not* included by default - add `include Cuprum::ExceptionHandling` to your command classes to use this feature.
549
617
 
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
554
- ```
618
+ <a id="Results"></a>
555
619
 
556
- ### Results
620
+ ## Results
557
621
 
558
622
  require 'cuprum'
559
623
 
560
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
561
-
562
624
  A `Cuprum::Result` is a data object that encapsulates the result of calling a Cuprum command. Each result has a `#value`, an `#error` object (defaults to `nil`), and a `#status` (either `:success` or `:failure`, and accessible via the `#success?` and `#failure?` predicates).
563
625
 
564
626
  ```ruby
@@ -624,42 +686,58 @@ result.success? #=> true
624
686
  result.failure? #=> false
625
687
  ```
626
688
 
627
- ### Errors
689
+ <a id="Errors"></a>
628
690
 
629
- require 'cuprum/error'
691
+ ## Errors
630
692
 
631
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FError)
693
+ require 'cuprum/error'
632
694
 
633
- A `Cuprum::Error` encapsulates a specific failure state of a Command. Each Error has a `#message` property, which defaults to nil.
695
+ A `Cuprum::Error` encapsulates a specific failure state of a Command. Each Error has a `#message` property which defaults to nil. Each Error also has a `#type` property which is determined by the Error class or subclass, although it can be overridden by passing a `:type` parameter to the constructor.
634
696
 
635
697
  ```ruby
636
698
  error = Cuprum::Error.new
637
699
  error.message => # nil
700
+ error.type => 'cuprum.error'
638
701
 
639
702
  error = Cuprum::Error.new(message: 'Something went wrong.')
640
703
  error.message => # 'Something went wrong.'
704
+
705
+ error = Cuprum::Error.new(type: 'example.custom_type')
706
+ error.type => 'example.custom_type'
641
707
  ```
642
708
 
643
709
  Each application should define its own failure states as errors. For example, a typical web application might define the following errors:
644
710
 
645
711
  ```ruby
646
712
  class NotFoundError < Cuprum::Error
713
+ TYPE = 'example.errors.not_found'
714
+
647
715
  def initialize(resource:, resource_id:)
648
716
  @resource = resource
649
717
  @resource_id = resource_id
650
718
 
651
- super(message: "#{resource} not found with id #{resource_id}")
719
+ super(
720
+ message: "#{resource} not found with id #{resource_id}",
721
+ resource: resource,
722
+ resource_id: resource_id
723
+ )
652
724
  end
653
725
 
654
726
  attr_reader :resource, :resource_id
655
727
  end
656
728
 
657
729
  class ValidationError < Cuprum::Error
730
+ TYPE = 'example.errors.validation'
731
+
658
732
  def initialize(resource:, errors:)
659
733
  @resource = resource
660
734
  @errors = errors
661
735
 
662
- super(message: "#{resource} was invalid")
736
+ super(
737
+ errors: errors,
738
+ message: "#{resource} was invalid",
739
+ resource: resource
740
+ )
663
741
  end
664
742
 
665
743
  attr_reader :resource, :errors
@@ -668,11 +746,185 @@ end
668
746
 
669
747
  It is optional but recommended to use a `Cuprum::Error` when returning a failed result from a command.
670
748
 
671
- ### Operations
749
+ ### Comparing Errors
672
750
 
673
- require 'cuprum'
751
+ There are circumstances when it is useful to compare Error objects, such as when writing tests to specify the failure states of a command. To accommodate this, you can pass additional properties to `Cuprum::Error.new` (or to `super` when defining a subclass). These "comparable properties", plus the type and message (if any), are used to compare the errors.
752
+
753
+ An instance of `Cuprum::Error` is equal to another (using the `#==` equality comparison) if and only if the two errors have the same `class` and the two errors have the same comparable properties.
754
+
755
+ ```ruby
756
+ red = Cuprum::Error.new(message: 'wrong color', color: 'red')
757
+ blue = Cuprum::Error.new(message: 'wrong color', color: 'blue')
758
+ crimson = Cuprum::Error.new(message: 'wrong color', color: 'red')
759
+
760
+ red == blue
761
+ #=> false
762
+
763
+ red == crimson
764
+ #=> true
765
+ ```
766
+
767
+ This can be particularly important when defining Error subclasses. By passing the constructor parameters to `super`, below, we will be able to compare different instances of the `NotFoundError`. The errors will only be equal if they have the same message, resource, and resource_id properties.
768
+
769
+ ```ruby
770
+ class NotFoundError < Cuprum::Error
771
+ def initialize(resource:, resource_id:)
772
+ @resource = resource
773
+ @resource_id = resource_id
774
+
775
+ super(
776
+ message: "#{resource} not found with id #{resource_id}",
777
+ resource: resource,
778
+ resource_id: resource_id,
779
+ )
780
+ end
781
+
782
+ attr_reader :resource, :resource_id
783
+ end
784
+ ```
785
+
786
+ Finally, by overriding the `#comparable_properties` method, you can customize how Error instances are compared.
787
+
788
+ ```ruby
789
+ class WrongColorError < Cuprum::Error
790
+ def initialize(color:, shape:)
791
+ super(message: "the #{shape} is the wrong color")
792
+
793
+ @color = color
794
+ @shape = shape
795
+ end
796
+
797
+ attr_reader :color
798
+
799
+ protected
800
+
801
+ def comparable_properties
802
+ { color: color }
803
+ end
804
+ end
805
+ ```
806
+
807
+ ### Serializing Errors
808
+
809
+ Some use cases require serializing error objects - for example, rendering an error response as JSON. To handle this, `Cuprum::Error` defines an `#as_json` method, which generates a representation of the error as a `Hash` with `String` keys. By default, this includes the `#type` and `#message` (if any) as well as an empty `:data` Hash.
810
+
811
+ Subclasses can override this behavior to include additional information in the `:data` Hash, which should always use `String` keys and have values composed of basic types and data structures. For example, if an error is passed a `Class`, consider serializing the name of the class to `:data`.
812
+
813
+ ```ruby
814
+ error = Cuprum::Error.new
815
+ error.as_json #=> { data: {}, message: nil, type: 'cuprum.error' }
816
+
817
+ error = Cuprum::Error.new(message: 'Something went wrong.')
818
+ error.as_json #=> { data: {}, message: 'Something went wrong.', type: 'cuprum.error' }
819
+
820
+ error = Cuprum::Error.new(type: 'example.custom_error')
821
+ error.as_json #=> { data: {}, message: nil, type: 'example.custom_error' }
822
+
823
+ class ModuleError < Cuprum::Error
824
+ TYPE = 'example.module_error'
674
825
 
675
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
826
+ def initialize(actual:)
827
+ @actual = actual
828
+ message = "Expected a Module, but #{actual.name} is a Class"
829
+
830
+ super(actual: actual, message: message)
831
+ end
832
+
833
+ attr_reader :actual
834
+
835
+ private
836
+
837
+ def as_json_data
838
+ { actual: actual.name }
839
+ end
840
+ end
841
+
842
+ error = ModuleError.new(actual: String)
843
+ error.as_json #=>
844
+ # {
845
+ # data: { actual: 'String' },
846
+ # message: 'Expected a Module, but String is a Class',
847
+ # type: 'example.module_error'
848
+ # }
849
+ ```
850
+
851
+ **Important Note:** Be careful when serializing error data - this may expose sensitive information or internal details about your system that you don't want to display to users. Recommended practice is to have a whitelist of serializable errors; all other errors will display a generic error message instead.
852
+
853
+ <a id="Middleware"></a>
854
+
855
+ ## Middleware
856
+
857
+ ```ruby
858
+ require 'cuprum/middleware'
859
+ ```
860
+
861
+ A middleware command wraps the execution of another command, allowing the developer to compose functionality without an explicit wrapper command. Because the middleware is responsible for calling the wrapped command, it has control over when that command is called, with what parameters, and how the command result is handled.
862
+
863
+ To use middleware, start by defining a middleware command. This can either be a class that includes Cuprum::Middleware, or a command instance that extends Cuprum::Middleware. Each middleware command's #process method takes as its first argument the wrapped command. By convention, any additional arguments and any keywords or a block are passed to the wrapped command, but some middleware will override ths behavior.
864
+
865
+ ```ruby
866
+ class ExampleCommand < Cuprum::Command
867
+ private def process(**options)
868
+ return failure(options[:error]) if options[:error]
869
+
870
+ "Options: #{options.inspect}"
871
+ end
872
+ end
873
+
874
+ class LoggingMiddleware < Cuprum::Command
875
+ include Cuprum::Middleware
876
+
877
+ # The middleware injects a logging step before the wrapped command is
878
+ # called. Notice that this middleware is generic, and can be used with
879
+ # virtually any other command.
880
+ private def process(next_command, *args, **kwargs)
881
+ Logger.info("Calling command #{next_command.class}")
882
+
883
+ super
884
+ end
885
+ end
886
+
887
+ command = Command.new { |**opts| "Called with #{opts.inspect}" }
888
+ middleware = LoggingMiddleware.new
889
+ result = middleware.call(command, { id: 0 })
890
+ #=> logs "Calling command ExampleCommand"
891
+ result.value
892
+ #=> "Options: { id: 0 }"
893
+ ```
894
+
895
+ When defining #process, make sure to either call super or call the wrapped command directly, unless the middleware is specifically intended not to call the wrapped command under those circumstances.
896
+
897
+ Middleware is powerful because it allows the developer to manipulate the parameters passed to a command, add handling to a result, or even intercept or override the command execution. These are some of the possible use cases for middleware:
898
+
899
+ - Injecting code before or after a command.
900
+ - Changing the parameters passed to a command.
901
+ - Adding behavior based on the command result.
902
+ - Overriding the command behavior based on the parameters.
903
+
904
+ ```ruby
905
+ class AuthenticationMiddleware < Cuprum::Command
906
+ include Cuprum::Middleware
907
+
908
+ # The middleware finds the current user based on the given keywords. If
909
+ # a valid user is found, the user is then passed on to the command.
910
+ # If a user is not found, then the middleware will immediately halt (due
911
+ # to #step) and return the failing result from the authentication
912
+ # command.
913
+ private def process(next_command, *args, **kwargs)
914
+ current_user = step { AuthenticateUser.new.call(**kwargs) }
915
+
916
+ super(next_command, *args, current_user: current_user, **kwargs)
917
+ end
918
+ end
919
+ ```
920
+
921
+ Middleware is loosely coupled, meaning that one middleware command can wrap any number of other commands. One example would be logging middleware, which could record when a command is called and with what parameters. For a more involved example, consider authorization in a web application. If individual actions are defined as commands, then a single authorization middleware class could wrap each individual action, reducing both the testing burden and the amount of code that must be maintained.
922
+
923
+ <a id="Operations"></a>
924
+
925
+ ## Operations
926
+
927
+ require 'cuprum'
676
928
 
677
929
  An Operation is like a Command, but with two key differences. First, an Operation retains a reference to the result object from the most recent time the operation was called, and delegates the methods defined by `Cuprum::Result` to the most recent result. This allows a called Operation to replace a `Cuprum::Result` in any code that expects or returns a result. Second, the `#call` method returns the operation instance, rather than the result itself.
678
930
 
@@ -703,15 +955,239 @@ Like a Command, an Operation can be defined directly by passing an implementatio
703
955
 
704
956
  An operation inherits the `#call` method from Cuprum::Command (see above), and delegates the `#value`, `#error`, `#success?`, and `#failure` methods to the most recent result. If the operation has not been called, these methods will return default values.
705
957
 
706
- #### The Operation Mixin
707
-
708
- [Module Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation%2FMixin)
958
+ ### The Operation Mixin
709
959
 
710
960
  The implementation of `Cuprum::Operation` is defined by the `Cuprum::Operation::Mixin` module, which provides the methods defined above. Any command class or instance can be converted to an operation by including (for a class) or extending (for an instance) the operation mixin.
711
961
 
712
- ### Command Factories
962
+ <a id="Matchers"></a>
963
+
964
+ ## Matchers
965
+
966
+ require 'cuprum/matcher'
967
+
968
+ A Matcher provides a simple DSL for defining behavior based on a Cuprum result object.
969
+
970
+ ```ruby
971
+ matcher = Cuprum::Matcher.new do
972
+ match(:failure) { 'Something went wrong' }
973
+
974
+ match(:success) { 'Ok' }
975
+ end
976
+
977
+ matcher.call(Cuprum::Result.new(status: :failure))
978
+ #=> 'Something went wrong'
979
+
980
+ matcher.call(Cuprum::Result.new(status: :success))
981
+ #=> 'Ok'
982
+ ```
983
+
984
+ First, the matcher defines possible matches using the `.match` method. This can either be called on a subclass of `Cuprum::Matcher` or by passing a block to the constructor, as above. Each match clause must have the matching status, and a block that is executed when a result matches that clause. The clause can also filter by the result value or error (see Matching Values And Errors, below).
985
+
986
+ Once the matcher has found a matching clause, it then calls the block in the clause definition. If the block accepts an argument, the result is passed to the block; otherwise, the block is called with no arguments. This allows the match clause to use the error or value of the result.
987
+
988
+ ```ruby
989
+ matcher = Cuprum::Matcher.new do
990
+ match(:failure) { |result| result.error.message }
991
+ end
992
+
993
+ error = Cuprum::Error.new(message: 'An error has occurred.')
994
+ matcher.call(Cuprum::Result.new(error: error))
995
+ #=> 'An error has occurred.'
996
+ ```
997
+
998
+ If the result does not match any of the clauses, a `Cuprum::Matching::NoMatchError` is raised.
999
+
1000
+ ```ruby
1001
+ matcher = Cuprum::Matcher.new do
1002
+ match(:success) { :ok }
1003
+ end
1004
+
1005
+ matcher.call(Cuprum::Result.new(status: :failure))
1006
+ #=> raises Cuprum::Matching::NoMatchError
1007
+ ```
1008
+
1009
+ ### Matching Values And Errors
1010
+
1011
+ In addition to a status, match clauses can specify the type of the value or error of a matching result. The error or value must be a Class or Module, and the clause will then match only results whose error or value is an instance of the specified Class or Module (or a subclass of the Class).
1012
+
1013
+ ```ruby
1014
+ class MagicSmokeError < Cuprum::Error; end
1015
+
1016
+ matcher = Cuprum::Matcher.new do
1017
+ match(:failure) { 'Something went wrong.' }
1018
+
1019
+ match(:failure, error: Cuprum::Error) do |result|
1020
+ "ERROR: #{result.error.message}"
1021
+ end
1022
+
1023
+ match(:failure, error: MagicSmokeError) do
1024
+ "PANIC: #{result.error.message}"
1025
+ end
1026
+ end
1027
+
1028
+ matcher.call(Cuprum::Result.new(status: :failure))
1029
+ #=> 'Something went wrong.'
1030
+
1031
+ error = Cuprum::Error.new(message: 'An error has occurred.')
1032
+ matcher.call(Cuprum::Result.new(error: error)
1033
+ #=> 'ERROR: An error has occurred.'
1034
+
1035
+ error = MagicSmokeError.new(message: 'The magic smoke is escaping.')
1036
+ matcher.call(Cuprum::Result.new(error: error))
1037
+ #=> 'PANIC: The magic smoke is escaping.'
1038
+ ```
1039
+
1040
+ The matcher will always apply the most specific match clause. In the example above, the result with a `MagicSmokeError` matches all three clauses, but only the final clause is executed.
1041
+
1042
+ You can also specify the value of a matching result:
1043
+
1044
+ ```ruby
1045
+ matcher = Cuprum::Matcher.new do
1046
+ match(:success, value: String) { 'a String' }
1047
+
1048
+ match(:success, value: Symbol) { 'a Symbol' }
1049
+ end
1050
+
1051
+ matcher.call(Cuprum::Result.new(value: 'Greetings, programs!'))
1052
+ #=> 'a String'
1053
+
1054
+ matcher.call(Cuprum::Result.new(value: :greetings_starfighter))
1055
+ #=> 'a Symbol'
1056
+ ```
1057
+
1058
+ ### Using Matcher Classes
1059
+
1060
+ Matcher classes allow you to define custom behavior that can be called as part of the defined match clauses.
1061
+
1062
+ ```ruby
1063
+ class LogMatcher < Cuprum::Matcher
1064
+ match(:failure) { |result| log(:error, result.error.message) }
1065
+
1066
+ match(:success) { log(:info, 'Ok') }
1067
+
1068
+ def log(level, message)
1069
+ puts "#{level.upcase}: #{message}"
1070
+ end
1071
+ end
1072
+
1073
+ matcher = LogMatcher.new
1074
+ matcher.call(Cuprum::Result.new(status: :success))
1075
+ #=> prints "INFO: Ok" to STDOUT
1076
+ ```
1077
+
1078
+ Match clauses are also inherited by matcher subclasses. Inherited clauses are sorted the same as clauses defined on the matcher directly - the most specific clause is matched first, followed by less specific clauses and finally the generic clause (if any) for that result status.
1079
+
1080
+ ```ruby
1081
+ class CustomLogMatcher < Cuprum::Matcher
1082
+ match(:failure, error: ReallyBadError) do |result|
1083
+ log(:fatal, result.error.message)
1084
+ end
1085
+ end
1086
+
1087
+ matcher = CustomLogMatcher.new
1088
+ result = Cuprum::Result.new(error: Cuprum::Error.new('Something went wrong.'))
1089
+ matcher.call(result)
1090
+ #=> prints "ERROR: Something went wrong." to STDOUT
1091
+
1092
+ result = Cuprum::Result.new(error: ReallyBadError.new('Computer on fire.'))
1093
+ matcher.call(result)
1094
+ #=> prints "FATAL: Computer on fire." to STDOUT
1095
+ ```
1096
+
1097
+ ### Match Contexts
1098
+
1099
+ Match contexts provide an alternative to defining custom matcher classes - instead of defining custom behavior in the matcher itself, the match clauses can be executed in the context of another object.
1100
+
1101
+ ```ruby
1102
+ class Inflector
1103
+ def capitalize(message)
1104
+ message.split(' ').map(&:capitalize).join(' ')
1105
+ end
1106
+ end
1107
+
1108
+ matcher = Cuprum::Matcher.new(inflector) do
1109
+ match(:success) { |result| capitalize(result.value) }
1110
+ end
1111
+ matcher.call(Cuprum::Result.new(value: 'greetings starfighter'))
1112
+ #=> 'Greetings Starfighter'
1113
+ ```
1114
+
1115
+ For example, a controller in a web framework might need to define behavior for handling different success and error cases for business logic that is defined as Commands. The controller itself defines methods such as `#render` and `#redirect` - by creating a matcher using the controller as the match context, the matcher can call upon those methods to generate a response.
1116
+
1117
+ You can also call an existing matcher with a new context. The `#with_context` method returns a copy of the matcher with the given object set as the match context.
1118
+
1119
+ ```ruby
1120
+ matcher = Cuprum::Matcher.new do
1121
+ match(:success) { |result| capitalize(result.value) }
1122
+ end
1123
+ matcher
1124
+ .with_context(inflector)
1125
+ .call(Cuprum::Result.new(value: 'greetings starfighter'))
1126
+ #=> 'Greetings Starfighter'
1127
+ ```
713
1128
 
714
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommandFactory)
1129
+ ### Matcher Lists
1130
+
1131
+ Matcher lists handle matching a result against an ordered group of matchers.
1132
+
1133
+ When given a result, a matcher list will check for the most specific matching clause in each of the matchers. A clause matching both the value and error will match first, followed by a clause matching only the result value or error, and finally a clause matching only the result status will match.
1134
+
1135
+ If none of the matchers have a clause that matches the result, a `Cuprum::Matching::NoMatchError` will be raised.
1136
+
1137
+ ```ruby
1138
+ generic_matcher = Cuprum::Matcher.new do
1139
+ match(:failure) { 'generic failure' }
1140
+ #
1141
+ match(:failure, error: CustomError) { 'custom failure' }
1142
+ end
1143
+ specific_matcher = Cuprum::Matcher.new do
1144
+ match(:failure, error: Cuprum::Error) { 'specific failure' }
1145
+ end
1146
+ matcher_list = Cuprum::MatcherList.new(
1147
+ [
1148
+ specific_matcher,
1149
+ generic_matcher
1150
+ ]
1151
+ )
1152
+
1153
+ generic_matcher = Cuprum::Matcher.new do
1154
+ match(:failure) { 'generic failure' }
1155
+
1156
+ match(:failure, error: CustomError) { 'custom failure' }
1157
+ end
1158
+ specific_matcher = Cuprum::Matcher.new do
1159
+ match(:failure, error: Cuprum::Error) { 'specific failure' }
1160
+ end
1161
+ matcher_list = Cuprum::MatcherList.new(
1162
+ [
1163
+ specific_matcher,
1164
+ generic_matcher
1165
+ ]
1166
+ )
1167
+
1168
+ # A failure without an error does not match the first matcher, so the
1169
+ # matcher list continues on to the next matcher in the list.
1170
+ result = Cuprum::Result.new(status: :failure)
1171
+ matcher_list.call(result)
1172
+ #=> 'generic failure'
1173
+
1174
+ # A failure with an error matches the first matcher.
1175
+ error = Cuprum::Error.new(message: 'Something went wrong.')
1176
+ result = Cuprum::Result.new(error: error)
1177
+ matcher_list.call(result)
1178
+ #=> 'specific failure'
1179
+
1180
+ # A failure with an error subclass still matches the first matcher, even
1181
+ # though the second matcher has a more exact match.
1182
+ error = CustomError.new(message: 'The magic smoke is escaping.')
1183
+ result = Cuprum::Result.new(error: error)
1184
+ matcher_list.call(result)
1185
+ #=> 'specific failure'
1186
+ ```
1187
+
1188
+ One use case for matcher lists would be in defining hierarchies of classes or objects that have matching functionality. For example, a generic controller class might define default success and failure behavior, an included mixin might provide handling for a particular scope of errors, and a specific controller might override the default behavior for a given action. Using a matcher list allows each class or module to define its own behavior as independent matchers, which the matcher list then composes together.
1189
+
1190
+ ## Command Factories
715
1191
 
716
1192
  Commands are powerful and flexible objects, but they do have a few disadvantages compared to traditional service objects which allow the developer to group together related functionality and shared implementation details. To bridge this gap, Cuprum implements the CommandFactory class. Command factories provide a DSL to quickly group together related commands and create context-specific command classes or instances.
717
1193
 
@@ -764,7 +1240,7 @@ book.author #=> 'Ursula K. Le Guin'
764
1240
  book.publisher #=> nil
765
1241
  ```
766
1242
 
767
- #### The ::command Method And A Command Class
1243
+ ### The ::command Method And A Command Class
768
1244
 
769
1245
  The first way to define a command for a factory is by calling the `::command` method and passing it the name of the command and a command class:
770
1246
 
@@ -776,7 +1252,7 @@ end
776
1252
 
777
1253
  This makes the command class available on a factory instance as `::Build`, and generates the `#build` method which returns an instance of `BuildBookCommand`.
778
1254
 
779
- #### The ::command Method And A Block
1255
+ ### The ::command Method And A Block
780
1256
 
781
1257
  By calling the `::command` method with a block, you can define a command with additional control over how the generated command. The block must return an instance of a subclass of Cuprum::Command.
782
1258
 
@@ -885,7 +1361,7 @@ ary = result.value #=> an array with the selected books
885
1361
  ary.count #=> 1
886
1362
  ```
887
1363
 
888
- #### The ::command_class Method
1364
+ ### The ::command_class Method
889
1365
 
890
1366
  The final way to define a command for a factory is calling the `::command_class` method with the command name and a block. The block must return a subclass (not an instance) of Cuprum::Command. This offers a balance between flexibility and power.
891
1367
 
@@ -1009,16 +1485,14 @@ books.count #=> 4
1009
1485
  books.include?(book) #=> true
1010
1486
  ```
1011
1487
 
1012
- ### Built In Commands
1488
+ ## Built In Commands
1013
1489
 
1014
1490
  Cuprum includes a small number of predefined commands and their equivalent operations.
1015
1491
 
1016
- #### IdentityCommand
1492
+ ### IdentityCommand
1017
1493
 
1018
1494
  require 'cuprum/built_in/identity_command'
1019
1495
 
1020
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
1021
-
1022
1496
  A pregenerated command that returns the value or result with which it was called.
1023
1497
 
1024
1498
  ```ruby
@@ -1028,12 +1502,10 @@ result.value #=> 'expected value'
1028
1502
  result.success? #=> true
1029
1503
  ```
1030
1504
 
1031
- #### IdentityOperation
1505
+ ### IdentityOperation
1032
1506
 
1033
1507
  require 'cuprum/built_in/identity_operation'
1034
1508
 
1035
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
1036
-
1037
1509
  A pregenerated operation that sets its result to the value or result with which it was called.
1038
1510
 
1039
1511
  ```ruby
@@ -1042,12 +1514,10 @@ operation.value #=> 'expected value'
1042
1514
  operation.success? #=> true
1043
1515
  ```
1044
1516
 
1045
- #### NullCommand
1517
+ ### NullCommand
1046
1518
 
1047
1519
  require 'cuprum/built_in/null_command'
1048
1520
 
1049
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
1050
-
1051
1521
  A pregenerated command that does nothing when called. Accepts any arguments.
1052
1522
 
1053
1523
  ```ruby
@@ -1057,12 +1527,10 @@ result.value #=> nil
1057
1527
  result.success? #=> true
1058
1528
  ```
1059
1529
 
1060
- #### NullOperation
1530
+ ### NullOperation
1061
1531
 
1062
1532
  require 'cuprum/built_in/null_operation'
1063
1533
 
1064
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
1065
-
1066
1534
  A pregenerated operation that does nothing when called. Accepts any arguments.
1067
1535
 
1068
1536
  ```ruby
@@ -1070,279 +1538,3 @@ operation = Cuprum::BuiltIn::NullOperation.new.call
1070
1538
  operation.value #=> nil
1071
1539
  operation.success? #=> true
1072
1540
  ```
1073
-
1074
- ## Reference
1075
-
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)