cuprum 0.9.0 → 0.11.0.rc.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -9
- data/DEVELOPMENT.md +45 -50
- data/README.md +728 -536
- data/lib/cuprum.rb +12 -6
- data/lib/cuprum/built_in.rb +3 -1
- data/lib/cuprum/built_in/identity_command.rb +6 -4
- data/lib/cuprum/built_in/identity_operation.rb +4 -2
- data/lib/cuprum/built_in/null_command.rb +5 -3
- data/lib/cuprum/built_in/null_operation.rb +4 -2
- data/lib/cuprum/command.rb +37 -59
- data/lib/cuprum/command_factory.rb +50 -24
- data/lib/cuprum/currying.rb +79 -0
- data/lib/cuprum/currying/curried_command.rb +116 -0
- data/lib/cuprum/error.rb +44 -10
- data/lib/cuprum/errors.rb +2 -0
- data/lib/cuprum/errors/command_not_implemented.rb +6 -3
- data/lib/cuprum/errors/operation_not_called.rb +6 -6
- data/lib/cuprum/errors/uncaught_exception.rb +55 -0
- data/lib/cuprum/exception_handling.rb +50 -0
- data/lib/cuprum/matcher.rb +90 -0
- data/lib/cuprum/matcher_list.rb +150 -0
- data/lib/cuprum/matching.rb +232 -0
- data/lib/cuprum/matching/match_clause.rb +65 -0
- data/lib/cuprum/middleware.rb +210 -0
- data/lib/cuprum/operation.rb +23 -15
- data/lib/cuprum/processing.rb +10 -14
- data/lib/cuprum/result.rb +2 -4
- data/lib/cuprum/result_helpers.rb +22 -0
- data/lib/cuprum/rspec/be_a_result.rb +11 -2
- data/lib/cuprum/rspec/be_a_result_matcher.rb +22 -9
- data/lib/cuprum/rspec/be_callable.rb +14 -0
- data/lib/cuprum/steps.rb +233 -0
- data/lib/cuprum/utils.rb +3 -1
- data/lib/cuprum/utils/instance_spy.rb +37 -30
- data/lib/cuprum/version.rb +14 -11
- metadata +36 -21
- data/lib/cuprum/chaining.rb +0 -420
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3938b7e4b35db9560c46f0c4233c4ee50972e34e66145a238ba8415af3998b80
|
4
|
+
data.tar.gz: b78ca19367cfb764110b32849a191bcd0519fbf4668c554b3af15b81313990b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5593d6900fb03df0c53681922e180722491d56d3b60281b881e7e9e04118d8cbea5b8e590df80b205f44fd21bcdfacdc2a79c83f8657e09b3e0fa2128e9f742
|
7
|
+
data.tar.gz: 18a2d36932b71428dc3cf93ffd767844c87eddf105666838907372d1276e873aaffc988d1d6543e413896670d0bde4a51ca7f9a4cd3bfa6088f565faca85db1d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,90 @@
|
|
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
|
+
|
73
|
+
## 0.9.1
|
74
|
+
|
75
|
+
### Operations
|
76
|
+
|
77
|
+
Delegate Operation#status to the most recent result.
|
78
|
+
|
79
|
+
### RSpec
|
80
|
+
|
81
|
+
The #be_a_passing_result macro now automatically adds a `with_error(nil)` expectation.
|
82
|
+
|
83
|
+
- This causes the error (if any) to be displayed when matching a failing result.
|
84
|
+
- This may break some cases when expecting a passing result that still has an error; in these cases, add an error expectation to the call, e.g. `expect().to be_a_passing_result.with_error(some_error)`.
|
85
|
+
|
86
|
+
Improve failure message of BeAResultMatcher under some circumstances.
|
87
|
+
|
3
88
|
## 0.9.0
|
4
89
|
|
5
90
|
The "'Tis Not Too Late To Seek A Newer World" Update
|
@@ -101,21 +186,21 @@ The "Name Not Found For NullFunction" Update.
|
|
101
186
|
|
102
187
|
Added the `Cuprum::warn` helper, which prints a warning message. By default, `::warn` delegates to `Kernel#warn`, but can be configured (e.g. to call a Logger) by setting `Cuprum::warning_proc=` with a Proc that accepts one argument (the message to display).
|
103
188
|
|
104
|
-
|
189
|
+
### Operations
|
105
190
|
|
106
191
|
The implementation of `Cuprum::Operation` has been extracted to a module at `Cuprum::Operation::Mixin`, allowing users to easily convert an existing function class or instance to an operation.
|
107
192
|
|
108
|
-
|
193
|
+
### Results
|
109
194
|
|
110
195
|
Implemented `Cuprum::Result#==` as a fuzzy comparison, allowing a result to be equal to any object with the same value and status.
|
111
196
|
|
112
197
|
Implemented `Cuprum::Result#empty?`, which returns true for a new result and false for a result with a value, with non-empty errors, a result with set status, or a halted result.
|
113
198
|
|
114
|
-
|
199
|
+
### Utilities
|
115
200
|
|
116
201
|
Added the `Cuprum::Utils::InstanceSpy` module to empower testing of code that calls a function without providing a reference, such as some chained functions.
|
117
202
|
|
118
|
-
|
203
|
+
### Built In Functions
|
119
204
|
|
120
205
|
Added the `NullFunction` and `NullOperation` predefined classes, which do nothing when called and return a result with no errors and a value of nil.
|
121
206
|
|
@@ -125,7 +210,7 @@ Added the `IdentityFunction` and `IdentityOperation` predefined classes, which r
|
|
125
210
|
|
126
211
|
The "Halt And Catch Fire" Update.
|
127
212
|
|
128
|
-
|
213
|
+
### Functions
|
129
214
|
|
130
215
|
Can now call `#success!` or `#failure!` in a function block or `#process` method to override the default, error-based status for the result. This allows for a passing result that still has errors, or a failing result that does not have explicit errors.
|
131
216
|
|
@@ -135,11 +220,11 @@ Can now generate results with custom error objects by overriding the `#build_err
|
|
135
220
|
|
136
221
|
Fixed an inconsistency issue when a function block or `#process` method returned an instance of `Cuprum::Result`.
|
137
222
|
|
138
|
-
|
223
|
+
### Operations
|
139
224
|
|
140
225
|
Calling `#call` on an operation now returns the operation instance.
|
141
226
|
|
142
|
-
|
227
|
+
### Results
|
143
228
|
|
144
229
|
Can now call `#success!` or `#failure!` to override the default, error-based status.
|
145
230
|
|
@@ -149,11 +234,11 @@ Can now call `#halt!` and check the `#halted?` status. A halted result will prev
|
|
149
234
|
|
150
235
|
The "Nothing To Lose But Your Chains" Update.
|
151
236
|
|
152
|
-
|
237
|
+
### Functions
|
153
238
|
|
154
239
|
Now support chaining via the `#chain`, `#then`, and `#else` methods.
|
155
240
|
|
156
|
-
|
241
|
+
### Results
|
157
242
|
|
158
243
|
Can pass a value and/or an errors object to the constructor.
|
159
244
|
|
data/DEVELOPMENT.md
CHANGED
@@ -1,77 +1,38 @@
|
|
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
|
10
|
-
|
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
|
-
|
21
|
-
### Matcher
|
22
|
-
|
23
|
-
- Handle success(), failure(), failure(SomeError) cases.
|
24
|
-
- Custom matcher to handle additional cases - halted, pending, etc?
|
25
|
-
|
26
3
|
## Version 1.0.0
|
27
4
|
|
28
|
-
|
29
|
-
|
30
|
-
- Integration specs.
|
31
|
-
- Configuration option to raise, warn, ignore discarded results.
|
32
|
-
- Code cleanup: Hash syntax, remove end comments, remove file headers
|
33
|
-
|
34
|
-
### Commands
|
5
|
+
The "Look On My Works, Ye Mighty, and Despair" Update
|
35
6
|
|
36
|
-
-
|
37
|
-
- :clear_errors => true option on #chain
|
7
|
+
- Documentation pass.
|
38
8
|
|
39
|
-
|
40
|
-
|
41
|
-
- MapCommand - wraps a command (or proc) and returns Result with value, errors
|
42
|
-
as array
|
43
|
-
- RetryCommand
|
44
|
-
|
45
|
-
### Documentation
|
46
|
-
|
47
|
-
Chaining Case Study: |
|
9
|
+
Steps Case Study: |
|
48
10
|
|
49
11
|
CMS application - creating a new post.
|
50
12
|
Directory has many Posts
|
51
13
|
Post has a Content
|
52
|
-
Content has many ContentVersions
|
53
14
|
Post has many Tags
|
54
15
|
|
55
16
|
Find Directory
|
56
17
|
Create Post
|
57
18
|
Create Content
|
58
|
-
Create ContentVersion
|
59
19
|
Tags.each { FindOrCreate Tag }
|
20
|
+
Publish Post # Requires that post have content
|
60
21
|
|
61
22
|
## Future Versions
|
62
23
|
|
24
|
+
Add `.rbs` files
|
25
|
+
|
63
26
|
### Commands
|
64
27
|
|
65
|
-
-
|
28
|
+
- Implement #<<, #>> composition methods.
|
29
|
+
- Calls commands in order passing values.
|
30
|
+
- Return Result early on Failure (or not Success), otherwise final Result.
|
66
31
|
|
67
|
-
####
|
32
|
+
#### DSL
|
68
33
|
|
69
34
|
- ::process - shortcut for defining #process
|
70
35
|
- ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
|
71
|
-
- chaining methods:
|
72
|
-
- ::chain (::success, ::failure):
|
73
|
-
on #initialize, chains the given command. Can be given a command class
|
74
|
-
(if ::new takes no arguments) or a block that returns a command.
|
75
36
|
- constructor methods:
|
76
37
|
- Programmatically generate a constructor method. Raises an error if
|
77
38
|
#initialize is defined. Automatically sets instance variables on initialize,
|
@@ -82,4 +43,38 @@ Chaining Case Study: |
|
|
82
43
|
optional arguments and their default values.
|
83
44
|
- ::keywords - sets keyword arguments; same arguments as ::arguments.
|
84
45
|
|
85
|
-
####
|
46
|
+
#### Dependency Injection
|
47
|
+
|
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](#
|
8
|
-
- [Operations](#
|
9
|
-
- [Results](#
|
10
|
-
- [Errors](#
|
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 [
|
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,
|
27
|
-
|
28
|
-
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
329
|
+
class RepeatCommand
|
330
|
+
def initialize(count)
|
331
|
+
@count = count
|
332
|
+
end
|
281
333
|
|
282
|
-
|
283
|
-
name_command
|
284
|
-
.chain(pluralize_command)
|
285
|
-
.chain(underscore_command)
|
334
|
+
private
|
286
335
|
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
303
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
312
|
-
|
385
|
+
class ShippingMethod < Cuprum::Command
|
386
|
+
private
|
313
387
|
|
314
|
-
|
315
|
-
|
388
|
+
def process(book:, user:)
|
389
|
+
return DeliverEbook.new(user.email) if book.ebook?
|
316
390
|
|
317
|
-
|
318
|
-
first_command.call #=> Outputs 'First command!' to STDOUT.
|
319
|
-
```
|
391
|
+
return ShipDomestic.new(user.address) if user.address&.domestic?
|
320
392
|
|
321
|
-
|
393
|
+
return ShipInternational.new(user.address) if user.address&.international?
|
322
394
|
|
323
|
-
|
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
|
-
|
333
|
-
|
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
|
-
|
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
|
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
|
-
|
406
|
+
### Command Steps
|
349
407
|
|
350
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
375
|
-
|
376
|
-
|
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
|
-
|
386
|
-
|
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
|
-
|
397
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
419
|
-
|
420
|
-
|
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
|
-
|
423
|
-
|
424
|
-
.tap_result do |result|
|
425
|
-
puts "The result value was #{result.inspect}"
|
426
|
-
end
|
471
|
+
end
|
472
|
+
```
|
427
473
|
|
428
|
-
|
429
|
-
|
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 #=> '
|
483
|
+
result.error #=> 'not authorized to reserve book'
|
434
484
|
```
|
435
485
|
|
436
|
-
|
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
|
-
|
440
|
-
|
441
|
-
|
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
|
-
|
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
|
-
|
446
|
-
|
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
|
-
|
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
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
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.
|
519
|
+
result.value #=> an instance of BookReservation
|
520
|
+
```
|
467
521
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
488
|
-
Cuprum::
|
489
|
-
|
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
|
-
|
492
|
-
|
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
|
-
|
495
|
-
|
570
|
+
|
571
|
+
def persist_book(book)
|
572
|
+
book.save ? success(book) : failure('unable to persist book')
|
496
573
|
end
|
497
574
|
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
585
|
+
### Handling Exceptions
|
520
586
|
|
521
|
-
|
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
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
-
|
600
|
+
class SafeCommand < UnsafeCommand
|
601
|
+
include Cuprum::ExceptionHandling
|
602
|
+
end
|
539
603
|
|
540
|
-
|
541
|
-
|
542
|
-
result = CreateCommentCommand.new.call({ user_id: '12345', body: body })
|
604
|
+
UnsafeCommand.new.call
|
605
|
+
#=> raises a StandardError
|
543
606
|
|
544
|
-
result
|
545
|
-
|
546
|
-
|
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
|
-
|
616
|
+
Exception handling is *not* included by default - add `include Cuprum::ExceptionHandling` to your command classes to use this feature.
|
549
617
|
|
550
|
-
|
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
|
-
|
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
|
-
|
689
|
+
<a id="Errors"></a>
|
628
690
|
|
629
|
-
|
691
|
+
## Errors
|
630
692
|
|
631
|
-
|
693
|
+
require 'cuprum/error'
|
632
694
|
|
633
|
-
A `Cuprum::Error` encapsulates a specific failure state of a Command. Each Error has a `#message` property
|
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(
|
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(
|
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
|
-
###
|
749
|
+
### Comparing Errors
|
672
750
|
|
673
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1488
|
+
## Built In Commands
|
1013
1489
|
|
1014
1490
|
Cuprum includes a small number of predefined commands and their equivalent operations.
|
1015
1491
|
|
1016
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|