cuprum 0.7.0 → 0.10.0.rc.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +101 -9
- data/DEVELOPMENT.md +67 -45
- data/README.md +669 -693
- data/lib/cuprum.rb +1 -25
- data/lib/cuprum/built_in/identity_command.rb +4 -4
- data/lib/cuprum/chaining.rb +235 -144
- data/lib/cuprum/command.rb +24 -18
- data/lib/cuprum/command_factory.rb +300 -0
- data/lib/cuprum/currying.rb +78 -0
- data/lib/cuprum/currying/curried_command.rb +109 -0
- data/lib/cuprum/error.rb +37 -0
- data/lib/cuprum/errors.rb +6 -0
- data/lib/cuprum/errors/command_not_implemented.rb +35 -0
- data/lib/cuprum/errors/operation_not_called.rb +35 -0
- data/lib/cuprum/operation.rb +37 -28
- data/lib/cuprum/processing.rb +45 -96
- data/lib/cuprum/result.rb +52 -115
- data/lib/cuprum/result_helpers.rb +14 -105
- data/lib/cuprum/rspec.rb +8 -0
- data/lib/cuprum/rspec/be_a_result.rb +19 -0
- data/lib/cuprum/rspec/be_a_result_matcher.rb +286 -0
- data/lib/cuprum/steps.rb +275 -0
- data/lib/cuprum/utils/instance_spy.rb +9 -2
- data/lib/cuprum/version.rb +3 -3
- metadata +30 -8
- data/lib/cuprum/not_implemented_error.rb +0 -14
- data/lib/cuprum/utils/result_not_empty_warning.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8ca65deb82f58ec6a0b9706a7eb81f6edc191ecf54d22a9e3155a82370aec83e
|
4
|
+
data.tar.gz: e259228f7b6128c24a0974666165a0655c4188f1011519c8d9dea498c5d59008
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eafd3d6b85ca035cec50fdbbaacf3f3238cfa1a624de85f0663fdfcef4aeb91298f08ee11915631d3f82069222ca641279c243a6d2087a8b29cc0cf1bd01bd01
|
7
|
+
data.tar.gz: '080089f9d312a1019e934e7ed84e6df92b566c2bff83c9472671c0bed9f3738762345082d99f298af70eab33f1ec8af6809310ee2a3f90cd65544a618e812fb8'
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,97 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.10
|
4
|
+
|
5
|
+
The "One Small Step" Update
|
6
|
+
|
7
|
+
**Note:** This update may have backwards incompatible changes for versions of Ruby before 2.7 when creating commands whose last parameter is an arguments Hash. See [separation of positional and keyword arguments](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/) for more information.
|
8
|
+
|
9
|
+
### Commands
|
10
|
+
|
11
|
+
Implemented the `#curry` method, which performs partial application of arguments or keywords.
|
12
|
+
|
13
|
+
#### Chaining
|
14
|
+
|
15
|
+
Added deprecation warnings to all chaining methods, and `Cuprum::Command` no longer includes `Cuprum::Chaining` by default. The `Cuprum::Chaining` module will be removed in version 1.0.
|
16
|
+
|
17
|
+
#### Steps
|
18
|
+
|
19
|
+
Implemented the `#step` method, which extracts the value of the called command (on a success) or halts execution (on a failure).
|
20
|
+
|
21
|
+
Implemented the `#steps` method, which wraps a series of steps and returns first failing result, or the the last result if all steps are passing.
|
22
|
+
|
23
|
+
## 0.9.1
|
24
|
+
|
25
|
+
### Operations
|
26
|
+
|
27
|
+
Delegate Operation#status to the most recent result.
|
28
|
+
|
29
|
+
### RSpec
|
30
|
+
|
31
|
+
The #be_a_passing_result macro now automatically adds a `with_error(nil)` expectation.
|
32
|
+
|
33
|
+
- This causes the error (if any) to be displayed when matching a failing result.
|
34
|
+
- 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)`.
|
35
|
+
|
36
|
+
Improve failure message of BeAResultMatcher under some circumstances.
|
37
|
+
|
38
|
+
## 0.9.0
|
39
|
+
|
40
|
+
The "'Tis Not Too Late To Seek A Newer World" Update
|
41
|
+
|
42
|
+
Major refactoring of Command processing and the Result object. This update is **not** backwards compatible.
|
43
|
+
|
44
|
+
### Commands
|
45
|
+
|
46
|
+
Removed the `#success` and `#failure` chaining helpers.
|
47
|
+
|
48
|
+
Permanently removed the deprecated ResultHelpers mixin.
|
49
|
+
|
50
|
+
### Errors
|
51
|
+
|
52
|
+
Added `Cuprum::Error`, which encapsulates the failure state of a result. It is *recommended*, but not required, that when creating a failing Result, the `:error` property be set to an instance of `Cuprum::Error`.
|
53
|
+
|
54
|
+
### Results
|
55
|
+
|
56
|
+
Results are now nominally immutable objects. All mutator methods have been removed, including `#failure!`, `#success!`, and `#update`. The `#empty?` predicate has also been removed.
|
57
|
+
|
58
|
+
Updated the constructor to take the following keyword arguments: `:value`, `:error`, and `:status`.
|
59
|
+
|
60
|
+
- The status can now be overridden on a new Result by passing in the `:status`.
|
61
|
+
- Resolves an issue when attempting to instantiate a result with a Hash value.
|
62
|
+
- *Note:* The value must now be passed in as a keyword.
|
63
|
+
- *Note:* The `:errors` keyword has been renamed to `:error`.
|
64
|
+
|
65
|
+
Removed the `:halted` status.
|
66
|
+
|
67
|
+
### Other Changes
|
68
|
+
|
69
|
+
Removed the `Cuprum#warn` functionality.
|
70
|
+
|
71
|
+
### Upgrade Notes
|
72
|
+
|
73
|
+
Anywhere a new `Cuprum::Result` is created directly, update the arguments to match the new `value:` and `error:` keywords.
|
74
|
+
|
75
|
+
Anywhere the `#result` is referenced inside a command, instead return the desired value directly, or return a result with the desired error.
|
76
|
+
|
77
|
+
Anywhere a command is chained with the `#success` or `#failure` shorthand, use the full `chain(on: :success)` or `chain(on: :failure)` format.
|
78
|
+
|
79
|
+
## 0.8.0
|
80
|
+
|
81
|
+
The "We Have The Technology" Update.
|
82
|
+
|
83
|
+
### Commands
|
84
|
+
|
85
|
+
Added protected chaining methods `#chain!`, `#tap_result!` and `#yield_result!`. These methods function as their non-imperative counterparts, but add the chained command or block to the current command instead of a clone.
|
86
|
+
|
87
|
+
Removed the ResultHelpers mixin from the default Command class. To use the result helper methods, include Cuprum::ResultHelpers in your command class.
|
88
|
+
|
89
|
+
Removed the #build_errors helper - each Result is now responsible for building its own errors object. To use a custom errors object, define a subclass of Cuprum::Result and override its #build_errors method, then update your Command's #build_result method to use your custom result class.
|
90
|
+
|
91
|
+
### Command Factory
|
92
|
+
|
93
|
+
Implemented the CommandFactory class, which provides a builder interface and DSL for grouping and creating commands with a common purpose or with shared configuration.
|
94
|
+
|
3
95
|
## 0.7.0
|
4
96
|
|
5
97
|
The "To Strive, To Seek, To Find, And Not To Yield" Update.
|
@@ -44,21 +136,21 @@ The "Name Not Found For NullFunction" Update.
|
|
44
136
|
|
45
137
|
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).
|
46
138
|
|
47
|
-
|
139
|
+
### Operations
|
48
140
|
|
49
141
|
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.
|
50
142
|
|
51
|
-
|
143
|
+
### Results
|
52
144
|
|
53
145
|
Implemented `Cuprum::Result#==` as a fuzzy comparison, allowing a result to be equal to any object with the same value and status.
|
54
146
|
|
55
147
|
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.
|
56
148
|
|
57
|
-
|
149
|
+
### Utilities
|
58
150
|
|
59
151
|
Added the `Cuprum::Utils::InstanceSpy` module to empower testing of code that calls a function without providing a reference, such as some chained functions.
|
60
152
|
|
61
|
-
|
153
|
+
### Built In Functions
|
62
154
|
|
63
155
|
Added the `NullFunction` and `NullOperation` predefined classes, which do nothing when called and return a result with no errors and a value of nil.
|
64
156
|
|
@@ -68,7 +160,7 @@ Added the `IdentityFunction` and `IdentityOperation` predefined classes, which r
|
|
68
160
|
|
69
161
|
The "Halt And Catch Fire" Update.
|
70
162
|
|
71
|
-
|
163
|
+
### Functions
|
72
164
|
|
73
165
|
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.
|
74
166
|
|
@@ -78,11 +170,11 @@ Can now generate results with custom error objects by overriding the `#build_err
|
|
78
170
|
|
79
171
|
Fixed an inconsistency issue when a function block or `#process` method returned an instance of `Cuprum::Result`.
|
80
172
|
|
81
|
-
|
173
|
+
### Operations
|
82
174
|
|
83
175
|
Calling `#call` on an operation now returns the operation instance.
|
84
176
|
|
85
|
-
|
177
|
+
### Results
|
86
178
|
|
87
179
|
Can now call `#success!` or `#failure!` to override the default, error-based status.
|
88
180
|
|
@@ -92,11 +184,11 @@ Can now call `#halt!` and check the `#halted?` status. A halted result will prev
|
|
92
184
|
|
93
185
|
The "Nothing To Lose But Your Chains" Update.
|
94
186
|
|
95
|
-
|
187
|
+
### Functions
|
96
188
|
|
97
189
|
Now support chaining via the `#chain`, `#then`, and `#else` methods.
|
98
190
|
|
99
|
-
|
191
|
+
### Results
|
100
192
|
|
101
193
|
Can pass a value and/or an errors object to the constructor.
|
102
194
|
|
data/DEVELOPMENT.md
CHANGED
@@ -1,30 +1,44 @@
|
|
1
1
|
# Development
|
2
2
|
|
3
|
-
## Version 1.0.0
|
3
|
+
## Version 1.0.0
|
4
4
|
|
5
|
-
|
5
|
+
The "Look On My Works, Ye Mighty, and Despair" Update
|
6
6
|
|
7
7
|
- Integration specs.
|
8
8
|
- Configuration option to raise, warn, ignore discarded results.
|
9
|
+
- Code cleanup: Hash syntax, remove end comments, remove file headers
|
10
|
+
- Status Badges!
|
11
|
+
|
12
|
+
Steps Case Study: |
|
13
|
+
|
14
|
+
CMS application - creating a new post.
|
15
|
+
Directory has many Posts
|
16
|
+
Post has a Content
|
17
|
+
Content has many ContentVersions
|
18
|
+
Post has many Tags
|
19
|
+
|
20
|
+
Find Directory
|
21
|
+
Create Post
|
22
|
+
Create Content
|
23
|
+
Create ContentVersion
|
24
|
+
Tags.each { FindOrCreate Tag }
|
9
25
|
|
10
26
|
### Commands
|
11
27
|
|
12
|
-
- Protected Chaining Methods:
|
13
|
-
- #chain!, #success!, #failure!, #tap_chain!, #yield_result!
|
14
|
-
- adds chained command to current command instead of a clone.
|
15
28
|
- Command#to_proc
|
16
|
-
- :clear_errors => true option on #chain
|
17
|
-
- #context object
|
18
|
-
- command currying
|
19
29
|
|
20
|
-
|
30
|
+
## Future Versions
|
31
|
+
|
32
|
+
### Commands
|
33
|
+
|
34
|
+
- Implement #<<, #>> composition methods.
|
35
|
+
- Calls commands in order passing values.
|
36
|
+
- Return Result early on Failure (or not Success), otherwise final Result.
|
37
|
+
|
38
|
+
#### DSL
|
21
39
|
|
22
40
|
- ::process - shortcut for defining #process
|
23
41
|
- ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
|
24
|
-
- chaining methods:
|
25
|
-
- ::chain (::success, ::failure):
|
26
|
-
on #initialize, chains the given command. Can be given a command class
|
27
|
-
(if ::new takes no arguments) or a block that returns a command.
|
28
42
|
- constructor methods:
|
29
43
|
- Programmatically generate a constructor method. Raises an error if
|
30
44
|
#initialize is defined. Automatically sets instance variables on initialize,
|
@@ -35,41 +49,49 @@
|
|
35
49
|
optional arguments and their default values.
|
36
50
|
- ::keywords - sets keyword arguments; same arguments as ::arguments.
|
37
51
|
|
38
|
-
####
|
52
|
+
#### Dependency Injection
|
39
53
|
|
40
|
-
-
|
54
|
+
- shorthand for referencing a sequence of operations
|
41
55
|
|
42
56
|
### Commands - Built In
|
43
57
|
|
44
58
|
- MapCommand - wraps a command (or proc) and returns Result with value, errors
|
45
59
|
as array
|
46
|
-
- RetryCommand
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
60
|
+
- RetryCommand - takes command, retry count
|
61
|
+
- optional only:, except: - restrict what errors are retried
|
62
|
+
|
63
|
+
### Matcher
|
64
|
+
|
65
|
+
- Handle success(), failure(), failure(SomeError) cases.
|
66
|
+
- Custom matcher to handle additional cases - halted, pending, etc?
|
67
|
+
|
68
|
+
### Middleware
|
69
|
+
|
70
|
+
- Implement Command.subclass
|
71
|
+
- Curries constructor arguments
|
72
|
+
- Implement Cuprum::Middleware
|
73
|
+
- #process takes next command, \*args, \*\*kwargs
|
74
|
+
- calls next command with \*args, \*\*kwargs
|
75
|
+
- .apply takes middleware: array, root: command
|
76
|
+
- Implement Cuprum::AppliedMiddleware < Cuprum::Command
|
77
|
+
- has readers #root (Class), #middleware (Array<Class>)
|
78
|
+
- #initialize
|
79
|
+
- initializes root command (passing constructor parameters)
|
80
|
+
- initializes each middleware command
|
81
|
+
- if Class defining .instance, call .instance
|
82
|
+
- if Class, call .new
|
83
|
+
- if Proc, call #call with constructor parameters
|
84
|
+
- calls Middleware.apply and caches as private #applied
|
85
|
+
- #call
|
86
|
+
- delegates to #applied
|
87
|
+
|
88
|
+
### RSpec
|
89
|
+
|
90
|
+
- be_callable matcher - delegates to respond_to(), but check arguments of
|
91
|
+
private #process method
|
92
|
+
- call_command_step matcher
|
93
|
+
- (optionally) alias be_a_result family as have_result for operations
|
94
|
+
|
95
|
+
### Steps::Strict
|
96
|
+
|
97
|
+
- #step raises exception unless block or method returns a result
|
data/README.md
CHANGED
@@ -6,7 +6,8 @@ It defines the following concepts:
|
|
6
6
|
|
7
7
|
- [Commands](#label-Commands) - A function-like object that responds to `#call` and returns a `Result`.
|
8
8
|
- [Operations](#label-Operations) - A stateful `Command` that wraps and delegates to its most recent `Result`.
|
9
|
-
- [Results](#label-Results) -
|
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.
|
10
11
|
|
11
12
|
## About
|
12
13
|
|
@@ -22,16 +23,16 @@ Traditional frameworks such as Rails focus on the objects of your application -
|
|
22
23
|
|
23
24
|
### Alternatives
|
24
25
|
|
25
|
-
If you want to extract your logic but Cuprum is not the right solution for you,
|
26
|
-
|
27
|
-
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
If you want to extract your logic but Cuprum is not the right solution for you, there are a number of alternatives, including
|
27
|
+
[ActiveInteraction](https://github.com/AaronLasseigne/active_interaction),
|
28
|
+
[Dry::Monads](https://dry-rb.org/gems/dry-monads/),
|
29
|
+
[Interactor](https://github.com/collectiveidea/interactor),
|
30
|
+
[Trailblazer](http://trailblazer.to/) Operations,
|
31
|
+
and [Waterfall](https://github.com/apneadiving/waterfall).
|
31
32
|
|
32
33
|
### Compatibility
|
33
34
|
|
34
|
-
Cuprum is tested against Ruby (MRI) 2.
|
35
|
+
Cuprum is tested against Ruby (MRI) 2.3 through 2.5.
|
35
36
|
|
36
37
|
### Documentation
|
37
38
|
|
@@ -41,7 +42,7 @@ Documentation is generated using [YARD](https://yardoc.org/), and can be generat
|
|
41
42
|
|
42
43
|
### License
|
43
44
|
|
44
|
-
Copyright (c)
|
45
|
+
Copyright (c) 2019 Rob Smith
|
45
46
|
|
46
47
|
Cuprum is released under the [MIT License](https://opensource.org/licenses/MIT).
|
47
48
|
|
@@ -63,9 +64,9 @@ Hi, I'm Rob Smith, a Ruby Engineer and the developer of this library. I use thes
|
|
63
64
|
|
64
65
|
require 'cuprum'
|
65
66
|
|
66
|
-
Commands are the core feature of Cuprum. In a nutshell, each Cuprum::Command is a functional object that encapsulates a business logic operation. A Command provides a consistent interface and tracking of result value and status. This minimizes boilerplate and allows for interchangeability between different implementations or strategies for managing your data and processes.
|
67
|
+
Commands are the core feature of Cuprum. In a nutshell, each `Cuprum::Command` is a functional object that encapsulates a business logic operation. A Command provides a consistent interface and tracking of result value and status. This minimizes boilerplate and allows for interchangeability between different implementations or strategies for managing your data and processes.
|
67
68
|
|
68
|
-
Each Command implements a `#call` method that wraps your defined business logic and returns an instance of Cuprum::Result
|
69
|
+
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).
|
69
70
|
|
70
71
|
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommand)
|
71
72
|
|
@@ -77,12 +78,13 @@ The recommended way to define commands is to create a subclass of `Cuprum::Comma
|
|
77
78
|
class BuildBookCommand < Cuprum::Command
|
78
79
|
def process attributes
|
79
80
|
Book.new(attributes)
|
80
|
-
end
|
81
|
-
end
|
81
|
+
end
|
82
|
+
end
|
82
83
|
|
83
84
|
command = BuildPostCommand.new
|
84
|
-
result = command.call(:
|
85
|
-
result.class
|
85
|
+
result = command.call(title: 'The Hobbit')
|
86
|
+
result.class #=> Cuprum::Result
|
87
|
+
result.success? #=> true
|
86
88
|
|
87
89
|
book = result.value
|
88
90
|
book.class #=> Book
|
@@ -97,23 +99,30 @@ Because a command is just a Ruby object, we can also pass values to the construc
|
|
97
99
|
class SaveBookCommand < Cuprum::Command
|
98
100
|
def initialize repository
|
99
101
|
@repository = repository
|
100
|
-
end
|
102
|
+
end
|
101
103
|
|
102
104
|
def process book
|
103
|
-
@repository.persist(book)
|
104
|
-
|
105
|
-
|
105
|
+
if @repository.persist(book)
|
106
|
+
success(book)
|
107
|
+
else
|
108
|
+
failure('unable to save book')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
106
112
|
|
107
113
|
books = [
|
108
|
-
Book.new(:
|
109
|
-
Book.new(:
|
110
|
-
Book.new(:
|
114
|
+
Book.new(title: 'The Fellowship of the Ring'),
|
115
|
+
Book.new(title: 'The Two Towers'),
|
116
|
+
Book.new(title: 'The Return of the King')
|
111
117
|
]
|
112
118
|
command = SaveBookCommand.new(books_repository)
|
113
119
|
books.each { |book| command.call(book) }
|
114
120
|
```
|
115
121
|
|
116
|
-
Here, we are
|
122
|
+
Here, we are defining a command that might fail - maybe the database is unavailable, or there's a constraint that is violated by the inserted attributes. If the call to `#persist` succeeds, we're returning a Result with a status of `:success` and the value set to the persisted book.
|
123
|
+
Conversely, if the call to `#persist` fails, we're returning a Result with a status of `:failure` and a custom error message. Since the `#process` method returns a Result, it is returned directly by `#call`.
|
124
|
+
|
125
|
+
Note also that we are reusing the same command three times, rather than creating a new save command for each book. Each book is persisted to the `books_repository`. This is also an example of how using commands can simplify code - notice that nothing about the `SaveBookCommand` is specific to the `Book` model. Thus, we could refactor this into a generic `SaveModelCommand`.
|
117
126
|
|
118
127
|
A command can also be defined by passing block to `Cuprum::Command.new`.
|
119
128
|
|
@@ -136,9 +145,9 @@ Commands defined using `Cuprum::Command.new` are quick to use, but more difficul
|
|
136
145
|
|
137
146
|
Calling the `#call` method on a `Cuprum::Command` instance will always return an instance of `Cuprum::Result`. The result's `#value` property is determined by the object returned by the `#process` method (if the command is defined as a class) or the block (if the command is defined by passing a block to `Cuprum::Command.new`).
|
138
147
|
|
139
|
-
The `#value` depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the
|
148
|
+
The `#value` depends on whether or not the returned object is a result or is compatible with the result interface. Specifically, any object that responds to the method `#to_cuprum_result` is considered to be a result.
|
140
149
|
|
141
|
-
If the object returned is **not** a result, then the `#value` of the returned result is set to the object.
|
150
|
+
If the object returned by `#process` is **not** a result, then the `#value` of the returned result is set to the object.
|
142
151
|
|
143
152
|
```ruby
|
144
153
|
command = Cuprum::Command.new { 'Greetings, programs!' }
|
@@ -147,46 +156,18 @@ result.class #=> Cuprum::Result
|
|
147
156
|
result.value #=> 'Greetings, programs!'
|
148
157
|
```
|
149
158
|
|
150
|
-
If the object returned
|
159
|
+
If the object returned by `#process` is a result object, then result is returned directly.
|
151
160
|
|
152
161
|
```ruby
|
153
|
-
command = Cuprum::Command.new {
|
162
|
+
command = Cuprum::Command.new { Cuprum::Result.new(value: 'Greetings, programs!') }
|
154
163
|
result = command.call
|
155
|
-
result.class #=> Cuprum::Result
|
156
|
-
result.value #=> nil
|
157
|
-
result.success? #=> false
|
158
|
-
```
|
159
|
-
|
160
|
-
If the object returned is another result or compatible object, then `#call` will call the `#to_result` method on the result and return the resulting object.
|
161
|
-
|
162
|
-
```ruby
|
163
|
-
command = Cuprum::Command.new { |value| Cuprum::Result.new(value) }
|
164
|
-
result = command.call('Greetings, starfighter!')
|
165
164
|
result.class #=> Cuprum::Result
|
166
|
-
result.value #=> 'Greetings,
|
167
|
-
```
|
168
|
-
|
169
|
-
In some cases, returning a result directly will discard information on the command's own result object. When this occurs, Cuprum will display a warning.
|
170
|
-
|
171
|
-
```ruby
|
172
|
-
command =
|
173
|
-
Cuprum::Command.new do
|
174
|
-
result.errors << 'Oops! We are throwing away this result.'
|
175
|
-
|
176
|
-
Cuprum::Result.new
|
177
|
-
end
|
178
|
-
|
179
|
-
#=> This calls Kernel#warn with a warning message.
|
180
|
-
result = command.call
|
181
|
-
result.class #=> Cuprum::Result
|
182
|
-
result.value #=> nil
|
183
|
-
result.success? #=> true
|
184
|
-
result.errors #=> []
|
165
|
+
result.value #=> 'Greetings, programs!'
|
185
166
|
```
|
186
167
|
|
187
168
|
#### Success, Failure, and Errors
|
188
169
|
|
189
|
-
|
170
|
+
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.
|
190
171
|
|
191
172
|
```ruby
|
192
173
|
class PublishBookCommand < Cuprum::Command
|
@@ -194,47 +175,99 @@ class PublishBookCommand < Cuprum::Command
|
|
194
175
|
|
195
176
|
def process book
|
196
177
|
if book.cover.nil?
|
197
|
-
|
198
|
-
|
199
|
-
return
|
200
|
-
end # if
|
178
|
+
return Cuprum::Result.new(error: 'This book does not have a cover.')
|
179
|
+
end
|
201
180
|
|
202
181
|
book.published = true
|
203
182
|
|
204
183
|
book
|
205
|
-
end
|
206
|
-
end
|
184
|
+
end
|
185
|
+
end
|
207
186
|
```
|
208
187
|
|
209
|
-
In addition, the result object defines `#success?` and `#failure?` predicates.
|
188
|
+
In addition, the result object defines `#success?` and `#failure?` predicates.
|
210
189
|
|
211
190
|
```ruby
|
212
|
-
book = Book.new(:
|
191
|
+
book = Book.new(title: 'The Silmarillion', cover: Cover.new)
|
213
192
|
book.published? #=> false
|
214
193
|
|
215
194
|
result = PublishBookCommand.new.call(book)
|
216
|
-
result.
|
195
|
+
result.error #=> nil
|
217
196
|
result.success? #=> true
|
218
197
|
result.failure? #=> false
|
219
198
|
result.value #=> book
|
220
199
|
book.published? #=> true
|
221
200
|
```
|
222
201
|
|
223
|
-
If the result does have
|
202
|
+
If the result does have an error, `#success?` will return false and `#failure?` will return true.
|
224
203
|
|
225
204
|
```ruby
|
226
|
-
book = Book.new(:
|
205
|
+
book = Book.new(title: 'The Silmarillion', cover: nil)
|
227
206
|
book.published? #=> false
|
228
207
|
|
229
208
|
result = PublishBookCommand.new.call(book)
|
230
|
-
result.
|
209
|
+
result.error #=> 'This book does not have a cover.'
|
231
210
|
result.success? #=> false
|
232
211
|
result.failure? #=> true
|
233
212
|
result.value #=> book
|
234
213
|
book.published? #=> false
|
235
214
|
```
|
236
215
|
|
237
|
-
####
|
216
|
+
#### Command Currying
|
217
|
+
|
218
|
+
Cuprum::Command defines the `#curry` method, which allows for partial application of command objects. Partial application (more commonly referred to, if imprecisely, as currying) refers to fixing some number of arguments to a function, resulting in a function with a smaller number of arguments.
|
219
|
+
|
220
|
+
In Cuprum's case, a curried (partially applied) command takes an original command and pre-defines some of its arguments. When the curried command is called, the predefined arguments and/or keywords will be combined with the arguments passed to #call.
|
221
|
+
|
222
|
+
##### Currying Arguments
|
223
|
+
|
224
|
+
We start by defining the base command. In this case, our base command takes two string arguments - a greeting and a person to be greeted.
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
say_command = Cuprum::Command.new do |greeting, person|
|
228
|
+
"#{greeting}, #{person}!"
|
229
|
+
end
|
230
|
+
say_command.call('Hello', 'world')
|
231
|
+
#=> returns a result with value 'Hello, world!'
|
232
|
+
```
|
233
|
+
|
234
|
+
Next, we create a curried command. Here, we pass in one argument. This will set the first argument to always be "Greetings"; therefore, our curried command only takes one argument, the name of the person being greeted.
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
greet_command = say_command.curry('Greetings')
|
238
|
+
greet_command.call('programs')
|
239
|
+
#=> returns a result with value 'Greetings, programs!'
|
240
|
+
```
|
241
|
+
|
242
|
+
Alternatively, we could pass both arguments to `#curry`. In this case, our curried argument does not take any arguments, and will always return the same string.
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
recruit_command = say_command.curry('Greetings', 'starfighter')
|
246
|
+
recruit_command.call
|
247
|
+
#=> returns a result with value 'Greetings, starfighter!'
|
248
|
+
```
|
249
|
+
|
250
|
+
##### Currying Keywords
|
251
|
+
|
252
|
+
We can also pass keywords to `#curry`. Again, we start by defining our base command. In this case, our base command takes a mathematical operation (addition, subtraction, multiplication, etc) and a list of operands.
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
math_command = Cuprum::Command.new do |operands:, operation:|
|
256
|
+
operations.reduce(&operation)
|
257
|
+
end
|
258
|
+
math_command.call(operands: [2, 2], operation: :+)
|
259
|
+
#=> returns a result with value 4
|
260
|
+
```
|
261
|
+
|
262
|
+
Our curried command still takes two keywords, but now the operation keyword is optional. It now defaults to :\*, for multiplication.
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
multiply_command = math_command.curry(operation: :*)
|
266
|
+
multiply_command.call(operands: [3, 3])
|
267
|
+
#=> returns a result with value 9
|
268
|
+
```
|
269
|
+
|
270
|
+
### Composing Commands
|
238
271
|
|
239
272
|
Because Cuprum::Command instances are proper objects, they can be composed like any other object. For example, we could define some basic mathematical operations by composing commands:
|
240
273
|
|
@@ -245,10 +278,12 @@ increment_command.call(2).value #=> 3
|
|
245
278
|
increment_command.call(3).value #=> 4
|
246
279
|
|
247
280
|
add_command = Cuprum::Command.new do |addend, i|
|
281
|
+
# Here, we are composing commands together by calling the increment_command
|
282
|
+
# instance from inside the add_command definition.
|
248
283
|
addend.times { i = increment_command(i).value }
|
249
284
|
|
250
285
|
i
|
251
|
-
end
|
286
|
+
end
|
252
287
|
|
253
288
|
add_command.call(1, 1).value #=> 2
|
254
289
|
add_command.call(1, 2).value #=> 3
|
@@ -274,8 +309,12 @@ class AddCommand < Cuprum::Command
|
|
274
309
|
|
275
310
|
private
|
276
311
|
|
312
|
+
def increment_command
|
313
|
+
@increment_command ||= IncrementCommand.new
|
314
|
+
end
|
315
|
+
|
277
316
|
def process i
|
278
|
-
addend.times { i =
|
317
|
+
addend.times { i = increment_command.call(i).value }
|
279
318
|
|
280
319
|
i
|
281
320
|
end
|
@@ -287,886 +326,823 @@ add_two_command.call(1).value #=> 3
|
|
287
326
|
add_two_command.call(8).value #=> 10
|
288
327
|
```
|
289
328
|
|
290
|
-
|
329
|
+
You can achieve even more powerful composition by passing in a command as an argument to a method, or by creating a method that returns a command.
|
291
330
|
|
292
|
-
|
293
|
-
|
294
|
-
```ruby
|
295
|
-
name_command = Cuprum::Command.new { |klass| klass.name }
|
296
|
-
pluralize_command = Cuprum::Command.new { |str| str.pluralize }
|
297
|
-
underscore_command = Cuprum::Command.new { |str| str.underscore }
|
331
|
+
#### Commands As Arguments
|
298
332
|
|
299
|
-
|
300
|
-
name_command
|
301
|
-
.chain(pluralize_command)
|
302
|
-
.chain(underscore_command)
|
303
|
-
|
304
|
-
result = table_name_command.call(LibraryCard)
|
305
|
-
result.class #=> Cuprum::Result
|
306
|
-
result.value #=> 'library_cards'
|
307
|
-
```
|
308
|
-
|
309
|
-
When the `table_name_command` is called, the class (in our case `LibraryCard`) is passed to the first command in the chain, which is the `name_command`. This produces a Result with a value of 'LibraryCard'. This value is then passed to `pluralize_command`, which returns a Result with a value of 'LibraryCards'. Finally, `underscore_command` is called and returns a Result with a value of 'library_cards'. Since there are no more commands in the chain, this final result is then returned.
|
310
|
-
|
311
|
-
Chained commands can also be defined with a block. This creates an anonymous command, equivalent to `Cuprum::Command.new {}`. Thus, the `table_name_command` could have been defined as either of these:
|
333
|
+
Since commands are objects, they can be passed in as arguments to a method or to another command. For example, consider a command that calls another command a given number of times:
|
312
334
|
|
313
335
|
```ruby
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
table_name_command =
|
320
|
-
Cuprum::Command.new(&:name).chain(&:pluralize).chain(&:underscore)
|
321
|
-
```
|
322
|
-
|
323
|
-
#### Chaining Details
|
324
|
-
|
325
|
-
The `#chain` method is defined for instances of `Cuprum::Command` (or object that extend `Cuprum::Chaining`). Calling `#chain` on a command will always create a copy of the command. The given command is then added to the command chain for the copied command. The original command is unchanged. Thus:
|
336
|
+
class RepeatCommand
|
337
|
+
def initialize(count)
|
338
|
+
@count = count
|
339
|
+
end
|
326
340
|
|
327
|
-
|
328
|
-
first_command = Cuprum::Command.new { puts 'First command!' }
|
329
|
-
first_command.call #=> Outputs 'First command!' to STDOUT.
|
341
|
+
private
|
330
342
|
|
331
|
-
|
332
|
-
|
343
|
+
def process(command)
|
344
|
+
@count.times { command.call }
|
345
|
+
end
|
346
|
+
end
|
333
347
|
|
334
|
-
|
335
|
-
|
348
|
+
greet_command = Cuprum::Command.new { puts 'Greetings, programs!' }
|
349
|
+
repeat_command = RepeatCommand.new(3)
|
350
|
+
repeat_command.call(greet_command) #=> prints 'Greetings, programs!' 3 times
|
336
351
|
```
|
337
352
|
|
338
|
-
|
353
|
+
This is an implementation of the Strategy pattern, which allows us to customize the behavior of a part of our system by passing in implementation code rather than burying conditionals in our logic.
|
339
354
|
|
340
|
-
|
355
|
+
Consider a more concrete example. Suppose we are running an online bookstore that sells both physuical and electronic books, and serves both domestic and international customers. Depending on what the customer ordered and where they live, our business logic for fulfilling an order will have different shipping instructions.
|
356
|
+
|
357
|
+
Traditionally this would be handled with a conditional inside the order fulfillment code, which adds complexity. However, we can use the Strategy pattern and pass in our shipping code as a command.
|
341
358
|
|
342
359
|
```ruby
|
343
|
-
|
344
|
-
increment_command = Cuprum::Command.new { |i| 1 + i }
|
345
|
-
square_command = Cuprum::Command.new { |i| i * i }
|
346
|
-
chained_command =
|
347
|
-
double_command
|
348
|
-
.chain(increment_command)
|
349
|
-
.chain(square_command)
|
350
|
-
|
351
|
-
# First, the double_commmand is called with 2. This returns a Cuprum::Result
|
352
|
-
# with a value of 4.
|
353
|
-
#
|
354
|
-
# Next, the increment_command is called with 4, returning a result with value 5.
|
355
|
-
#
|
356
|
-
# Finally, the square_command is called with 5, returning a result with a value
|
357
|
-
# of 25. This final result is returned by #call.
|
358
|
-
result = chained_command.call(2)
|
359
|
-
result.class #=> Cuprum::Result
|
360
|
-
result.value #=> 25
|
361
|
-
```
|
360
|
+
class DeliverEbook < Cuprum::Command; end
|
362
361
|
|
363
|
-
|
362
|
+
class ShipDomestic < Cuprum::Command; end
|
364
363
|
|
365
|
-
|
366
|
-
validate_command =
|
367
|
-
Cuprum::Command.new do |object|
|
368
|
-
result.errors << 'Object is invalid!' unless object.valid?
|
364
|
+
class ShipInternational < Cuprum::Command; end
|
369
365
|
|
370
|
-
|
366
|
+
class FulfillOrder < Cuprum::Command
|
367
|
+
def initialize(delivery_command)
|
368
|
+
@delivery_command = delivery_command
|
371
369
|
end
|
372
|
-
persist_command =
|
373
|
-
Cuprum::Command.new do |object|
|
374
|
-
object.save if result.success?
|
375
370
|
|
376
|
-
|
371
|
+
private
|
372
|
+
|
373
|
+
def process(book:, user:)
|
374
|
+
# Here we will check inventory, process payments, and so on. The final step
|
375
|
+
# is actually delivering the book to the user:
|
376
|
+
delivery_command.call(book: book, user: user)
|
377
377
|
end
|
378
|
-
|
379
|
-
|
380
|
-
# First, validate_command is called with a valid object. This creates a result
|
381
|
-
# with no errors and whose value is the valid object.
|
382
|
-
#
|
383
|
-
# Then, persist_command is called with the object, and its result is assigned to
|
384
|
-
# the previous result. Since there are no errors on the result, the object is
|
385
|
-
# saved. Finally, the value of the result is set to the object, and the result
|
386
|
-
# is returned.
|
387
|
-
result = chained_command.call(a_valid_object) #=> Saves the object.
|
388
|
-
result.value #=> a_valid_object
|
389
|
-
result.errors #=> []
|
390
|
-
a_valid_object.persisted? #=> true
|
391
|
-
|
392
|
-
# First, validate_command is called with an invalid object. This creates a
|
393
|
-
# result whose value is the invalid object, and with errors 'Object is
|
394
|
-
# invalid!'.
|
395
|
-
#
|
396
|
-
# Then, persist_command is called with the object, and its result is assigned to
|
397
|
-
# the previous result. Since the result has an error, the object is not saved.
|
398
|
-
# Finally, the value of the result is set to the object, and the result
|
399
|
-
# is returned.
|
400
|
-
result = chained_command.call(an_invalid_object) #=> Does not save the object.
|
401
|
-
result.value #=> an_invalid_object
|
402
|
-
result.errors #=> ['Object is invalid!']
|
403
|
-
an_invalid_object.persisted? #=> false
|
378
|
+
end
|
404
379
|
```
|
405
380
|
|
406
|
-
|
381
|
+
This pattern is also useful for testing. When writing specs for the FulfillOrder command, simply pass in a mock double as the delivery command. This removes any need to stub out the implementation of whatever shipping method is used (or worse, calls to external services).
|
407
382
|
|
408
|
-
|
383
|
+
#### Commands As Returned Values
|
409
384
|
|
410
|
-
|
385
|
+
We can also return commands as an object from a method call or from another command. One use case for this is the Abstract Factory pattern.
|
411
386
|
|
412
|
-
|
387
|
+
Consider our shipping example, above. The traditional way to generate a shipping command is to use an `if-then-else` or `case` construct, which would be embedded in whatever code is calling `FulfillOrder`. This adds complexity and increases the testing burden.
|
413
388
|
|
414
|
-
|
415
|
-
|
416
|
-
If the command is chained with `:on => always`, then the chained command will always be executed, even if the previous result is halted.
|
389
|
+
Instead, let's create a factory command. This command will take a user and a book, and will return the command used to ship that item.
|
417
390
|
|
418
391
|
```ruby
|
419
|
-
|
420
|
-
|
421
|
-
book = Book.where(:id => attributes[:id]).first
|
392
|
+
class ShippingMethod < Cuprum::Command
|
393
|
+
private
|
422
394
|
|
423
|
-
|
395
|
+
def process(book:, user:)
|
396
|
+
return DeliverEbook.new(user.email) if book.ebook?
|
424
397
|
|
425
|
-
|
426
|
-
end
|
427
|
-
create_command =
|
428
|
-
Cuprum::Command.new do |attributes|
|
429
|
-
book = Book.new(attributes)
|
398
|
+
return ShipDomestic.new(user.address) if user.address&.domestic?
|
430
399
|
|
431
|
-
if
|
432
|
-
result.success!
|
433
|
-
else
|
434
|
-
book.errors.full_messages.each { |message| result.errors << message }
|
435
|
-
end
|
400
|
+
return ShipInternational.new(user.address) if user.address&.international?
|
436
401
|
|
437
|
-
|
438
|
-
end
|
402
|
+
err = Cuprum::Error.new(message: 'user does not have a valid address')
|
439
403
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
# returns a result with no errors and a value of the found book. The
|
444
|
-
# create_command is not called.
|
445
|
-
hsh = { id: 0, title: 'Journey to the West' }
|
446
|
-
result = find_or_create_command.call(hsh)
|
447
|
-
book = result.value
|
448
|
-
book.id #=> 0
|
449
|
-
book.title #=> 'Journey to the West'
|
450
|
-
result.success? #=> true
|
451
|
-
result.errors #=> []
|
452
|
-
|
453
|
-
# With a book that does not exist but with valid attributes, the find command
|
454
|
-
# returns a failing result with a value of nil. The create_command is called and
|
455
|
-
# creates a new book with the attributes, returning a passing result but
|
456
|
-
# preserving the errors.
|
457
|
-
hsh = { id: 1, title: 'The Ramayana' }
|
458
|
-
result = find_or_create_command.call(hsh)
|
459
|
-
book = result.value
|
460
|
-
book.id #=> 1
|
461
|
-
book.title #=> 'The Ramayana'
|
462
|
-
result.success? #=> true
|
463
|
-
result.errors #=> ['Book not found']
|
464
|
-
|
465
|
-
# With a book that does not exist and with invalid attributes, the find command
|
466
|
-
# returns a failing result with a value of nil. The create_command is called and
|
467
|
-
# is unable to create a new book with the attributes, returning the
|
468
|
-
# (non-persisted) book and adding the validation errors.
|
469
|
-
hsh = { id: 2, title: nil }
|
470
|
-
result = find_or_create_command.call(hsh)
|
471
|
-
book = result.value
|
472
|
-
book.id #=> 2
|
473
|
-
book.title #=> nil
|
474
|
-
result.success? #=> false
|
475
|
-
result.errors #=> ['Book not found', "Title can't be blank"]
|
404
|
+
failure(err)
|
405
|
+
end
|
406
|
+
end
|
476
407
|
```
|
477
408
|
|
478
|
-
|
409
|
+
Notice that our factory includes error handling - if the user does not have a valid address, that is handled immediately rather than when trying to ship the item.
|
479
410
|
|
480
|
-
|
411
|
+
The [Command Factory](#label-Command+Factories) defined by Cuprum is another example of using the Abstract Factory pattern to return command instances. One use case for a command factory would be defining CRUD operations for data records. Depending on the class or the type of record passed in, the factory could return a generic command or a specific command tied to that specific record type.
|
481
412
|
|
482
|
-
|
413
|
+
### Command Steps
|
483
414
|
|
484
|
-
|
485
|
-
double_command = Cuprum::Command.new { |i| 2 * i }
|
486
|
-
halt_command = Cuprum::Command.new { |value| value.tap { result.halt! } }
|
487
|
-
add_one_command = Cuprum::Command.new { |i| i + 1 }
|
488
|
-
|
489
|
-
chained_command =
|
490
|
-
double_command
|
491
|
-
.chain(halt_command)
|
492
|
-
.chain(add_one_command)
|
493
|
-
.chain(:on => :always) { |count| "There are #{count} lights!" }
|
494
|
-
|
495
|
-
# First, double_command is called with 2, returning a result with no errors and
|
496
|
-
# a value of 4. Then, halt_command is called, which marks the result as halted.
|
497
|
-
# Because the result is now halted, the add_one_command is not called. Finally,
|
498
|
-
# the last command is called (even though the result is halted, the command is
|
499
|
-
# chained :on => :always), which returns a result with the string value. The
|
500
|
-
# result is passing, but is still halted.
|
501
|
-
result = chained_command.call(2)
|
502
|
-
result.value #=> 'There are 4 lights!'
|
503
|
-
result.success? #=> true
|
504
|
-
result.halted? #=> true
|
505
|
-
```
|
506
|
-
|
507
|
-
### Advanced Chaining
|
508
|
-
|
509
|
-
The `#tap_result` and `#yield_result` methods provide advanced control over the flow of chained commands.
|
415
|
+
Separating out business logic into commands is a powerful tool, but it does come with some overhead, particularly when checking whether a result is passing, or when converting between results and values. When a process has many steps, each of which can fail or return a value, this can result in a lot of boilerplate.
|
510
416
|
|
511
|
-
|
512
|
-
|
513
|
-
The `#tap_result` method allows you to insert arbitrary code into a command chain without affecting later commands. The method takes a block and yields the previous result, which is then returned and passed to the next command, or returned by `#call` if `#tap_result` is the last item in the chain.
|
417
|
+
The solution Cuprum provides is the `#step` method, which calls either a named method or a given block. If the result of the block or method is passing, then the `#step` method returns the value of the result.
|
514
418
|
|
515
419
|
```ruby
|
516
|
-
|
517
|
-
Cuprum::Command.new do
|
518
|
-
result.errors << 'Example error'
|
519
|
-
|
520
|
-
'Example value'
|
521
|
-
end
|
522
|
-
chained_command =
|
523
|
-
command
|
524
|
-
.tap_result do |result|
|
525
|
-
puts "The result value was #{result.inspect}"
|
526
|
-
end
|
420
|
+
triple_command = Cuprum::Command.new { |i| success(3 * i) }
|
527
421
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
result.value #=> 'Example value'
|
532
|
-
result.success? #=> false
|
533
|
-
result.errors #=> ['Example error']
|
422
|
+
int = 2
|
423
|
+
int = step { triple_command.call(int) } #=> returns 6
|
424
|
+
int = step { triple_command.call(int) } #=> returns 18
|
534
425
|
```
|
535
426
|
|
536
|
-
|
427
|
+
Notice that in each step, we are returning the *value* of the result from `#step`, not the result itself. This means we do not need explicit calls to the `#value` method.
|
537
428
|
|
538
|
-
|
539
|
-
find_book_command =
|
540
|
-
Cuprum::Command.new do |book_id|
|
541
|
-
book = Book.where(:id => book_id).first
|
429
|
+
Of course, not all commands return a passing result. If the result of the block or method is failing, then `#step` will throw `:cuprum_failed_result` and the result, immediately halting the execution chain. If the `#step` method is used inside a command definition (or inside a `#steps` block; [see below](#label-Using+Steps+Outside+Of+Commands)), that symbol will be caught and the failing result returned by `#call`.
|
542
430
|
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
end
|
431
|
+
```ruby
|
432
|
+
divide_command = Cuprum::Command.new do |dividend, divisor|
|
433
|
+
return failure('divide by zero') if divisor.zero?
|
547
434
|
|
548
|
-
|
549
|
-
|
550
|
-
.tap_result(:on => :success) do |result|
|
551
|
-
render :show, :locals => { :book => result.value }
|
552
|
-
end
|
553
|
-
.tap_result(:on => :failure) do
|
554
|
-
redirect_to books_path
|
555
|
-
end
|
435
|
+
success(dividend / divisor)
|
436
|
+
end
|
556
437
|
|
557
|
-
|
558
|
-
|
559
|
-
# block is evaluated, calling the render method and passing it the book via the
|
560
|
-
# result.value method. Because the result is passing, the next block is skipped.
|
561
|
-
# Finally, the result of find_book_command is returned unchanged.
|
562
|
-
result = chained_command.call(valid_id)
|
563
|
-
result.class #=> Cuprum::Result
|
564
|
-
result.value #=> an instance of Book
|
565
|
-
result.success? #=> true
|
566
|
-
result.errors #=> []
|
567
|
-
|
568
|
-
# Calls find_book_command with the id, which queries for the book and returns a
|
569
|
-
# result with a value of nil and the error message. Because the result is
|
570
|
-
# failing, the first block is skipped. The second block is then evaluated,
|
571
|
-
# calling the redirect_to method. Finally, the result of find_book_command is
|
572
|
-
# returned unchanged.
|
573
|
-
result = chained_command.call(invalid_id)
|
574
|
-
result.class #=> Cuprum::Result
|
575
|
-
result.value #=> nil
|
576
|
-
result.success? #=> false
|
577
|
-
result.errors #=> ['Unable to find book with id invalid_id']
|
438
|
+
value = step { divide_command.call(10, 5) } #=> returns 2
|
439
|
+
value = step { divide_command.call(2, 0) } #=> throws :cuprum_failed_result
|
578
440
|
```
|
579
441
|
|
580
|
-
|
442
|
+
Here, the `divide_command` can either return a passing result (if the divisor is not zero) or a failing result (if the divisor is zero). When wrapped in a `#step`, the failing result is then thrown, halting execution.
|
443
|
+
|
444
|
+
This is important when using a sequence of steps. Let's consider a case study - reserving a book from the library. This entails several steps, each of which could potentially fail:
|
581
445
|
|
582
|
-
|
446
|
+
- Validating that the user can reserve books. Maybe the user has too many unpaid fines.
|
447
|
+
- Finding the requested book in the library system. Maybe the requested title isn't in the system.
|
448
|
+
- Placing a reservation on the book. Maybe there are no copies of the book available to reserve.
|
583
449
|
|
584
|
-
|
450
|
+
Using `#step`, as soon as one of the subtasks fails then the command will immediately return the failed value. This prevents us from hitting later subtasks with invalid data, it returns the actual failing result for analytics and for displaying a useful error message to the user, and it avoids the overhead (and the boilerplate) of exception-based failure handling.
|
585
451
|
|
586
452
|
```ruby
|
587
|
-
|
588
|
-
Cuprum::Command.new do
|
589
|
-
'Example value'
|
590
|
-
end
|
591
|
-
.yield_result do |result|
|
592
|
-
result.errors << 'Example error'
|
453
|
+
class CheckUserStatus < Cuprum::Command; end
|
593
454
|
|
594
|
-
|
595
|
-
end
|
596
|
-
.yield_result do |result|
|
597
|
-
"The last result was a #{result.success? ? 'success' : 'failure'}."
|
598
|
-
end
|
455
|
+
class CreateBookReservation < Cuprum::Command; end
|
599
456
|
|
600
|
-
|
601
|
-
#
|
602
|
-
# This is passed to the first yield_result block, which adds the 'Example error'
|
603
|
-
# string to the result errors, and then returns the result. Because the
|
604
|
-
# yield_result block returns the previous result, it is then passed to the next
|
605
|
-
# item in the chain.
|
606
|
-
#
|
607
|
-
# Finally, the second yield_result block is called, which checks the status of
|
608
|
-
# the passed result. Since the second block does not return the previous result,
|
609
|
-
# the previous result is discarded and a new result is created with the string
|
610
|
-
# value starting with 'The last result was ...'.
|
611
|
-
result = chained_command.call
|
612
|
-
result.class #=> Cuprum::Result
|
613
|
-
result.value #=> 'The last result was a failure.'
|
614
|
-
result.success? #=> true
|
615
|
-
result.errors #=> []
|
616
|
-
```
|
617
|
-
|
618
|
-
Like `#chain`, `#yield_result` can be given an `:on => value` keyword.
|
457
|
+
class FindBookByTitle < Cuprum::Command; end
|
619
458
|
|
620
|
-
|
621
|
-
|
622
|
-
# as follows. Start with any positive integer. If the number is even, then
|
623
|
-
# divide the number by two. If the number is odd, multiply by three and add one.
|
624
|
-
# These steps are repeated until the value is one or the numbers loop.
|
625
|
-
step_command =
|
626
|
-
Cuprum::Command.new do |i|
|
627
|
-
result.failure! unless i.even?
|
459
|
+
class ReserveBookByTitle < Cuprum::Command
|
460
|
+
private
|
628
461
|
|
629
|
-
|
462
|
+
def process(title:, user:)
|
463
|
+
# If CheckUserStatus fails, #process will immediately return that result.
|
464
|
+
# For this step, we already have the user, so we don't need to use the
|
465
|
+
# result value.
|
466
|
+
step { CheckUserStatus.new.call(user) }
|
467
|
+
|
468
|
+
# Here, we are looking up the requested title. In this case, we will need
|
469
|
+
# the book object, so we save it as a variable. Notice that we don't need
|
470
|
+
# an explicit #value call - #step handles that for us.
|
471
|
+
book = step { FindBookByTitle.new.call(title) }
|
472
|
+
|
473
|
+
# Finally, we want to reserve the book. Since this is the last subtask, we
|
474
|
+
# don't strictly need to use #step. However, it's good practice, especially
|
475
|
+
# if we might need to add more steps to the command in the future.
|
476
|
+
step { CreateBookReservation.new.call(book: book, user: user) }
|
630
477
|
end
|
631
|
-
|
632
|
-
.yield_result(:on => :failure) { |result| 1 + 3 * result.value }
|
633
|
-
|
634
|
-
# Because the value is even, the first command returns a passing result with a
|
635
|
-
# value of 8. The first yield_result block is then called with that result,
|
636
|
-
# returning a new result with a value of 4. The result is passing, so the second
|
637
|
-
# yield_result block is skipped and the result is returned.
|
638
|
-
result = collatz_command.call(8)
|
639
|
-
result.value #=> 4
|
640
|
-
result.success? #=> true
|
641
|
-
|
642
|
-
# Because the value is odd, the first command returns a failing result with a
|
643
|
-
# value of 5. Because the result is failing, the first yield_result block is
|
644
|
-
# skipped. The second yield_result block is then called with the result,
|
645
|
-
# returning a new (passing) result with a value of 16.
|
646
|
-
result = collatz_command.call(5)
|
647
|
-
result.value #=> 16
|
648
|
-
result.success? #=> true
|
478
|
+
end
|
649
479
|
```
|
650
480
|
|
651
|
-
|
652
|
-
|
653
|
-
### Results
|
654
|
-
|
655
|
-
require 'cuprum'
|
656
|
-
|
657
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
|
658
|
-
|
659
|
-
A Cuprum::Result is a data object that encapsulates the result of calling a Cuprum command. Each result has a `#value`, an `#errors` object (defaults to an Array), and status methods `#success?`, `#failure?`, and `#halted?`.
|
481
|
+
First, our user may not have borrowing privileges. In this case, `CheckUserStatus` will fail, and neither of the subsequent steps will be called. The `#call` method will return the failing result from `CheckUserStatus`.
|
660
482
|
|
661
483
|
```ruby
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
result.
|
667
|
-
result.success? #=>
|
668
|
-
result.
|
669
|
-
result.halted? #=> false
|
484
|
+
result = ReserveBookByTitle.new.call(
|
485
|
+
title: 'The C Programming Language',
|
486
|
+
user: 'Ed Dillinger'
|
487
|
+
)
|
488
|
+
result.class #=> Cuprum::Result
|
489
|
+
result.success? #=> false
|
490
|
+
result.error #=> 'not authorized to reserve book'
|
670
491
|
```
|
671
492
|
|
672
|
-
|
493
|
+
Second, our user may be valid but our requested title may not exist in the system. In this case, `FindBookByTitle` will fail, and the final step will not be called. The `#call` method will return the failing result from `FindBookByTitle`.
|
673
494
|
|
674
495
|
```ruby
|
675
|
-
result
|
496
|
+
result = ReserveBookByTitle.new.call(
|
497
|
+
title: 'Using GOTO For Fun And Profit',
|
498
|
+
user: 'Alan Bradley'
|
499
|
+
)
|
500
|
+
result.class #=> Cuprum::Result
|
676
501
|
result.success? #=> false
|
677
|
-
result.
|
502
|
+
result.error #=> 'title not found'
|
678
503
|
```
|
679
504
|
|
680
|
-
|
505
|
+
Third, our user and book may be valid, but all of the copies are checked out. In this case, each of the steps will be called, and the `#call` method will return the failing result from `CreateBookReservation`.
|
681
506
|
|
682
507
|
```ruby
|
683
|
-
result.
|
684
|
-
|
685
|
-
|
686
|
-
|
508
|
+
result = ReserveBookByTitle.new.call(
|
509
|
+
title: 'Design Patterns: Elements of Reusable Object-Oriented Software',
|
510
|
+
user: 'Alan Bradley'
|
511
|
+
)
|
512
|
+
result.class #=> Cuprum::Result
|
513
|
+
result.success? #=> false
|
514
|
+
result.error #=> 'no copies available'
|
687
515
|
```
|
688
516
|
|
689
|
-
|
690
|
-
|
691
|
-
require 'cuprum'
|
692
|
-
|
693
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
|
694
|
-
|
695
|
-
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.
|
696
|
-
|
697
|
-
These two features allow developers to simplify logic around calling and using the results of operations, and reduce the need for boilerplate code (particularly when using an operation as part of an existing framework, such as inside of an asynchronous worker or a Rails controller action).
|
517
|
+
Finally, if each of the steps succeeds, the `#call` method will return the result of the final step.
|
698
518
|
|
699
519
|
```ruby
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
def create
|
708
|
-
operation = CreateBookOperation.new.call(book_params)
|
709
|
-
|
710
|
-
if operation.success?
|
711
|
-
redirect_to(operation.value)
|
712
|
-
else
|
713
|
-
@book = operation.value
|
714
|
-
|
715
|
-
render :new
|
716
|
-
end # if-else
|
717
|
-
end # create
|
520
|
+
result = ReserveBookByTitle.new.call(
|
521
|
+
title: 'The C Programming Language',
|
522
|
+
user: 'Alan Bradley'
|
523
|
+
)
|
524
|
+
result.class #=> Cuprum::Result
|
525
|
+
result.success? #=> true
|
526
|
+
result.value #=> an instance of BookReservation
|
718
527
|
```
|
719
528
|
|
720
|
-
|
529
|
+
#### Using Methods As Steps
|
721
530
|
|
722
|
-
|
531
|
+
Steps can also be defined as method calls. Instead of providing a block to `#step`, provide the name of the method as the first argument, either as a symbol or as a string. Any subsequent arguments, keywords, or a block is passed to the method when it is called.
|
723
532
|
|
724
|
-
|
533
|
+
A step defined with a method behaves the same as a step defined with a block. If the method returns a successful result, then `#step` will return the value of the result. If the method returns a failing result, then `#step` will throw `:cuprum_failed_result` and the result, to be caught by the `#process` method or the containing `#steps` block.
|
725
534
|
|
726
|
-
|
727
|
-
|
728
|
-
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.
|
729
|
-
|
730
|
-
Finally, the result can be halted using the `#halt` method, which indicates that further chained commands should not be executed.
|
535
|
+
We can use this to rewrite our `ReserveBookByTitle` command to use methods:
|
731
536
|
|
732
537
|
```ruby
|
733
|
-
|
734
|
-
|
735
|
-
```
|
736
|
-
|
737
|
-
### Built In Commands
|
538
|
+
class ReserveBookByTitle < Cuprum::Result
|
539
|
+
private
|
738
540
|
|
739
|
-
|
541
|
+
def check_user_status(user)
|
542
|
+
CheckUserStatus.new(user)
|
543
|
+
end
|
740
544
|
|
741
|
-
|
545
|
+
def create_book_reservation(book:, user:)
|
546
|
+
CreateBookReservation.new(book: book, user: user)
|
547
|
+
end
|
742
548
|
|
743
|
-
|
549
|
+
def find_book_by_title(title)
|
550
|
+
FindBookByTitle.new.call(title)
|
551
|
+
end
|
744
552
|
|
745
|
-
|
553
|
+
def process(title:, user:)
|
554
|
+
step :check_user_status, user
|
746
555
|
|
747
|
-
|
556
|
+
book = step :find_book_by_title, title
|
748
557
|
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
result.value #=> 'expected value'
|
753
|
-
result.success? #=> true
|
558
|
+
create_book_reservation, book: book, user: user
|
559
|
+
end
|
560
|
+
end
|
754
561
|
```
|
755
562
|
|
756
|
-
|
563
|
+
In this case, our methods simply delegate to our previously defined commands. However, a more complex example could include other logic in each method, or even a sequence of steps defining subtasks for the method. The only requirement is that the method returns a result. You can use the `#success` helpers to wrap a non-result value, or the `#failure` helper to generate a failing result.
|
757
564
|
|
758
|
-
|
565
|
+
#### Using Steps Outside Of Commands
|
759
566
|
|
760
|
-
|
567
|
+
Steps can also be used outside of a command. For example, a controller action might define a sequence of steps to run when the corresponding endpoint is called.
|
761
568
|
|
762
|
-
|
569
|
+
To use steps outside of a command, include the `Cuprum::Steps` module. Then, each sequence of steps should be wrapped in a `#steps` block as follows:
|
763
570
|
|
764
571
|
```ruby
|
765
|
-
|
766
|
-
|
767
|
-
operation.success? #=> true
|
768
|
-
```
|
769
|
-
|
770
|
-
#### NullCommand
|
771
|
-
|
772
|
-
require 'cuprum/built_in/null_command'
|
773
|
-
|
774
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
|
572
|
+
steps do
|
573
|
+
step { check_something }
|
775
574
|
|
776
|
-
|
575
|
+
obj = step { find_something }
|
777
576
|
|
778
|
-
|
779
|
-
|
780
|
-
result = command.call
|
781
|
-
result.value #=> nil
|
782
|
-
result.success? #=> true
|
577
|
+
step :do_something, with: obj
|
578
|
+
end
|
783
579
|
```
|
784
580
|
|
785
|
-
|
786
|
-
|
787
|
-
require 'cuprum/built_in/null_operation'
|
581
|
+
Each step will be executed in sequence until a failing result is returned by the block or method. The `#steps` block will return that failing result. If no step returns a failing result, then the return value of the block will be wrapped in a result and returned by `#steps`.
|
788
582
|
|
789
|
-
|
583
|
+
Let's consider the example of a controller action for creating a new resource. This would have several steps, each of which can fail:
|
790
584
|
|
791
|
-
|
585
|
+
- First, we build a new instance of the resource with the provided attributes. This can fail if the attributes are incompatible with the resource, e.g. with extra attributes not included in the resource's table columns.
|
586
|
+
- Second, we run validations on the resource itself. This can fail if the attributes do not match the expected format.
|
587
|
+
- Finally, we persist the resource to the database. This can fail if the record violates any database constraints, or if the database itself is unavailable.
|
792
588
|
|
793
589
|
```ruby
|
794
|
-
|
795
|
-
|
796
|
-
operation.success? #=> true
|
797
|
-
```
|
798
|
-
|
799
|
-
## Reference
|
800
|
-
|
801
|
-
### Cuprum::BuiltIn::IdentityCommand
|
802
|
-
|
803
|
-
require 'cuprum/built_in/identity_command'
|
804
|
-
|
805
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
|
806
|
-
|
807
|
-
Cuprum::BuiltIn::IdentityCommand defines the following methods:
|
808
|
-
|
809
|
-
#### `#call`
|
810
|
-
|
811
|
-
call(value) #=> Cuprum::Result
|
812
|
-
|
813
|
-
Returns a result, whose `#value` is equal to the given value.
|
814
|
-
|
815
|
-
### Cuprum::BuiltIn::IdentityOperation
|
816
|
-
|
817
|
-
require 'cuprum/built_in/identity_operation'
|
818
|
-
|
819
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
|
820
|
-
|
821
|
-
Cuprum::BuiltIn::IdentityOperation defines the following methods:
|
822
|
-
|
823
|
-
#### `#call`
|
824
|
-
|
825
|
-
call(value) #=> Cuprum::BuiltIn::IdentityOperation
|
826
|
-
|
827
|
-
Sets the last result to a new result, whose `#value` is equal to the given value.
|
590
|
+
class BooksController
|
591
|
+
include Cuprum::Steps
|
828
592
|
|
829
|
-
|
593
|
+
def create
|
594
|
+
attributes = params[:books]
|
595
|
+
result = steps do
|
596
|
+
@book = step :build_book, attributes
|
830
597
|
|
831
|
-
|
832
|
-
|
833
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
|
834
|
-
|
835
|
-
Cuprum::BuiltIn::NullCommand defines the following methods:
|
598
|
+
step :run_validations, @book
|
836
599
|
|
837
|
-
|
838
|
-
|
839
|
-
call(*args, **keywords) { ... } #=> Cuprum::Result
|
840
|
-
|
841
|
-
Returns a result with nil value. Any arguments or keywords are ignored.
|
600
|
+
step :persist_book, book
|
601
|
+
end
|
842
602
|
|
843
|
-
|
603
|
+
result.success ? redirect_to(@book) : render(:edit)
|
604
|
+
end
|
844
605
|
|
845
|
-
|
606
|
+
private
|
846
607
|
|
847
|
-
|
608
|
+
def build_book(attributes)
|
609
|
+
success(Book.new(attributes))
|
610
|
+
rescue InvalidAttributes
|
611
|
+
failure('attributes are invalid')
|
612
|
+
end
|
848
613
|
|
849
|
-
|
614
|
+
def persist_book(book)
|
615
|
+
book.save ? success(book) : failure('unable to persist book')
|
616
|
+
end
|
850
617
|
|
851
|
-
|
618
|
+
def run_validations(book)
|
619
|
+
book.valid? ? success : failure('book is invalid')
|
620
|
+
end
|
621
|
+
end
|
622
|
+
```
|
852
623
|
|
853
|
-
|
624
|
+
A few things to note about this example. First, we have a couple of examples of wrapping existing code in a result, both by rescuing exceptions (in `#build_book`) or by checking a returned status (in `#persist_book`). Second, note that each of our helper methods can be reused in other controller actions. For even more encapsulation and reusability, the next step might be to convert those methods to commands of their own.
|
854
625
|
|
855
|
-
|
626
|
+
You can define even more complex logic by defining multiple `#steps` blocks. Each block represents a series of tasks that will terminate on the first failure. Steps blocks can even be nested in one another, or inside a `#process` method.
|
856
627
|
|
857
|
-
###
|
628
|
+
### Results
|
858
629
|
|
859
630
|
require 'cuprum'
|
860
631
|
|
861
|
-
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%
|
862
|
-
|
863
|
-
A Cuprum::Command defines the following methods:
|
864
|
-
|
865
|
-
#### `#initialize`
|
632
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
|
866
633
|
|
867
|
-
|
634
|
+
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).
|
868
635
|
|
869
|
-
|
636
|
+
```ruby
|
637
|
+
result = Cuprum::Result.new
|
870
638
|
|
871
|
-
|
639
|
+
result.value #=> nil
|
640
|
+
result.error #=> nil
|
641
|
+
result.status #=> :success
|
642
|
+
result.success? #=> true
|
643
|
+
result.failure? #=> true
|
644
|
+
```
|
872
645
|
|
873
|
-
|
646
|
+
Creating a result with a value stores the value.
|
874
647
|
|
875
|
-
|
648
|
+
```ruby
|
649
|
+
value = 'A result value'.freeze
|
650
|
+
result = Cuprum::Result.new(value: value)
|
876
651
|
|
877
|
-
|
652
|
+
result.value #=> 'A result value'
|
653
|
+
result.error #=> nil
|
654
|
+
result.status #=> :success
|
655
|
+
result.success? #=> true
|
656
|
+
result.failure? #=> false
|
657
|
+
```
|
878
658
|
|
879
|
-
|
659
|
+
Creating a Result with an error stores the error and sets the status to `:failure`.
|
880
660
|
|
881
|
-
|
661
|
+
```ruby
|
662
|
+
error = Cuprum::Error.new(message: "I'm sorry, something went wrong.")
|
663
|
+
result = Cuprum::Result.new(error: error)
|
664
|
+
result.value #=> nil
|
665
|
+
result.error #=> Error with message "I'm sorry, something went wrong."
|
666
|
+
result.status #=> :failure
|
667
|
+
result.success? #=> false
|
668
|
+
result.failure? #=> true
|
669
|
+
```
|
882
670
|
|
883
|
-
|
671
|
+
Although using a `Cuprum::Error` instance as the `:error` is recommended, it is not required. You can use a custom error object, or just a string message.
|
884
672
|
|
885
|
-
|
673
|
+
```ruby
|
674
|
+
result = Cuprum::Result.new(error: "I'm sorry, something went wrong.")
|
675
|
+
result.value #=> nil
|
676
|
+
result.error #=> "I'm sorry, something went wrong."
|
677
|
+
result.status #=> :failure
|
678
|
+
result.success? #=> false
|
679
|
+
result.failure? #=> true
|
680
|
+
```
|
886
681
|
|
887
|
-
|
682
|
+
Finally, the status can be overridden via the `:status` keyword.
|
888
683
|
|
889
|
-
|
684
|
+
```ruby
|
685
|
+
result = Cuprum::Result.new(status: :failure)
|
686
|
+
result.error #=> nil
|
687
|
+
result.status #=> :failure
|
688
|
+
result.success? #=> false
|
689
|
+
result.failure? #=> true
|
890
690
|
|
891
|
-
|
691
|
+
error = Cuprum::Error.new(message: "I'm sorry, something went wrong.")
|
692
|
+
result = Cuprum::Result.new(error: error, status: :success)
|
693
|
+
result.error #=> Error with message "I'm sorry, something went wrong."
|
694
|
+
result.status #=> :success
|
695
|
+
result.success? #=> true
|
696
|
+
result.failure? #=> false
|
697
|
+
```
|
892
698
|
|
893
|
-
|
699
|
+
### Errors
|
894
700
|
|
895
|
-
|
701
|
+
require 'cuprum/error'
|
896
702
|
|
897
|
-
|
703
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FError)
|
898
704
|
|
899
|
-
|
705
|
+
A `Cuprum::Error` encapsulates a specific failure state of a Command. Each Error has a `#message` property, which defaults to nil.
|
900
706
|
|
901
|
-
|
707
|
+
```ruby
|
708
|
+
error = Cuprum::Error.new
|
709
|
+
error.message => # nil
|
902
710
|
|
903
|
-
|
711
|
+
error = Cuprum::Error.new(message: 'Something went wrong.')
|
712
|
+
error.message => # 'Something went wrong.'
|
713
|
+
```
|
904
714
|
|
905
|
-
|
715
|
+
Each application should define its own failure states as errors. For example, a typical web application might define the following errors:
|
906
716
|
|
907
|
-
|
717
|
+
```ruby
|
718
|
+
class NotFoundError < Cuprum::Error
|
719
|
+
def initialize(resource:, resource_id:)
|
720
|
+
@resource = resource
|
721
|
+
@resource_id = resource_id
|
908
722
|
|
909
|
-
|
723
|
+
super(message: "#{resource} not found with id #{resource_id}")
|
724
|
+
end
|
910
725
|
|
911
|
-
|
726
|
+
attr_reader :resource, :resource_id
|
727
|
+
end
|
912
728
|
|
913
|
-
|
729
|
+
class ValidationError < Cuprum::Error
|
730
|
+
def initialize(resource:, errors:)
|
731
|
+
@resource = resource
|
732
|
+
@errors = errors
|
914
733
|
|
915
|
-
|
734
|
+
super(message: "#{resource} was invalid")
|
735
|
+
end
|
916
736
|
|
917
|
-
|
737
|
+
attr_reader :resource, :errors
|
738
|
+
end
|
739
|
+
```
|
918
740
|
|
919
|
-
|
741
|
+
It is optional but recommended to use a `Cuprum::Error` when returning a failed result from a command.
|
920
742
|
|
921
|
-
|
743
|
+
### Operations
|
922
744
|
|
923
|
-
|
745
|
+
require 'cuprum'
|
924
746
|
|
925
|
-
|
747
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
|
926
748
|
|
927
|
-
|
749
|
+
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.
|
928
750
|
|
929
|
-
|
751
|
+
These two features allow developers to simplify logic around calling and using the results of operations, and reduce the need for boilerplate code (particularly when using an operation as part of an existing framework, such as inside of an asynchronous worker or a Rails controller action).
|
930
752
|
|
931
|
-
|
753
|
+
```ruby
|
754
|
+
class CreateBookOperation < Cuprum::Operation
|
755
|
+
def process
|
756
|
+
# Implementation here.
|
757
|
+
end
|
758
|
+
end
|
932
759
|
|
933
|
-
|
934
|
-
|
935
|
-
|
760
|
+
# Defining a controller action using an operation.
|
761
|
+
def create
|
762
|
+
operation = CreateBookOperation.new.call(book_params)
|
936
763
|
|
937
|
-
|
938
|
-
|
764
|
+
if operation.success?
|
765
|
+
redirect_to(operation.value)
|
766
|
+
else
|
767
|
+
@book = operation.value
|
939
768
|
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
#=> ["I'm sorry, something went wrong."]
|
769
|
+
render :new
|
770
|
+
end
|
771
|
+
end
|
772
|
+
```
|
945
773
|
|
946
|
-
|
774
|
+
Like a Command, an Operation can be defined directly by passing an implementation block to the constructor or by creating a subclass that overwrites the #process method.
|
947
775
|
|
948
|
-
|
776
|
+
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.
|
949
777
|
|
950
|
-
|
778
|
+
#### The Operation Mixin
|
951
779
|
|
952
|
-
|
780
|
+
[Module Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation%2FMixin)
|
953
781
|
|
954
|
-
|
782
|
+
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.
|
955
783
|
|
956
|
-
|
784
|
+
### Command Factories
|
957
785
|
|
958
|
-
|
786
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommandFactory)
|
959
787
|
|
960
|
-
|
788
|
+
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.
|
961
789
|
|
962
|
-
|
790
|
+
For example, consider a basic entity command:
|
963
791
|
|
964
|
-
|
792
|
+
```ruby
|
793
|
+
class Book
|
794
|
+
def initialize(attributes = {})
|
795
|
+
@title = attributes[:title]
|
796
|
+
@author = attributes[:author]
|
797
|
+
end
|
965
798
|
|
966
|
-
|
799
|
+
attr_accessor :author, :publisher, :title
|
800
|
+
end
|
967
801
|
|
968
|
-
|
802
|
+
class BuildBookCommand < Cuprum::Command
|
803
|
+
private
|
969
804
|
|
970
|
-
|
805
|
+
def process(attributes = {})
|
806
|
+
Book.new(attributes)
|
807
|
+
end
|
808
|
+
end
|
971
809
|
|
972
|
-
|
810
|
+
class BookFactory < Cuprum::CommandFactory
|
811
|
+
command :build, BuildBookCommand
|
812
|
+
end
|
813
|
+
```
|
973
814
|
|
974
|
-
|
815
|
+
Our factory is defined by subclassing `Cuprum::CommandFactory`, and then we map the individual commands with the `::command` or `::command_class` class methods. In this case, we've defined a Book factory with the build command. The build command can be accessed on a factory instance in one of two ways.
|
975
816
|
|
976
|
-
|
817
|
+
First, the command class can be accessed directly as a constant on the factory instance.
|
977
818
|
|
978
|
-
|
819
|
+
```ruby
|
820
|
+
factory = BookFactory.new
|
821
|
+
factory::Build #=> BuildBookCommand
|
822
|
+
```
|
979
823
|
|
980
|
-
|
824
|
+
Second, the factory instance now defines a `#build` method, which returns an instance of our defined command class. This command instance can be called like any command, or returned or passed around like any other object.
|
981
825
|
|
982
|
-
|
826
|
+
```ruby
|
827
|
+
factory = BookFactory.new
|
983
828
|
|
984
|
-
|
829
|
+
attrs = { title: 'A Wizard of Earthsea', author: 'Ursula K. Le Guin' }
|
830
|
+
command = factory.build() #=> an instance of BuildBookCommand
|
831
|
+
result = command.call(attrs) #=> an instance of Cuprum::Result
|
832
|
+
book = result.value #=> an instance of Book
|
985
833
|
|
986
|
-
|
834
|
+
book.title #=> 'A Wizard of Earthsea'
|
835
|
+
book.author #=> 'Ursula K. Le Guin'
|
836
|
+
book.publisher #=> nil
|
837
|
+
```
|
987
838
|
|
988
|
-
|
839
|
+
#### The ::command Method And A Command Class
|
989
840
|
|
990
|
-
|
841
|
+
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:
|
991
842
|
|
992
|
-
|
843
|
+
```ruby
|
844
|
+
class BookFactory < Cuprum::CommandFactory
|
845
|
+
command :build, BuildBookCommand
|
846
|
+
end
|
847
|
+
```
|
993
848
|
|
994
|
-
|
849
|
+
This makes the command class available on a factory instance as `::Build`, and generates the `#build` method which returns an instance of `BuildBookCommand`.
|
995
850
|
|
996
|
-
The
|
851
|
+
#### The ::command Method And A Block
|
997
852
|
|
998
|
-
|
853
|
+
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.
|
999
854
|
|
1000
|
-
|
855
|
+
```ruby
|
856
|
+
class PublishBookCommand < Cuprum::Command
|
857
|
+
def initialize(publisher:)
|
858
|
+
@publisher = publisher
|
859
|
+
end
|
1001
860
|
|
1002
|
-
|
861
|
+
attr_reader :publisher
|
1003
862
|
|
1004
|
-
|
863
|
+
private
|
1005
864
|
|
1006
|
-
|
865
|
+
def process(book)
|
866
|
+
book.publisher = publisher
|
1007
867
|
|
1008
|
-
|
868
|
+
book
|
869
|
+
end
|
870
|
+
end
|
1009
871
|
|
1010
|
-
|
872
|
+
class BookFactory < Cuprum::CommandFactory
|
873
|
+
command :publish do |publisher|
|
874
|
+
PublishBookCommand.new(publisher: publisher)
|
875
|
+
end
|
876
|
+
end
|
877
|
+
```
|
1011
878
|
|
1012
|
-
|
879
|
+
This defines the `#publish` method on an instance of the factory. The method takes one argument (the publisher), which is then passed on to the constructor for `PublishBookCommand` by our block. Finally, the block returns an instance of the publish command, which is then returned by `#publish`.
|
1013
880
|
|
1014
|
-
|
881
|
+
```ruby
|
882
|
+
factory = BookFactory.new
|
883
|
+
book = Book.new(title: 'The Tombs of Atuan', author: 'Ursula K. Le Guin')
|
884
|
+
book.publisher #=> nil
|
1015
885
|
|
1016
|
-
|
886
|
+
command = factory.publish('Harper & Row') #=> an instance of PublishBookCommand
|
887
|
+
result = command.call(book) #=> an instance of Cuprum::Result
|
888
|
+
book.publisher #=> 'Harper & Row'
|
889
|
+
```
|
1017
890
|
|
1018
|
-
|
891
|
+
Note that unlike when `::command` is called with a command class, calling `::command` with a block will not set a constant on the factory instance. In this case, trying to access the `PublishBookCommand` at `factory::Publish` will raise a `NameError`.
|
1019
892
|
|
1020
|
-
|
893
|
+
The block is evaluated in the context of the factory instance. This means that instance variables or methods are available to the block, allowing you to create commands with instance-specific configuration.
|
1021
894
|
|
1022
|
-
|
895
|
+
```ruby
|
896
|
+
class PublishedBooksCommand < Cuprum::Command
|
897
|
+
def initialize(collection = [])
|
898
|
+
@collection = collection
|
899
|
+
end
|
1023
900
|
|
1024
|
-
|
901
|
+
attr_reader :collection
|
1025
902
|
|
1026
|
-
|
903
|
+
private
|
1027
904
|
|
1028
|
-
|
905
|
+
def process
|
906
|
+
books.reject { |book| book.publisher.nil? }
|
907
|
+
end
|
908
|
+
end
|
1029
909
|
|
1030
|
-
|
910
|
+
class BookFactory < Cuprum::CommandFactory
|
911
|
+
def initialize(books)
|
912
|
+
@books_collection = books
|
913
|
+
end
|
1031
914
|
|
1032
|
-
|
915
|
+
attr_reader :books_collection
|
1033
916
|
|
1034
|
-
|
917
|
+
command :published do
|
918
|
+
PublishedBooksCommand.new(books_collection)
|
919
|
+
end
|
920
|
+
end
|
921
|
+
```
|
1035
922
|
|
1036
|
-
|
923
|
+
This defines the `#published` method on an instance of the factory. The method takes no arguments, but grabs the books collection from the factory instance. The block returns an instance of `PublishedBooksCommand`, which is then returned by `#published`.
|
1037
924
|
|
1038
|
-
|
925
|
+
```ruby
|
926
|
+
books = [Book.new, Book.new(publisher: 'Baen'), Book.new(publisher: 'Tor')]
|
927
|
+
factory = BookFactory.new(books)
|
928
|
+
factory.books_collection #=> the books array
|
1039
929
|
|
1040
|
-
|
930
|
+
command = factory.published #=> an instance of PublishedBooksCommand
|
931
|
+
result = command.call #=> an instance of Cuprum::Result
|
932
|
+
ary = result.value #=> an array with the published books
|
1041
933
|
|
1042
|
-
|
934
|
+
ary.count #=> 2
|
935
|
+
ary.any? { |book| book.publisher == 'Baen' } #=> true
|
936
|
+
ary.any? { |book| book.publisher.nil? } #=> false
|
937
|
+
```
|
1043
938
|
|
1044
|
-
|
939
|
+
Simple commands can be defined directly in the block, rather than referencing an existing command class:
|
1045
940
|
|
1046
|
-
|
941
|
+
```ruby
|
942
|
+
class BookFactory < Cuprum::CommandFactory
|
943
|
+
command :published_by_baen do
|
944
|
+
Cuprum::Command.new do |books|
|
945
|
+
books.select { |book| book.publisher == 'Baen' }
|
946
|
+
end
|
947
|
+
end
|
948
|
+
end
|
1047
949
|
|
1048
|
-
|
950
|
+
books = [Book.new, Book.new(publisher: 'Baen'), Book.new(publisher: 'Tor')]
|
951
|
+
factory = BookFactory.new(books)
|
1049
952
|
|
1050
|
-
|
953
|
+
command = factory.published_by_baen #=> an instance of the anonymous command
|
954
|
+
result = command.call #=> an instance of Cuprum::Result
|
955
|
+
ary = result.value #=> an array with the selected books
|
1051
956
|
|
1052
|
-
|
957
|
+
ary.count #=> 1
|
958
|
+
```
|
1053
959
|
|
1054
|
-
|
960
|
+
#### The ::command_class Method
|
1055
961
|
|
1056
|
-
|
962
|
+
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.
|
1057
963
|
|
1058
|
-
|
964
|
+
```ruby
|
965
|
+
class SelectByAuthorCommand < Cuprum::Command
|
966
|
+
def initialize(author)
|
967
|
+
@author = author
|
968
|
+
end
|
1059
969
|
|
1060
|
-
|
970
|
+
attr_reader :author
|
1061
971
|
|
1062
|
-
|
972
|
+
private
|
1063
973
|
|
1064
|
-
|
974
|
+
def process(books)
|
975
|
+
books.select { |book| book.author == author }
|
976
|
+
end
|
977
|
+
end
|
1065
978
|
|
1066
|
-
|
979
|
+
class BooksFactory < Cuprum::CommandFactory
|
980
|
+
command_class :select_by_author do
|
981
|
+
SelectByAuthorCommand
|
982
|
+
end
|
983
|
+
end
|
984
|
+
```
|
1067
985
|
|
1068
|
-
|
986
|
+
The command class can be accessed directly as a constant on the factory instance:
|
1069
987
|
|
1070
|
-
|
988
|
+
```ruby
|
989
|
+
factory = BookFactory.new
|
990
|
+
factory::SelectByAuthor #=> SelectByAuthorCommand
|
991
|
+
```
|
1071
992
|
|
1072
|
-
|
993
|
+
The factory instance now defines a `#select_by_author` method, which returns an instance of our defined command class. This command instance can be called like any command, or returned or passed around like any other object.
|
1073
994
|
|
1074
|
-
|
995
|
+
```ruby
|
996
|
+
factory = BookFactory.new
|
997
|
+
books = [
|
998
|
+
Book.new,
|
999
|
+
Book.new(author: 'Arthur C. Clarke'),
|
1000
|
+
Book.new(author: 'Ursula K. Le Guin')
|
1001
|
+
]
|
1075
1002
|
|
1076
|
-
|
1003
|
+
command = factory.select_by_author('Ursula K. Le Guin')
|
1004
|
+
#=> an instance of SelectByAuthorCommand
|
1005
|
+
command.author #=> 'Ursula K. Le Guin'
|
1077
1006
|
|
1078
|
-
|
1007
|
+
result = command.call(books) #=> an instance of Cuprum::Result
|
1008
|
+
ary = result.value #=> an array with the selected books
|
1079
1009
|
|
1080
|
-
|
1010
|
+
ary.count #=> 1
|
1011
|
+
ary.any? { |book| book.author == 'Ursula K. Le Guin' } #=> true
|
1012
|
+
ary.any? { |book| book.author == 'Arthur C. Clarke' } #=> false
|
1013
|
+
ary.any? { |book| book.author.nil? } #=> false
|
1014
|
+
```
|
1081
1015
|
|
1082
|
-
|
1016
|
+
The block is evaluated in the context of the factory instance. This means that instance variables or methods are available to the block, allowing you to create custom command subclasses with instance-specific configuration.
|
1083
1017
|
|
1084
|
-
|
1018
|
+
```ruby
|
1019
|
+
class SaveBookCommand < Cuprum::Command
|
1020
|
+
def initialize(collection = [])
|
1021
|
+
@collection = collection
|
1022
|
+
end
|
1085
1023
|
|
1086
|
-
|
1024
|
+
attr_reader :collection
|
1087
1025
|
|
1088
|
-
|
1026
|
+
private
|
1089
1027
|
|
1090
|
-
|
1028
|
+
def process(book)
|
1029
|
+
books << book
|
1091
1030
|
|
1092
|
-
|
1031
|
+
book
|
1032
|
+
end
|
1033
|
+
end
|
1093
1034
|
|
1094
|
-
|
1035
|
+
class BookFactory < Cuprum::CommandFactory
|
1036
|
+
command :save do
|
1037
|
+
collection = self.books_collection
|
1095
1038
|
|
1096
|
-
|
1039
|
+
Class.new(SaveBookCommand) do
|
1040
|
+
define_method(:initialize) do
|
1041
|
+
@books = collection
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
end
|
1097
1045
|
|
1098
|
-
|
1046
|
+
def initialize(books)
|
1047
|
+
@books_collection = books
|
1048
|
+
end
|
1099
1049
|
|
1100
|
-
|
1050
|
+
attr_reader :books_collection
|
1051
|
+
end
|
1052
|
+
```
|
1101
1053
|
|
1102
|
-
|
1054
|
+
The custom command subclass can be accessed directly as a constant on the factory instance:
|
1103
1055
|
|
1104
|
-
|
1056
|
+
```ruby
|
1057
|
+
books = [Book.new, Book.new, Book.new]
|
1058
|
+
factory = BookFactory.new(books)
|
1059
|
+
factory::Save #=> a subclass of SaveBookCommand
|
1105
1060
|
|
1106
|
-
|
1061
|
+
command = factory::Save.new # an instance of the command subclass
|
1062
|
+
command.collection #=> the books array
|
1063
|
+
command.collection.count #=> 3
|
1064
|
+
```
|
1107
1065
|
|
1108
|
-
|
1066
|
+
The factory instance now defines a `#save` method, which returns an instance of our custom command subclass. This command instance can be called like any command, or returned or passed around like any other object.
|
1109
1067
|
|
1110
|
-
|
1068
|
+
The custom command subclass can be accessed directly as a constant on the factory instance:
|
1111
1069
|
|
1112
|
-
|
1070
|
+
```ruby
|
1071
|
+
books = [Book.new, Book.new, Book.new]
|
1072
|
+
factory = BookFactory.new(books)
|
1073
|
+
command = factory.save # an instance of the command subclass
|
1074
|
+
command.collection #=> the books array
|
1075
|
+
command.collection.count #=> 3
|
1113
1076
|
|
1114
|
-
|
1077
|
+
book = Book.new(title: 'The Farthest Shore', author: 'Ursula K. Le Guin')
|
1078
|
+
result = command.call(book) #=> an instance of Cuprum::Result
|
1115
1079
|
|
1116
|
-
|
1080
|
+
books.count #=> 4
|
1081
|
+
books.include?(book) #=> true
|
1082
|
+
```
|
1117
1083
|
|
1118
|
-
|
1084
|
+
### Built In Commands
|
1119
1085
|
|
1120
|
-
|
1086
|
+
Cuprum includes a small number of predefined commands and their equivalent operations.
|
1121
1087
|
|
1122
|
-
|
1088
|
+
#### IdentityCommand
|
1123
1089
|
|
1124
|
-
|
1090
|
+
require 'cuprum/built_in/identity_command'
|
1125
1091
|
|
1126
|
-
|
1092
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
|
1127
1093
|
|
1128
|
-
|
1094
|
+
A pregenerated command that returns the value or result with which it was called.
|
1129
1095
|
|
1130
|
-
|
1096
|
+
```ruby
|
1097
|
+
command = Cuprum::BuiltIn::IdentityCommand.new
|
1098
|
+
result = command.call('expected value')
|
1099
|
+
result.value #=> 'expected value'
|
1100
|
+
result.success? #=> true
|
1101
|
+
```
|
1131
1102
|
|
1132
|
-
####
|
1103
|
+
#### IdentityOperation
|
1133
1104
|
|
1134
|
-
|
1105
|
+
require 'cuprum/built_in/identity_operation'
|
1135
1106
|
|
1136
|
-
|
1107
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
|
1137
1108
|
|
1138
|
-
|
1109
|
+
A pregenerated operation that sets its result to the value or result with which it was called.
|
1139
1110
|
|
1140
|
-
|
1111
|
+
```ruby
|
1112
|
+
operation = Cuprum::BuiltIn::IdentityOperation.new.call('expected value')
|
1113
|
+
operation.value #=> 'expected value'
|
1114
|
+
operation.success? #=> true
|
1115
|
+
```
|
1141
1116
|
|
1142
|
-
####
|
1117
|
+
#### NullCommand
|
1143
1118
|
|
1144
|
-
|
1145
|
-
spy_on(command_class) { |spy| ... } #=> nil
|
1119
|
+
require 'cuprum/built_in/null_command'
|
1146
1120
|
|
1147
|
-
|
1121
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
|
1148
1122
|
|
1149
|
-
|
1150
|
-
spy = Cuprum::Utils::InstanceSpy.spy_on(CustomCommand)
|
1123
|
+
A pregenerated command that does nothing when called. Accepts any arguments.
|
1151
1124
|
|
1152
|
-
|
1125
|
+
```ruby
|
1126
|
+
command = Cuprum::BuiltIn::NullCommand.new
|
1127
|
+
result = command.call
|
1128
|
+
result.value #=> nil
|
1129
|
+
result.success? #=> true
|
1130
|
+
```
|
1153
1131
|
|
1154
|
-
|
1132
|
+
#### NullOperation
|
1155
1133
|
|
1156
|
-
|
1157
|
-
spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedCommand)
|
1134
|
+
require 'cuprum/built_in/null_operation'
|
1158
1135
|
|
1159
|
-
|
1136
|
+
[Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
|
1160
1137
|
|
1161
|
-
|
1162
|
-
chain { |result| ChainedCommand.new.call(result) }.
|
1163
|
-
call
|
1138
|
+
A pregenerated operation that does nothing when called. Accepts any arguments.
|
1164
1139
|
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1140
|
+
```ruby
|
1141
|
+
operation = Cuprum::BuiltIn::NullOperation.new.call
|
1142
|
+
operation.value #=> nil
|
1143
|
+
operation.success? #=> true
|
1144
|
+
```
|
1168
1145
|
|
1169
|
-
|
1170
|
-
end # spy_on
|
1146
|
+
## Reference
|
1171
1147
|
|
1172
|
-
|
1148
|
+
Method and class documentation is available courtesy of [RubyDoc](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master).
|