cuprum 0.8.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cafecaeff8b2c0209616cd9303613d88723c2477
4
- data.tar.gz: b4d1c2cdde687815ac835631c460a45d15a3306d
2
+ SHA256:
3
+ metadata.gz: 87866e465634782fef9cd2ce7bebf42f56eeb789d235874a280134e6d2431104
4
+ data.tar.gz: e60264b79e82f77f5cbc2149c6522f0ef74fad2c198238ea6db3338ba3e35ce3
5
5
  SHA512:
6
- metadata.gz: f465ecb20dd65d5e835f6f506bbaca272e39637a5b127fac31929fef88a880069ed8b7f86802b207b01b767de952d641a688344dfabd8b1d4347d904daf926d7
7
- data.tar.gz: be12f0a8d8be45ba1aabc854b48be53be13e2716d39e1c48fecae038b99019880c19686fd96bb1d1ff04272400650723df47b789ebe48b60da1e6ba844175f3a
6
+ metadata.gz: 89aa00632c89bfddcc3063d0aff7344af81e36e4568d6899ef16c5a80b464ae43f7b530f689fb6a8e8f7e2c3987ee4463014744ab242b20828959483e070d968
7
+ data.tar.gz: 05cf7814eeff05e179bc006d1a9c80f8bc3ca7b9e4ca12256b34f65bc6aa1ba253e12a2c00fe411e38162b65d9d1a5a1ac6a3df3e820af40cec2944291b6e3a5
@@ -1,5 +1,81 @@
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
+
3
79
  ## 0.8.0
4
80
 
5
81
  The "We Have The Technology" Update.
@@ -60,21 +136,21 @@ The "Name Not Found For NullFunction" Update.
60
136
 
61
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).
62
138
 
63
- ## Operations
139
+ ### Operations
64
140
 
65
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.
66
142
 
67
- ## Results
143
+ ### Results
68
144
 
69
145
  Implemented `Cuprum::Result#==` as a fuzzy comparison, allowing a result to be equal to any object with the same value and status.
70
146
 
71
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.
72
148
 
73
- ## Utilities
149
+ ### Utilities
74
150
 
75
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.
76
152
 
77
- ## Built In Functions
153
+ ### Built In Functions
78
154
 
79
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.
80
156
 
@@ -84,7 +160,7 @@ Added the `IdentityFunction` and `IdentityOperation` predefined classes, which r
84
160
 
85
161
  The "Halt And Catch Fire" Update.
86
162
 
87
- ## Functions
163
+ ### Functions
88
164
 
89
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.
90
166
 
@@ -94,11 +170,11 @@ Can now generate results with custom error objects by overriding the `#build_err
94
170
 
95
171
  Fixed an inconsistency issue when a function block or `#process` method returned an instance of `Cuprum::Result`.
96
172
 
97
- ## Operations
173
+ ### Operations
98
174
 
99
175
  Calling `#call` on an operation now returns the operation instance.
100
176
 
101
- ## Results
177
+ ### Results
102
178
 
103
179
  Can now call `#success!` or `#failure!` to override the default, error-based status.
104
180
 
@@ -108,11 +184,11 @@ Can now call `#halt!` and check the `#halted?` status. A halted result will prev
108
184
 
109
185
  The "Nothing To Lose But Your Chains" Update.
110
186
 
111
- ## Functions
187
+ ### Functions
112
188
 
113
189
  Now support chaining via the `#chain`, `#then`, and `#else` methods.
114
190
 
115
- ## Results
191
+ ### Results
116
192
 
117
193
  Can pass a value and/or an errors object to the constructor.
118
194
 
@@ -1,46 +1,14 @@
1
1
  # Development
2
2
 
3
- ## Version 0.9.0
4
-
5
- The "Second Star To The Right" Update
6
-
7
- ### Actions
8
-
9
- #### LifecycleHooks
10
-
11
- - :before, :after hooks
12
- - NOT included in Command by default
13
-
14
- ## Version 0.10.0
15
-
16
- 'The "Out Of Context Problem" Update'
17
-
18
- ### Commands
19
-
20
- - #context object
21
-
22
3
  ## Version 1.0.0
23
4
 
24
- 'The "Look On My Works, Ye Mighty, and Despair" Update'
5
+ The "Look On My Works, Ye Mighty, and Despair" Update
25
6
 
26
7
  - Integration specs.
27
- - Configuration option to raise, warn, ignore discarded results.
28
8
  - Code cleanup: Hash syntax, remove end comments, remove file headers
9
+ - Status Badges!
29
10
 
30
- ### Commands
31
-
32
- - Command#to_proc
33
- - :clear_errors => true option on #chain
34
-
35
- ### Commands - Built In
36
-
37
- - MapCommand - wraps a command (or proc) and returns Result with value, errors
38
- as array
39
- - RetryCommand
40
-
41
- ### Documentation
42
-
43
- Chaining Case Study: |
11
+ Steps Case Study: |
44
12
 
45
13
  CMS application - creating a new post.
46
14
  Directory has many Posts
@@ -54,20 +22,23 @@ Chaining Case Study: |
54
22
  Create ContentVersion
55
23
  Tags.each { FindOrCreate Tag }
56
24
 
25
+ ### Commands
26
+
27
+ - Remove `Cuprum::Chaining`.
28
+ - Implement `Command#to_proc`.
29
+
57
30
  ## Future Versions
58
31
 
59
32
  ### Commands
60
33
 
61
- - command currying
34
+ - Implement #<<, #>> composition methods.
35
+ - Calls commands in order passing values.
36
+ - Return Result early on Failure (or not Success), otherwise final Result.
62
37
 
63
- #### Cuprum::DSL
38
+ #### DSL
64
39
 
65
40
  - ::process - shortcut for defining #process
66
41
  - ::rescue - `rescue StandardError do ... end`, rescues matched errors in #process
67
- - chaining methods:
68
- - ::chain (::success, ::failure):
69
- on #initialize, chains the given command. Can be given a command class
70
- (if ::new takes no arguments) or a block that returns a command.
71
42
  - constructor methods:
72
43
  - Programmatically generate a constructor method. Raises an error if
73
44
  #initialize is defined. Automatically sets instance variables on initialize,
@@ -78,4 +49,49 @@ Chaining Case Study: |
78
49
  optional arguments and their default values.
79
50
  - ::keywords - sets keyword arguments; same arguments as ::arguments.
80
51
 
81
- #### Hooks
52
+ #### Dependency Injection
53
+
54
+ - shorthand for referencing a sequence of operations
55
+
56
+ ### Commands - Built In
57
+
58
+ - MapCommand - wraps a command (or proc) and returns Result with value, errors
59
+ as array
60
+ - RetryCommand - takes command, retry count
61
+ - optional only:, except: - restrict what errors are retried
62
+
63
+ ### Matcher
64
+
65
+ - Handle success(), failure(), failure(SomeError) cases.
66
+ - Custom matcher to handle additional cases - halted, pending, etc?
67
+
68
+ ### Middleware
69
+
70
+ - Implement Command.subclass
71
+ - Curries constructor arguments
72
+ - Implement Cuprum::Middleware
73
+ - #process takes next command, \*args, \*\*kwargs
74
+ - calls next command with \*args, \*\*kwargs
75
+ - .apply takes middleware: array, root: command
76
+ - Implement Cuprum::AppliedMiddleware < Cuprum::Command
77
+ - has readers #root (Class), #middleware (Array<Class>)
78
+ - #initialize
79
+ - initializes root command (passing constructor parameters)
80
+ - initializes each middleware command
81
+ - if Class defining .instance, call .instance
82
+ - if Class, call .new
83
+ - if Proc, call #call with constructor parameters
84
+ - calls Middleware.apply and caches as private #applied
85
+ - #call
86
+ - delegates to #applied
87
+
88
+ ### RSpec
89
+
90
+ - be_callable matcher - delegates to respond_to(), but check arguments of
91
+ private #process method
92
+ - call_command_step matcher
93
+ - (optionally) alias be_a_result family as have_result for operations
94
+
95
+ ### Steps::Strict
96
+
97
+ - #step raises exception unless block or method returns a result
data/README.md CHANGED
@@ -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) - A data object with a `#value`, an `#errors` object, and `#success?` and `#failure?` status methods.
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,12 +23,12 @@ 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, here are several alternatives:
26
-
27
- - Service objects. A common pattern used when first refactoring an application that has outgrown its abstractions. Service objects are simple and let you group related functionality, but they are harder to compose and require firm conventions to tame.
28
- - The [Interactor](https://github.com/collectiveidea/interactor) gem. Provides an `Action` module to implement logic and an `Organizer` module to manage control flow. Supports before, around, and after hooks.
29
- - The [Waterfall](https://github.com/apneadiving/waterfall) gem. Focused more on control flow.
30
- - [Trailblazer](http://trailblazer.to/) Operations. A pipeline-based approach to control flow, and can integrate tightly with other Trailblazer elements.
26
+ If you want to extract your logic but Cuprum is not the right solution for you, there are a number of alternatives, including
27
+ [ActiveInteraction](https://github.com/AaronLasseigne/active_interaction),
28
+ [Dry::Monads](https://dry-rb.org/gems/dry-monads/),
29
+ [Interactor](https://github.com/collectiveidea/interactor),
30
+ [Trailblazer](http://trailblazer.to/) Operations,
31
+ and [Waterfall](https://github.com/apneadiving/waterfall).
31
32
 
32
33
  ### Compatibility
33
34
 
@@ -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) 2017 Rob Smith
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. The result wraps the returned data (with the `#value` method), any `#errors` generated when running the Command, and the overall status with the `#success?` and `#failure` methods. For more details about Cuprum::Result, [see below](#label-Results).
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 # method process
81
- end # class
81
+ end
82
+ end
82
83
 
83
84
  command = BuildPostCommand.new
84
- result = command.call(:title => 'The Hobbit')
85
- result.class #=> Cuprum::Result
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 # constructor
102
+ end
101
103
 
102
104
  def process book
103
- @repository.persist(book)
104
- end # method process
105
- end # class
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(:title => 'The Fellowship of the Ring'),
109
- Book.new(:title => 'The Two Towers'),
110
- Book.new(:title => 'The Return of the King')
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 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`.
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 methods `#to_result`, `#value`, and `#success?` is considered to be a result.
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 is the command's own result object, then the `#value` of the returned result is unchanged. For convenience, methods to set the result status or mark the result as halted will return the result.
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 { result.failure! }
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, starfighter!'
167
- ```
168
-
169
- In some cases, returning a result directly will discard information on the command's own result object. When this occurs, Cuprum will display a warning.
170
-
171
- ```ruby
172
- command =
173
- Cuprum::Command.new do
174
- result.errors << 'Oops! We are throwing away this result.'
175
-
176
- Cuprum::Result.new
177
- end
178
-
179
- #=> This calls Kernel#warn with a warning message.
180
- result = command.call
181
- result.class #=> Cuprum::Result
182
- result.value #=> nil
183
- result.success? #=> true
184
- result.errors #=> []
165
+ result.value #=> 'Greetings, programs!'
185
166
  ```
186
167
 
187
168
  #### Success, Failure, and Errors
188
169
 
189
- Whether defined with a block or in the `#process` method, the Command implementation can access an `#errors` object while in the `#call` method. Any errors added to the errors object will be exposed by the `#errors` method on the result object.
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
- errors << 'This book does not have a cover.'
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 # method process
206
- end # class
184
+ end
185
+ end
207
186
  ```
208
187
 
209
- In addition, the result object defines `#success?` and `#failure?` predicates. If the result has no errors, then `#success?` will return true and `#failure?` will return false.
188
+ In addition, the result object defines `#success?` and `#failure?` predicates.
210
189
 
211
190
  ```ruby
212
- book = Book.new(:title => 'The Silmarillion', :cover => Cover.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.errors #=> []
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 errors, `#success?` will return false and `#failure?` will return true.
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(:title => 'The Silmarillion', :cover => nil)
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.errors #=> ['This book does not have a cover.']
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
- #### Composing Commands
216
+ #### Command Currying
217
+
218
+ Cuprum::Command defines the `#curry` method, which allows for partial application of command objects. Partial application (more commonly referred to, if imprecisely, as currying) refers to fixing some number of arguments to a function, resulting in a function with a smaller number of arguments.
219
+
220
+ In Cuprum's case, a curried (partially applied) command takes an original command and pre-defines some of its arguments. When the curried command is called, the predefined arguments and/or keywords will be combined with the arguments passed to #call.
221
+
222
+ ##### Currying Arguments
223
+
224
+ We start by defining the base command. In this case, our base command takes two string arguments - a greeting and a person to be greeted.
225
+
226
+ ```ruby
227
+ say_command = Cuprum::Command.new do |greeting, person|
228
+ "#{greeting}, #{person}!"
229
+ end
230
+ say_command.call('Hello', 'world')
231
+ #=> returns a result with value 'Hello, world!'
232
+ ```
233
+
234
+ Next, we create a curried command. Here, we pass in one argument. This will set the first argument to always be "Greetings"; therefore, our curried command only takes one argument, the name of the person being greeted.
235
+
236
+ ```ruby
237
+ greet_command = say_command.curry('Greetings')
238
+ greet_command.call('programs')
239
+ #=> returns a result with value 'Greetings, programs!'
240
+ ```
241
+
242
+ Alternatively, we could pass both arguments to `#curry`. In this case, our curried argument does not take any arguments, and will always return the same string.
243
+
244
+ ```ruby
245
+ recruit_command = say_command.curry('Greetings', 'starfighter')
246
+ recruit_command.call
247
+ #=> returns a result with value 'Greetings, starfighter!'
248
+ ```
249
+
250
+ ##### Currying Keywords
251
+
252
+ We can also pass keywords to `#curry`. Again, we start by defining our base command. In this case, our base command takes a mathematical operation (addition, subtraction, multiplication, etc) and a list of operands.
253
+
254
+ ```ruby
255
+ math_command = Cuprum::Command.new do |operands:, operation:|
256
+ operations.reduce(&operation)
257
+ end
258
+ math_command.call(operands: [2, 2], operation: :+)
259
+ #=> returns a result with value 4
260
+ ```
261
+
262
+ Our curried command still takes two keywords, but now the operation keyword is optional. It now defaults to :\*, for multiplication.
263
+
264
+ ```ruby
265
+ multiply_command = math_command.curry(operation: :*)
266
+ multiply_command.call(operands: [3, 3])
267
+ #=> returns a result with value 9
268
+ ```
269
+
270
+ ### Composing Commands
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 # command
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 = IncrementCommand.new.call(i).value }
317
+ addend.times { i = increment_command.call(i).value }
279
318
 
280
319
  i
281
320
  end
@@ -287,442 +326,420 @@ add_two_command.call(1).value #=> 3
287
326
  add_two_command.call(8).value #=> 10
288
327
  ```
289
328
 
290
- ### Chaining Commands
329
+ You can achieve even more powerful composition by passing in a command as an argument to a method, or by creating a method that returns a command.
291
330
 
292
- Cuprum::Command also defines methods for chaining commands together. When a chain of commands is called, each command in the chain is called in sequence and passed the value of the previous command. The result of the last command in the chain is returned from the chained call.
331
+ #### Commands As Arguments
332
+
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:
293
334
 
294
335
  ```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 }
336
+ class RepeatCommand
337
+ def initialize(count)
338
+ @count = count
339
+ end
298
340
 
299
- table_name_command =
300
- name_command
301
- .chain(pluralize_command)
302
- .chain(underscore_command)
341
+ private
303
342
 
304
- result = table_name_command.call(LibraryCard)
305
- result.class #=> Cuprum::Result
306
- result.value #=> 'library_cards'
343
+ def process(command)
344
+ @count.times { command.call }
345
+ end
346
+ end
347
+
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
307
351
  ```
308
352
 
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.
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.
310
354
 
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:
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.
312
356
 
313
- ```ruby
314
- table_name_command =
315
- Cuprum::Command.new { |klass| klass.name }
316
- .chain { |str| str.pluralize }
317
- .chain { |str| str.underscore }
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.
318
358
 
319
- table_name_command =
320
- Cuprum::Command.new(&:name).chain(&:pluralize).chain(&:underscore)
321
- ```
359
+ ```ruby
360
+ class DeliverEbook < Cuprum::Command; end
322
361
 
323
- #### Chaining Details
362
+ class ShipDomestic < Cuprum::Command; end
324
363
 
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:
364
+ class ShipInternational < Cuprum::Command; end
326
365
 
327
- ```ruby
328
- first_command = Cuprum::Command.new { puts 'First command!' }
329
- first_command.call #=> Outputs 'First command!' to STDOUT.
366
+ class FulfillOrder < Cuprum::Command
367
+ def initialize(delivery_command)
368
+ @delivery_command = delivery_command
369
+ end
330
370
 
331
- second_command = first_command.chain { puts 'Second command!' }
332
- second_command.call #=> Outputs 'First command!' then 'Second command!'.
371
+ private
333
372
 
334
- # The original command is unchanged.
335
- first_command.call #=> Outputs 'First command!' to STDOUT.
373
+ def process(book:, user:)
374
+ # Here we will check inventory, process payments, and so on. The final step
375
+ # is actually delivering the book to the user:
376
+ delivery_command.call(book: book, user: user)
377
+ end
378
+ end
336
379
  ```
337
380
 
338
- When a chained command is called, the original command is called with whatever parameters are passed in to the `#call` method, and the command executes the `#process` method as normal, generating a `Cuprum::Result` and assigning it a value and optionally errors or a status. Rather than returning this result, however, it is passed on to the next command in the chain. In the context of a chained command, this means two things.
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).
339
382
 
340
- First, the next command is called. If the next command does not take any arguments, it is not passed any arguments. If the next command takes one or more arguments, it is passed the `#value` of that previous result.
383
+ #### Commands As Returned Values
341
384
 
342
- ```ruby
343
- double_command = Cuprum::Command.new { |i| 2 * i }
344
- increment_command = Cuprum::Command.new { |i| 1 + i }
345
- square_command = Cuprum::Command.new { |i| i * i }
346
- chained_command =
347
- double_command
348
- .chain(increment_command)
349
- .chain(square_command)
350
-
351
- # First, the double_commmand is called with 2. This returns a Cuprum::Result
352
- # with a value of 4.
353
- #
354
- # Next, the increment_command is called with 4, returning a result with value 5.
355
- #
356
- # Finally, the square_command is called with 5, returning a result with a value
357
- # of 25. This final result is returned by #call.
358
- result = chained_command.call(2)
359
- result.class #=> Cuprum::Result
360
- result.value #=> 25
361
- ```
385
+ We can also return commands as an object from a method call or from another command. One use case for this is the Abstract Factory pattern.
386
+
387
+ Consider our shipping example, above. The traditional way to generate a shipping command is to use an `if-then-else` or `case` construct, which would be embedded in whatever code is calling `FulfillOrder`. This adds complexity and increases the testing burden.
362
388
 
363
- Second, the previous result is set as the result of the next command (before it is evaluated). This makes it available to the next command during execution as the `#result` method, and makes available the value, errors, and status of the previous command (and avoids unnecessary object allocation). The value of the result will be updated to reflect the return value of the next command execution.
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.
364
390
 
365
391
  ```ruby
366
- validate_command =
367
- Cuprum::Command.new do |object|
368
- result.errors << 'Object is invalid!' unless object.valid?
392
+ class ShippingMethod < Cuprum::Command
393
+ private
369
394
 
370
- object
371
- end
372
- persist_command =
373
- Cuprum::Command.new do |object|
374
- object.save if result.success?
395
+ def process(book:, user:)
396
+ return DeliverEbook.new(user.email) if book.ebook?
397
+
398
+ return ShipDomestic.new(user.address) if user.address&.domestic?
399
+
400
+ return ShipInternational.new(user.address) if user.address&.international?
375
401
 
376
- object
402
+ err = Cuprum::Error.new(message: 'user does not have a valid address')
403
+
404
+ failure(err)
377
405
  end
378
- chained_command = validate_command.chain(persist_command)
379
-
380
- # First, validate_command is called with a valid object. This creates a result
381
- # with no errors and whose value is the valid object.
382
- #
383
- # Then, persist_command is called with the object, and its result is assigned to
384
- # the previous result. Since there are no errors on the result, the object is
385
- # saved. Finally, the value of the result is set to the object, and the result
386
- # is returned.
387
- result = chained_command.call(a_valid_object) #=> Saves the object.
388
- result.value #=> a_valid_object
389
- result.errors #=> []
390
- a_valid_object.persisted? #=> true
391
-
392
- # First, validate_command is called with an invalid object. This creates a
393
- # result whose value is the invalid object, and with errors 'Object is
394
- # invalid!'.
395
- #
396
- # Then, persist_command is called with the object, and its result is assigned to
397
- # the previous result. Since the result has an error, the object is not saved.
398
- # Finally, the value of the result is set to the object, and the result
399
- # is returned.
400
- result = chained_command.call(an_invalid_object) #=> Does not save the object.
401
- result.value #=> an_invalid_object
402
- result.errors #=> ['Object is invalid!']
403
- an_invalid_object.persisted? #=> false
406
+ end
404
407
  ```
405
408
 
406
- #### Conditional Chaining
407
-
408
- The `#chain` method can be passed an optional `:on => value` keyword. This keyword determines whether or not the chained command will execute, based on the previous result status. Possible values are `:success`, `:failure`, `:always`, or `nil`. The default value is `nil`.
409
+ 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.
409
410
 
410
- If the `:on` keyword is omitted, the chained command will always be executed after the previous command unless the result is halted (see [Halting A Command Chain](#label-Commands))
411
+ 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.
411
412
 
412
- If the command is chained with `:on => :success`, then the chained command will only execute if the previous result is passing, e.g. the `#success?` method returns true and the command is not halted. A result is passing if there are no errors, or if the status is set to `:success`.
413
+ ### Command Steps
413
414
 
414
- If the command is chained with `:on => :failure`, then the chained command will only execute if the previous result is failing, e.g. the `#success?` method returns false and the command is not halted. A result is failing if the errors object is not empty, or if the status is set to `:failure`.
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.
415
416
 
416
- If the command is chained with `:on => always`, then the chained command will always be executed, even if the previous result is halted.
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.
417
418
 
418
419
  ```ruby
419
- find_command =
420
- Cuprum::Command.new do |attributes|
421
- book = Book.where(:id => attributes[:id]).first
420
+ triple_command = Cuprum::Command.new { |i| success(3 * i) }
422
421
 
423
- result.errors << 'Book not found' unless book
422
+ int = 2
423
+ int = step { triple_command.call(int) } #=> returns 6
424
+ int = step { triple_command.call(int) } #=> returns 18
425
+ ```
424
426
 
425
- book
426
- end
427
- create_command =
428
- Cuprum::Command.new do |attributes|
429
- book = Book.new(attributes)
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.
430
428
 
431
- if book.save
432
- result.success!
433
- else
434
- book.errors.full_messages.each { |message| result.errors << message }
435
- end
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`.
436
430
 
437
- book
438
- end
431
+ ```ruby
432
+ divide_command = Cuprum::Command.new do |dividend, divisor|
433
+ return failure('divide by zero') if divisor.zero?
439
434
 
440
- find_or_create_command = find_command.chain(create_command, :on => :failure)
435
+ success(dividend / divisor)
436
+ end
441
437
 
442
- # With a book that exists in the database, the find_command is called and
443
- # returns a result with no errors and a value of the found book. The
444
- # create_command is not called.
445
- hsh = { id: 0, title: 'Journey to the West' }
446
- result = find_or_create_command.call(hsh)
447
- book = result.value
448
- book.id #=> 0
449
- book.title #=> 'Journey to the West'
450
- result.success? #=> true
451
- result.errors #=> []
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"]
438
+ value = step { divide_command.call(10, 5) } #=> returns 2
439
+ value = step { divide_command.call(2, 0) } #=> throws :cuprum_failed_result
476
440
  ```
477
441
 
478
- The `#success` method can be used as shorthand for `chain(command, :on => :success)`. Likewise, the `#failure` method can be used in place of `chain(command, :on => :failure)`.
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.
479
443
 
480
- #### Halting A Command Chain
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:
481
445
 
482
- If the `#halt` method is called as part of a Command block or `#process` method, the command chain is halted. Any subsequent chained commands will not be called unless they were chained with the `:on => :always` option. This allows you to terminate a Command chain early without having to raise and rescue an exception.
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.
483
449
 
484
- ```ruby
485
- double_command = Cuprum::Command.new { |i| 2 * i }
486
- halt_command = Cuprum::Command.new { |value| value.tap { result.halt! } }
487
- add_one_command = Cuprum::Command.new { |i| i + 1 }
488
-
489
- chained_command =
490
- double_command
491
- .chain(halt_command)
492
- .chain(add_one_command)
493
- .chain(:on => :always) { |count| "There are #{count} lights!" }
494
-
495
- # First, double_command is called with 2, returning a result with no errors and
496
- # a value of 4. Then, halt_command is called, which marks the result as halted.
497
- # Because the result is now halted, the add_one_command is not called. Finally,
498
- # the last command is called (even though the result is halted, the command is
499
- # chained :on => :always), which returns a result with the string value. The
500
- # result is passing, but is still halted.
501
- result = chained_command.call(2)
502
- result.value #=> 'There are 4 lights!'
503
- result.success? #=> true
504
- result.halted? #=> true
505
- ```
506
-
507
- ### Advanced Chaining
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.
508
451
 
509
- The `#tap_result` and `#yield_result` methods provide advanced control over the flow of chained commands.
452
+ ```ruby
453
+ class CheckUserStatus < Cuprum::Command; end
510
454
 
511
- #### Tap Result
455
+ class CreateBookReservation < Cuprum::Command; end
512
456
 
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.
457
+ class FindBookByTitle < Cuprum::Command; end
514
458
 
515
- ```ruby
516
- command =
517
- Cuprum::Command.new do
518
- result.errors << 'Example error'
459
+ class ReserveBookByTitle < Cuprum::Command
460
+ private
519
461
 
520
- 'Example value'
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) }
521
477
  end
522
- chained_command =
523
- command
524
- .tap_result do |result|
525
- puts "The result value was #{result.inspect}"
526
- end
478
+ end
479
+ ```
527
480
 
528
- # Prints 'The result value was "Example value"' to STDOUT.
529
- result = chained_command.call
481
+ First, our user may not have borrowing privileges. In this case, `CheckUserStatus` will fail, and neither of the subsequent steps will be called. The `#call` method will return the failing result from `CheckUserStatus`.
482
+
483
+ ```ruby
484
+ result = ReserveBookByTitle.new.call(
485
+ title: 'The C Programming Language',
486
+ user: 'Ed Dillinger'
487
+ )
530
488
  result.class #=> Cuprum::Result
531
- result.value #=> 'Example value'
532
489
  result.success? #=> false
533
- result.errors #=> ['Example error']
490
+ result.error #=> 'not authorized to reserve book'
534
491
  ```
535
492
 
536
- Like `#chain`, `#tap_result` can be given an `:on => value` keyword.
493
+ Second, our user may be valid but our requested title may not exist in the system. In this case, `FindBookByTitle` will fail, and the final step will not be called. The `#call` method will return the failing result from `FindBookByTitle`.
537
494
 
538
495
  ```ruby
539
- find_book_command =
540
- Cuprum::Command.new do |book_id|
541
- book = Book.where(:id => book_id).first
496
+ result = ReserveBookByTitle.new.call(
497
+ title: 'Using GOTO For Fun And Profit',
498
+ user: 'Alan Bradley'
499
+ )
500
+ result.class #=> Cuprum::Result
501
+ result.success? #=> false
502
+ result.error #=> 'title not found'
503
+ ```
542
504
 
543
- result.errors << "Unable to find book with id #{book_id}" unless book
505
+ Third, our user and book may be valid, but all of the copies are checked out. In this case, each of the steps will be called, and the `#call` method will return the failing result from `CreateBookReservation`.
544
506
 
545
- book
546
- end
507
+ ```ruby
508
+ result = ReserveBookByTitle.new.call(
509
+ title: 'Design Patterns: Elements of Reusable Object-Oriented Software',
510
+ user: 'Alan Bradley'
511
+ )
512
+ result.class #=> Cuprum::Result
513
+ result.success? #=> false
514
+ result.error #=> 'no copies available'
515
+ ```
547
516
 
548
- chained_command =
549
- find_book_command
550
- .tap_result(:on => :success) do |result|
551
- render :show, :locals => { :book => result.value }
552
- end
553
- .tap_result(:on => :failure) do
554
- redirect_to books_path
555
- end
517
+ Finally, if each of the steps succeeds, the `#call` method will return the result of the final step.
556
518
 
557
- # Calls find_book_command with the id, which queries for the book and returns a
558
- # result with a value of the book and no errors. Then, the first tap_result
559
- # block is evaluated, calling the render method and passing it the book via the
560
- # result.value method. Because the result is passing, the next block is skipped.
561
- # Finally, the result of find_book_command is returned unchanged.
562
- result = chained_command.call(valid_id)
519
+ ```ruby
520
+ result = ReserveBookByTitle.new.call(
521
+ title: 'The C Programming Language',
522
+ user: 'Alan Bradley'
523
+ )
563
524
  result.class #=> Cuprum::Result
564
- result.value #=> an instance of Book
565
525
  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']
526
+ result.value #=> an instance of BookReservation
578
527
  ```
579
528
 
580
- #### Yield Result
529
+ #### Using Methods As Steps
530
+
531
+ Steps can also be defined as method calls. Instead of providing a block to `#step`, provide the name of the method as the first argument, either as a symbol or as a string. Any subsequent arguments, keywords, or a block is passed to the method when it is called.
581
532
 
582
- The `#yield_result` method offers advanced control over a chained command step. The method takes a block and yields the previous result. If the object returned by the block is not a result, a new result is created with a value equal to the returned object. In either case, the result is returned and passed to the next command, or returned by `#call` if `#tap_result` is the last item in the chain.
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.
583
534
 
584
- Unlike `#chain`, the block in `#yield_result` yields the previous result, not the previous value. In addition, `#yield_result` does not automatically carry over any errors from the previous result, the result status, or whether the result was marked as halted.
535
+ We can use this to rewrite our `ReserveBookByTitle` command to use methods:
585
536
 
586
537
  ```ruby
587
- chained_command =
588
- Cuprum::Command.new do
589
- 'Example value'
538
+ class ReserveBookByTitle < Cuprum::Result
539
+ private
540
+
541
+ def check_user_status(user)
542
+ CheckUserStatus.new(user)
590
543
  end
591
- .yield_result do |result|
592
- result.errors << 'Example error'
593
544
 
594
- result
545
+ def create_book_reservation(book:, user:)
546
+ CreateBookReservation.new(book: book, user: user)
595
547
  end
596
- .yield_result do |result|
597
- "The last result was a #{result.success? ? 'success' : 'failure'}."
548
+
549
+ def find_book_by_title(title)
550
+ FindBookByTitle.new.call(title)
598
551
  end
599
552
 
600
- # The first command creates a passing result with a value of 'Example value'.
601
- #
602
- # This is passed to the first yield_result block, which adds the 'Example error'
603
- # string to the result errors, and then returns the result. Because the
604
- # yield_result block returns the previous result, it is then passed to the next
605
- # item in the chain.
606
- #
607
- # Finally, the second yield_result block is called, which checks the status of
608
- # the passed result. Since the second block does not return the previous result,
609
- # the previous result is discarded and a new result is created with the string
610
- # value starting with 'The last result was ...'.
611
- result = chained_command.call
612
- result.class #=> Cuprum::Result
613
- result.value #=> 'The last result was a failure.'
614
- result.success? #=> true
615
- result.errors #=> []
553
+ def process(title:, user:)
554
+ step :check_user_status, user
555
+
556
+ book = step :find_book_by_title, title
557
+
558
+ create_book_reservation, book: book, user: user
559
+ end
560
+ end
616
561
  ```
617
562
 
618
- Like `#chain`, `#yield_result` can be given an `:on => value` keyword.
563
+ In this case, our methods simply delegate to our previously defined commands. However, a more complex example could include other logic in each method, or even a sequence of steps defining subtasks for the method. The only requirement is that the method returns a result. You can use the `#success` helpers to wrap a non-result value, or the `#failure` helper to generate a failing result.
564
+
565
+ #### Using Steps Outside Of Commands
566
+
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.
568
+
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:
619
570
 
620
571
  ```ruby
621
- # The Collatz conjecture in mathematics concerns a sequence of numbers defined
622
- # as follows. Start with any positive integer. If the number is even, then
623
- # divide the number by two. If the number is odd, multiply by three and add one.
624
- # These steps are repeated until the value is one or the numbers loop.
625
- step_command =
626
- Cuprum::Command.new do |i|
627
- result.failure! unless i.even?
572
+ steps do
573
+ step { check_something }
628
574
 
629
- i
630
- end
631
- .yield_result(:on => :success) { |result| result.value / 2 }
632
- .yield_result(:on => :failure) { |result| 1 + 3 * result.value }
633
-
634
- # Because the value is even, the first command returns a passing result with a
635
- # value of 8. The first yield_result block is then called with that result,
636
- # returning a new result with a value of 4. The result is passing, so the second
637
- # yield_result block is skipped and the result is returned.
638
- result = collatz_command.call(8)
639
- result.value #=> 4
640
- result.success? #=> true
575
+ obj = step { find_something }
641
576
 
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
577
+ step :do_something, with: obj
578
+ end
649
579
  ```
650
580
 
651
- Under the hood, both `#chain` and `#tap_result` are implemented on top of `#yield_result`.
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`.
652
582
 
653
- #### Protected Chaining Methods
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:
654
584
 
655
- Each Command also defines the `#chain!`, `#tap_result!`, and `#yield_result!` methods - note the imperative `!`. These methods behave identically to their non-imperative counterparts, but they modify the current command directly instead of creating a clone. They are also protected methods, so they cannot be called from outside the command itself. These methods are designed for use when defining commands.
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.
656
588
 
657
589
  ```ruby
658
- # We subclass the build command, which will be executed first.
659
- class CreateCommentCommand < BuildCommentCommand
660
- include Cuprum::Chaining
661
- include Cuprum::Processing
662
- #
663
- def initialize
664
- # After the build step is run, we validate the comment.
665
- chain!(ValidateCommentCommand.new)
666
- #
667
- # If the validation passes, we then save the comment.
668
- chain!(SaveCommentCommand.new, on: :success)
669
- end
670
- end
590
+ class BooksController
591
+ include Cuprum::Steps
671
592
 
672
- Comment.count #=> 0
593
+ def create
594
+ attributes = params[:books]
595
+ result = steps do
596
+ @book = step :build_book, attributes
673
597
 
674
- body = 'Why do hot dogs come in packages of ten, and hot dog buns come in ' \
675
- 'packages of eight?'
676
- result = CreateCommentCommand.new.call({ user_id: '12345', body: body })
598
+ step :run_validations, @book
677
599
 
678
- result.value #=> an instance of Comment with the given user_id and body.
679
- result.success? #=> true
680
- Comment.count #=> 1; the comment was added to the database
600
+ step :persist_book, book
601
+ end
681
602
 
682
- result = CreateCommentCommand.new.call({ user_id: nil, body: body })
603
+ result.success ? redirect_to(@book) : render(:edit)
604
+ end
683
605
 
684
- result.value #=> an instance of Comment with the given user_id and body.
685
- result.success? #=> false
686
- result.errors #=> ["User id can't be blank"]
687
- Comment.count #=> 1; the comment was not added to the database
606
+ private
607
+
608
+ def build_book(attributes)
609
+ success(Book.new(attributes))
610
+ rescue InvalidAttributes
611
+ failure('attributes are invalid')
612
+ end
613
+
614
+ def persist_book(book)
615
+ book.save ? success(book) : failure('unable to persist book')
616
+ end
617
+
618
+ def run_validations(book)
619
+ book.valid? ? success : failure('book is invalid')
620
+ end
621
+ end
688
622
  ```
689
623
 
624
+ A few things to note about this example. First, we have a couple of examples of wrapping existing code in a result, both by rescuing exceptions (in `#build_book`) or by checking a returned status (in `#persist_book`). Second, note that each of our helper methods can be reused in other controller actions. For even more encapsulation and reusability, the next step might be to convert those methods to commands of their own.
625
+
626
+ You can define even more complex logic by defining multiple `#steps` blocks. Each block represents a series of tasks that will terminate on the first failure. Steps blocks can even be nested in one another, or inside a `#process` method.
627
+
690
628
  ### Results
691
629
 
692
630
  require 'cuprum'
693
631
 
694
632
  [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
695
633
 
696
- 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?`.
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).
635
+
636
+ ```ruby
637
+ result = Cuprum::Result.new
638
+
639
+ result.value #=> nil
640
+ result.error #=> nil
641
+ result.status #=> :success
642
+ result.success? #=> true
643
+ result.failure? #=> true
644
+ ```
645
+
646
+ Creating a result with a value stores the value.
697
647
 
698
648
  ```ruby
699
649
  value = 'A result value'.freeze
700
- result = Cuprum::Result.new(value)
650
+ result = Cuprum::Result.new(value: value)
701
651
 
702
652
  result.value #=> 'A result value'
703
- result.errors #=> []
653
+ result.error #=> nil
654
+ result.status #=> :success
704
655
  result.success? #=> true
705
656
  result.failure? #=> false
706
- result.halted? #=> false
707
657
  ```
708
658
 
709
- Adding errors to the `#errors` object will change the status of the result.
659
+ Creating a Result with an error stores the error and sets the status to `:failure`.
660
+
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
+ ```
670
+
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.
710
672
 
711
673
  ```ruby
712
- result.errors << "I'm sorry, something went wrong."
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
713
678
  result.success? #=> false
714
679
  result.failure? #=> true
715
680
  ```
716
681
 
717
- The status can also be overriden with the `#success!` and `#failure!` methods, which will set the status regardless of the presence or absence of errors.
682
+ Finally, the status can be overridden via the `:status` keyword.
718
683
 
719
684
  ```ruby
720
- result.success!
721
- result.errors #=> ["I'm sorry, something went wrong."]
685
+ result = Cuprum::Result.new(status: :failure)
686
+ result.error #=> nil
687
+ result.status #=> :failure
688
+ result.success? #=> false
689
+ result.failure? #=> true
690
+
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
722
695
  result.success? #=> true
723
696
  result.failure? #=> false
724
697
  ```
725
698
 
699
+ ### Errors
700
+
701
+ require 'cuprum/error'
702
+
703
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FError)
704
+
705
+ A `Cuprum::Error` encapsulates a specific failure state of a Command. Each Error has a `#message` property, which defaults to nil.
706
+
707
+ ```ruby
708
+ error = Cuprum::Error.new
709
+ error.message => # nil
710
+
711
+ error = Cuprum::Error.new(message: 'Something went wrong.')
712
+ error.message => # 'Something went wrong.'
713
+ ```
714
+
715
+ Each application should define its own failure states as errors. For example, a typical web application might define the following errors:
716
+
717
+ ```ruby
718
+ class NotFoundError < Cuprum::Error
719
+ def initialize(resource:, resource_id:)
720
+ @resource = resource
721
+ @resource_id = resource_id
722
+
723
+ super(message: "#{resource} not found with id #{resource_id}")
724
+ end
725
+
726
+ attr_reader :resource, :resource_id
727
+ end
728
+
729
+ class ValidationError < Cuprum::Error
730
+ def initialize(resource:, errors:)
731
+ @resource = resource
732
+ @errors = errors
733
+
734
+ super(message: "#{resource} was invalid")
735
+ end
736
+
737
+ attr_reader :resource, :errors
738
+ end
739
+ ```
740
+
741
+ It is optional but recommended to use a `Cuprum::Error` when returning a failed result from a command.
742
+
726
743
  ### Operations
727
744
 
728
745
  require 'cuprum'
@@ -737,8 +754,8 @@ These two features allow developers to simplify logic around calling and using t
737
754
  class CreateBookOperation < Cuprum::Operation
738
755
  def process
739
756
  # Implementation here.
740
- end # method process
741
- end # class
757
+ end
758
+ end
742
759
 
743
760
  # Defining a controller action using an operation.
744
761
  def create
@@ -750,13 +767,13 @@ def create
750
767
  @book = operation.value
751
768
 
752
769
  render :new
753
- end # if-else
754
- end # create
770
+ end
771
+ end
755
772
  ```
756
773
 
757
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.
758
775
 
759
- An operation inherits the `#call` method from Cuprum::Command (see above), and delegates the `#value`, `#errors`, `#success?`, and `#failure` methods to the most recent result. If the operation has not been called, these methods will return default values.
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.
760
777
 
761
778
  #### The Operation Mixin
762
779
 
@@ -764,13 +781,6 @@ An operation inherits the `#call` method from Cuprum::Command (see above), and d
764
781
 
765
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.
766
783
 
767
- Finally, the result can be halted using the `#halt` method, which indicates that further chained commands should not be executed.
768
-
769
- ```ruby
770
- result.halt!
771
- result.halted? #=> true
772
- ```
773
-
774
784
  ### Command Factories
775
785
 
776
786
  [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommandFactory)
@@ -898,15 +908,15 @@ class PublishedBooksCommand < Cuprum::Command
898
908
  end
899
909
 
900
910
  class BookFactory < Cuprum::CommandFactory
901
- command :published do
902
- PublishedBooksCommand.new(books_collection)
903
- end
904
-
905
911
  def initialize(books)
906
912
  @books_collection = books
907
913
  end
908
914
 
909
915
  attr_reader :books_collection
916
+
917
+ command :published do
918
+ PublishedBooksCommand.new(books_collection)
919
+ end
910
920
  end
911
921
  ```
912
922
 
@@ -1135,354 +1145,4 @@ operation.success? #=> true
1135
1145
 
1136
1146
  ## Reference
1137
1147
 
1138
- ### Cuprum::BuiltIn::IdentityCommand
1139
-
1140
- require 'cuprum/built_in/identity_command'
1141
-
1142
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
1143
-
1144
- Cuprum::BuiltIn::IdentityCommand defines the following methods:
1145
-
1146
- #### `#call`
1147
-
1148
- call(value) #=> Cuprum::Result
1149
-
1150
- Returns a result, whose `#value` is equal to the given value.
1151
-
1152
- ### Cuprum::BuiltIn::IdentityOperation
1153
-
1154
- require 'cuprum/built_in/identity_operation'
1155
-
1156
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
1157
-
1158
- Cuprum::BuiltIn::IdentityOperation defines the following methods:
1159
-
1160
- #### `#call`
1161
-
1162
- call(value) #=> Cuprum::BuiltIn::IdentityOperation
1163
-
1164
- Sets the last result to a new result, whose `#value` is equal to the given value.
1165
-
1166
- ### Cuprum::BuiltIn::NullCommand
1167
-
1168
- require 'cuprum/built_in/null_command'
1169
-
1170
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
1171
-
1172
- Cuprum::BuiltIn::NullCommand defines the following methods:
1173
-
1174
- #### `#call`
1175
-
1176
- call(*args, **keywords) { ... } #=> Cuprum::Result
1177
-
1178
- Returns a result with nil value. Any arguments or keywords are ignored.
1179
-
1180
- ### Cuprum::BuiltIn::NullOperation
1181
-
1182
- require 'cuprum/built_in/null_operation'
1183
-
1184
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
1185
-
1186
- Cuprum::BuiltIn::NullOperation defines the following methods:
1187
-
1188
- #### `#call`
1189
-
1190
- call(*args, **keywords) { ... } #=> Cuprum::BuiltIn::NullOperation
1191
-
1192
- Sets the last result to a result with nil value. Any arguments or keywords are ignored.
1193
-
1194
- ### Cuprum::Command
1195
-
1196
- require 'cuprum'
1197
-
1198
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommand)
1199
-
1200
- A Cuprum::Command defines the following methods:
1201
-
1202
- #### `#initialize`
1203
-
1204
- initialize { |*arguments, **keywords, &block| ... } #=> Cuprum::Command
1205
-
1206
- 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.
1207
-
1208
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#initialize-instance_method)
1209
-
1210
- #### `#build_errors`
1211
-
1212
- *(Private Method)*
1213
-
1214
- build_errors() #=> Array
1215
-
1216
- Generates an empty errors object. When the command is called, the result will have its `#errors` property initialized to the value returned by `#build_errors`. By default, this is an array. If you want to use a custom errors object type, override this method in a subclass.
1217
-
1218
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#build_errors-instance_method)
1219
-
1220
- #### `#call`
1221
-
1222
- call(*arguments, **keywords) { ... } #=> Cuprum::Result
1223
-
1224
- Executes the logic encoded in the constructor block, or the #process method if no block was passed to the constructor.
1225
-
1226
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#call-instance_method)
1227
-
1228
- #### `#chain`
1229
-
1230
- chain(on: nil) { |result| ... } #=> Cuprum::Command
1231
-
1232
- 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.
1233
-
1234
- chain(command, on: nil) #=> Cuprum::Command
1235
-
1236
- 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).
1237
-
1238
- The block will be passed the #result of the previous command as its parameter. If your use case depends on the status of the previous command or on any errors generated, use the block form of #chain.
1239
-
1240
- If the block returns a Cuprum::Result (or an object responding to #value and #success?), 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.
1241
-
1242
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#chain-instance_method)
1243
-
1244
- #### `#chain!`
1245
-
1246
- *(Protected Method)*
1247
-
1248
- chain!(on: nil) { |result| ... } #=> Cuprum::Command
1249
-
1250
- chain!(command, on: nil) #=> Cuprum::Command
1251
-
1252
- As `#chain`, but modifies the current command instead of creating a clone.
1253
-
1254
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#chain!-instance_method)
1255
-
1256
- #### `#else`
1257
-
1258
- else(command) #=> Cuprum::Command
1259
-
1260
- Shorthand for `command.chain(:on => :failure)`. Registers a command or block to run after the current command. The chained command will only run if the previous command was unsuccessfully run.
1261
-
1262
- 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).
1263
-
1264
- else() { |result| ... } #=> Cuprum::Command
1265
-
1266
- The block will be passed the #result of the previous command as its parameter. If your use case depends on the status of the previous command or on any errors generated, use the block form of #chain.
1267
-
1268
- If the block returns a Cuprum::Result (or an object responding to #value and #success?), 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.
1269
-
1270
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#else-instance_method)
1271
-
1272
- #### `#tap_result`
1273
-
1274
- tap_result(on: nil) { |previous_result| } #=> Cuprum::Result
1275
-
1276
- 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.
1277
-
1278
- If the `on` parameter is omitted, the block will be called if the last result is not halted. If the `on` parameter is set to `:success`, the block will be called if the last result is successful and not halted. If the `on` parameter is set to `:failure`, the block will be called if the last result is failing and not halted. Finally, if the `on` parameter is set to `:always`, the block will always be called, even if the last result is halted.
1279
-
1280
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#tap_result-instance_method)
1281
-
1282
- #### `#tap_result!`
1283
-
1284
- *(Protected Method)*
1285
-
1286
- tap_result!(on: nil) { |previous_result| } #=> Cuprum::Result
1287
-
1288
- As `#tap_result`, but modifies the current command instead of creating a clone.
1289
-
1290
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#tap_result!-instance_method)
1291
-
1292
- #### `#then`
1293
-
1294
- then(command) #=> Cuprum::Command
1295
-
1296
- Shorthand for `command.chain(:on => :success)`. Registers a command or block to run after the current command. The chained command will only run if the previous command was successfully run.
1297
-
1298
- 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).
1299
-
1300
- then() { |result| ... } #=> Cuprum::Command
1301
-
1302
- The block will be passed the #result of the previous command as its parameter. If your use case depends on the status of the previous command or on any errors generated, use the block form of #chain.
1303
-
1304
- If the block returns a Cuprum::Result (or an object responding to #value and #success?), 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.
1305
-
1306
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#then-instance_method)
1307
-
1308
- #### `#yield_result`
1309
-
1310
- yield_result(on: nil) { |previous_result| } #=> Cuprum::Result
1311
-
1312
- 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.
1313
-
1314
- If the `on` parameter is omitted, the block will be called if the last result is not halted. If the `on` parameter is set to `:success`, the block will be called if the last result is successful and not halted. If the `on` parameter is set to `:failure`, the block will be called if the last result is failing and not halted. Finally, if the `on` parameter is set to `:always`, the block will always be called, even if the last result is halted.
1315
-
1316
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#yield_result-instance_method)
1317
-
1318
- #### `#yield_result!`
1319
-
1320
- *(Protected Method)*
1321
-
1322
- yield_result!(on: nil) { |previous_result| } #=> Cuprum::Result
1323
-
1324
- As `#yield_result`, but modifies the current command instead of creating a clone.
1325
-
1326
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#yield_result!-instance_method)
1327
-
1328
- ### Cuprum::Operation
1329
-
1330
- require 'cuprum'
1331
-
1332
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
1333
-
1334
- A Cuprum::Operation inherits the methods from Cuprum::Command (see above), and defines the following additional methods:
1335
-
1336
- #### `#called?`
1337
-
1338
- called?() #=> true, false
1339
-
1340
- True if the operation has been called and there is a result available by calling `#result` or one of the delegated methods, otherwise false.
1341
-
1342
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#called%3F-instance_method)
1343
-
1344
- #### `#reset!`
1345
-
1346
- reset!()
1347
-
1348
- 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.
1349
-
1350
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#reset!-instance_method)
1351
-
1352
- #### `#result`
1353
-
1354
- result() #=> Cuprum::Result
1355
-
1356
- The most recent result, from the previous time `#call` was executed for the operation.
1357
-
1358
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#result-instance_method)
1359
-
1360
- ### Cuprum::Result
1361
-
1362
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
1363
-
1364
- A Cuprum::Result defines the following methods:
1365
-
1366
- #### `#==`
1367
-
1368
- ==(other) #=> true, false
1369
-
1370
- Performs a fuzzy comparison with the other object. At a minimum, the other object must respond to `#value` and `#success?`, and the values of `other.value` and `other.success?` must be equal to the corresponding value on the result. In addition, if the `#failure?`, `#errors`, or `#halted?` methods are defined on the other object, then the value of each defined method is compared to the value on the result. Returns true if all values match, otherwise returns false.
1371
-
1372
- #### `#empty?`
1373
-
1374
- empty?() #=> true, false
1375
-
1376
- Helper method that returns true for a new result. The method returns false if `result.value` is not nil, if `result.errors` is not empty, if the status has been manually set with `#success!` or `#failure!`, or if the result has been halted.
1377
-
1378
- #### `#errors`
1379
-
1380
- errors() #=> Array
1381
-
1382
- The errors generated by the command, or an empty array if no errors were generated.
1383
-
1384
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#errors-instance_method)
1385
-
1386
- #### `#failure!`
1387
-
1388
- failure!() #=> Cuprum::Result
1389
-
1390
- Marks the result as failing and returns the result. Calling `#failure?` will return true, even if the result has no errors.
1391
-
1392
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#failure!-instance_method)
1393
-
1394
- #### `#failure?`
1395
-
1396
- failure?() #=> true, false
1397
-
1398
- True if the command generated one or more errors or was marked as failing. Otherwise false.
1399
-
1400
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#failure%3F-instance_method)
1401
-
1402
- #### `#halt!`
1403
-
1404
- halt!() #=> Cuprum::Result
1405
-
1406
- Marks the result as halted and returns the result. Calling `#halted?` will return true.
1407
-
1408
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#halt!-instance_method)
1409
-
1410
- #### `#halted?`
1411
-
1412
- halted?() #=> true, false
1413
-
1414
- True if the result is halted, which prevents chained commands from executing.
1415
-
1416
- #### `#success!`
1417
-
1418
- success!() #=> Cuprum::Result
1419
-
1420
- Marks the result as passing and returns the result. Calling `#success?` will return true, even if the result has errors.
1421
-
1422
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#success!-instance_method)
1423
-
1424
- #### `#success?`
1425
-
1426
- success?() #=> true, false
1427
-
1428
- True if the command did not generate any errors, or the result has errors but was marked as passing. Otherwise false.
1429
-
1430
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#success%3F-instance_method)
1431
-
1432
- #### `#value`
1433
-
1434
- value() #=> Object
1435
-
1436
- 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.
1437
-
1438
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#value-instance_method)
1439
-
1440
- ### Cuprum::Utilities::InstanceSpy
1441
-
1442
- require 'cuprum/utils/instance_spy'
1443
-
1444
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FUtils%2FInstanceSpy)
1445
-
1446
- 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.
1447
-
1448
- #### `::clear_spies`
1449
-
1450
- clear_spies() #=> nil
1451
-
1452
- 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.
1453
-
1454
- after(:example) { Cuprum::Utils::InstanceSpy.clear_spies }
1455
-
1456
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#clear_spies%3F-instance_method)
1457
-
1458
- #### `::spy_on`
1459
-
1460
- spy_on(command_class) #=> InstanceSpy
1461
- spy_on(command_class) { |spy| ... } #=> nil
1462
-
1463
- 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.
1464
-
1465
- # Observing calls to instances of a command.
1466
- spy = Cuprum::Utils::InstanceSpy.spy_on(CustomCommand)
1467
-
1468
- expect(spy).to receive(:call).with(1, 2, 3, :four => '4')
1469
-
1470
- CustomCommand.new.call(1, 2, 3, :four => '4')
1471
-
1472
- # Observing calls to a chained command.
1473
- spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedCommand)
1474
-
1475
- expect(spy).to receive(:call)
1476
-
1477
- Cuprum::Command.new {}.
1478
- chain { |result| ChainedCommand.new.call(result) }.
1479
- call
1480
-
1481
- # Block syntax
1482
- Cuprum::Utils::InstanceSpy.spy_on(CustomCommand) do |spy|
1483
- expect(spy).to receive(:call)
1484
-
1485
- CustomCommand.new.call
1486
- end # spy_on
1487
-
1488
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#spy_on%3F-instance_method)
1148
+ Method and class documentation is available courtesy of [RubyDoc](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master).