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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8d393a222eda76e4b943c721df4a4d54c89966e9
4
- data.tar.gz: d0e9efa6075c9346114ecb02883b380621873510
2
+ SHA256:
3
+ metadata.gz: 8ca65deb82f58ec6a0b9706a7eb81f6edc191ecf54d22a9e3155a82370aec83e
4
+ data.tar.gz: e259228f7b6128c24a0974666165a0655c4188f1011519c8d9dea498c5d59008
5
5
  SHA512:
6
- metadata.gz: 196f5f4472c76d664f4d6b8764503a7fee5fabae50061edc877d1cc060421b05067861c96fc654a8a2586911c318b6ff9d65e515c299ffaed647f9742021dd88
7
- data.tar.gz: c1cd4f744cdfb37493f100de095ef89d1e3c080ae59f99af76ca43c69f8aaeaf14cfddc9068a824956da2e19aed4017bb70e6bb6d608969ea6e4acc5f096f875
6
+ metadata.gz: eafd3d6b85ca035cec50fdbbaacf3f3238cfa1a624de85f0663fdfcef4aeb91298f08ee11915631d3f82069222ca641279c243a6d2087a8b29cc0cf1bd01bd01
7
+ data.tar.gz: '080089f9d312a1019e934e7ed84e6df92b566c2bff83c9472671c0bed9f3738762345082d99f298af70eab33f1ec8af6809310ee2a3f90cd65544a618e812fb8'
@@ -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
- ## Operations
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
- ## Results
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
- ## Utilities
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
- ## Built In Functions
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
- ## Functions
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
- ## Operations
173
+ ### Operations
82
174
 
83
175
  Calling `#call` on an operation now returns the operation instance.
84
176
 
85
- ## Results
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
- ## Functions
187
+ ### Functions
96
188
 
97
189
  Now support chaining via the `#chain`, `#then`, and `#else` methods.
98
190
 
99
- ## Results
191
+ ### Results
100
192
 
101
193
  Can pass a value and/or an errors object to the constructor.
102
194
 
@@ -1,30 +1,44 @@
1
1
  # Development
2
2
 
3
- ## Version 1.0.0+
3
+ ## Version 1.0.0
4
4
 
5
- 'The "Look On My Works, Ye Mighty, and Despair" Update'
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
- #### Cuprum::DSL
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
- #### Hooks
52
+ #### Dependency Injection
39
53
 
40
- - :before, :around, :after hooks
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
- ### CommandFactory
49
-
50
- - builder/aggregator for command objects, esp. with shared
51
- initializers/parameters, e.g. actions for a resource
52
- - Syntax: |
53
-
54
- actions = ResourceCommandFactory.new(Book)
55
- command = actions::Build.new #=> returns a book builder command
56
- result = command.call(attributes) #=> returns a result with value => a Book
57
- # OR
58
- result = actions.build(attributes) #=> returns a result with value => a Book
59
- book = result.value
60
-
61
- ### Documentation
62
-
63
- Chaining Case Study: |
64
-
65
- CMS application - creating a new post.
66
- Directory has many Posts
67
- Post has a Content
68
- Content has many ContentVersions
69
- Post has many Tags
70
-
71
- Find Directory
72
- Create Post
73
- Create Content
74
- Create ContentVersion
75
- Tags.each { FindOrCreate Tag }
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,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, 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
 
34
- Cuprum is tested against Ruby (MRI) 2.4.
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) 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,886 +326,823 @@ 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.
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
- table_name_command =
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
- table_name_command =
315
- Cuprum::Command.new { |klass| klass.name }
316
- .chain { |str| str.pluralize }
317
- .chain { |str| str.underscore }
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
- ```ruby
328
- first_command = Cuprum::Command.new { puts 'First command!' }
329
- first_command.call #=> Outputs 'First command!' to STDOUT.
341
+ private
330
342
 
331
- second_command = first_command.chain { puts 'Second command!' }
332
- second_command.call #=> Outputs 'First command!' then 'Second command!'.
343
+ def process(command)
344
+ @count.times { command.call }
345
+ end
346
+ end
333
347
 
334
- # The original command is unchanged.
335
- first_command.call #=> Outputs 'First command!' to STDOUT.
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
- 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.
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
- 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.
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
- 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
- ```
360
+ class DeliverEbook < Cuprum::Command; end
362
361
 
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.
362
+ class ShipDomestic < Cuprum::Command; end
364
363
 
365
- ```ruby
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
- object
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
- object
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
- 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
378
+ end
404
379
  ```
405
380
 
406
- #### Conditional Chaining
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
- 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`.
383
+ #### Commands As Returned Values
409
384
 
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))
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
- 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`.
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
- 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
-
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
- find_command =
420
- Cuprum::Command.new do |attributes|
421
- book = Book.where(:id => attributes[:id]).first
392
+ class ShippingMethod < Cuprum::Command
393
+ private
422
394
 
423
- result.errors << 'Book not found' unless book
395
+ def process(book:, user:)
396
+ return DeliverEbook.new(user.email) if book.ebook?
424
397
 
425
- book
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 book.save
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
- book
438
- end
402
+ err = Cuprum::Error.new(message: 'user does not have a valid address')
439
403
 
440
- find_or_create_command = find_command.chain(create_command, :on => :failure)
441
-
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"]
404
+ failure(err)
405
+ end
406
+ end
476
407
  ```
477
408
 
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)`.
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
- #### Halting A Command Chain
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
- 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.
413
+ ### Command Steps
483
414
 
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
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
- #### Tap Result
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
- command =
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
- # Prints 'The result value was "Example value"' to STDOUT.
529
- result = chained_command.call
530
- result.class #=> Cuprum::Result
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
- Like `#chain`, `#tap_result` can be given an `:on => value` keyword.
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
- ```ruby
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
- result.errors << "Unable to find book with id #{book_id}" unless book
544
-
545
- book
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
- 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
435
+ success(dividend / divisor)
436
+ end
556
437
 
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)
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
- #### Yield Result
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
- 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.
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
- 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.
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
- chained_command =
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
- result
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
- # 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 #=> []
616
- ```
617
-
618
- Like `#chain`, `#yield_result` can be given an `:on => value` keyword.
457
+ class FindBookByTitle < Cuprum::Command; end
619
458
 
620
- ```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?
459
+ class ReserveBookByTitle < Cuprum::Command
460
+ private
628
461
 
629
- i
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
- .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
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
- Under the hood, both `#chain` and `#tap_result` are implemented on top of `#yield_result`.
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
- value = 'A result value'.freeze
663
- result = Cuprum::Result.new(value)
664
-
665
- result.value #=> 'A result value'
666
- result.errors #=> []
667
- result.success? #=> true
668
- result.failure? #=> false
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
- Adding errors to the `#errors` object will change the status of the result.
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.errors << "I'm sorry, something went wrong."
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.failure? #=> true
502
+ result.error #=> 'title not found'
678
503
  ```
679
504
 
680
- 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.
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.success!
684
- result.errors #=> ["I'm sorry, something went wrong."]
685
- result.success? #=> true
686
- result.failure? #=> false
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
- ### Operations
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
- class CreateBookOperation < Cuprum::Operation
701
- def process
702
- # Implementation here.
703
- end # method process
704
- end # class
705
-
706
- # Defining a controller action using an operation.
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
- 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.
529
+ #### Using Methods As Steps
721
530
 
722
- 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.
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
- #### The Operation Mixin
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
- [Module Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation%2FMixin)
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
- result.halt!
734
- result.halted? #=> true
735
- ```
736
-
737
- ### Built In Commands
538
+ class ReserveBookByTitle < Cuprum::Result
539
+ private
738
540
 
739
- Cuprum includes a small number of predefined commands and their equivalent operations.
541
+ def check_user_status(user)
542
+ CheckUserStatus.new(user)
543
+ end
740
544
 
741
- #### IdentityCommand
545
+ def create_book_reservation(book:, user:)
546
+ CreateBookReservation.new(book: book, user: user)
547
+ end
742
548
 
743
- require 'cuprum/built_in/identity_command'
549
+ def find_book_by_title(title)
550
+ FindBookByTitle.new.call(title)
551
+ end
744
552
 
745
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
553
+ def process(title:, user:)
554
+ step :check_user_status, user
746
555
 
747
- A pregenerated command that returns the value or result with which it was called.
556
+ book = step :find_book_by_title, title
748
557
 
749
- ```ruby
750
- command = Cuprum::BuiltIn::IdentityCommand.new
751
- result = command.call('expected value')
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
- #### IdentityOperation
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
- require 'cuprum/built_in/identity_operation'
565
+ #### Using Steps Outside Of Commands
759
566
 
760
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
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
- A pregenerated operation that sets its result to the value or result with which it was called.
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
- operation = Cuprum::BuiltIn::IdentityOperation.new.call('expected value')
766
- operation.value #=> 'expected value'
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
- A pregenerated command that does nothing when called. Accepts any arguments.
575
+ obj = step { find_something }
777
576
 
778
- ```ruby
779
- command = Cuprum::BuiltIn::NullCommand.new
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
- #### NullOperation
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
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
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
- A pregenerated operation that does nothing when called. Accepts any arguments.
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
- operation = Cuprum::BuiltIn::NullOperation.new.call
795
- operation.value #=> nil
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
- ### Cuprum::BuiltIn::NullCommand
593
+ def create
594
+ attributes = params[:books]
595
+ result = steps do
596
+ @book = step :build_book, attributes
830
597
 
831
- require 'cuprum/built_in/null_command'
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
- #### `#call`
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
- ### Cuprum::BuiltIn::NullOperation
603
+ result.success ? redirect_to(@book) : render(:edit)
604
+ end
844
605
 
845
- require 'cuprum/built_in/null_operation'
606
+ private
846
607
 
847
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
608
+ def build_book(attributes)
609
+ success(Book.new(attributes))
610
+ rescue InvalidAttributes
611
+ failure('attributes are invalid')
612
+ end
848
613
 
849
- Cuprum::BuiltIn::NullOperation defines the following methods:
614
+ def persist_book(book)
615
+ book.save ? success(book) : failure('unable to persist book')
616
+ end
850
617
 
851
- #### `#call`
618
+ def run_validations(book)
619
+ book.valid? ? success : failure('book is invalid')
620
+ end
621
+ end
622
+ ```
852
623
 
853
- call(*args, **keywords) { ... } #=> Cuprum::BuiltIn::NullOperation
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
- Sets the last result to a result with nil value. Any arguments or keywords are ignored.
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
- ### Cuprum::Command
628
+ ### Results
858
629
 
859
630
  require 'cuprum'
860
631
 
861
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommand)
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
- initialize { |*arguments, **keywords, &block| ... } #=> Cuprum::Command
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
- 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.
636
+ ```ruby
637
+ result = Cuprum::Result.new
870
638
 
871
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#initialize-instance_method)
639
+ result.value #=> nil
640
+ result.error #=> nil
641
+ result.status #=> :success
642
+ result.success? #=> true
643
+ result.failure? #=> true
644
+ ```
872
645
 
873
- #### `#build_errors`
646
+ Creating a result with a value stores the value.
874
647
 
875
- *(Private Method)*
648
+ ```ruby
649
+ value = 'A result value'.freeze
650
+ result = Cuprum::Result.new(value: value)
876
651
 
877
- build_errors() #=> Array
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
- 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.
659
+ Creating a Result with an error stores the error and sets the status to `:failure`.
880
660
 
881
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#build_errors-instance_method)
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
- #### #call
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
- call(*arguments, **keywords) { ... } #=> Cuprum::Result
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
- Executes the logic encoded in the constructor block, or the #process method if no block was passed to the constructor.
682
+ Finally, the status can be overridden via the `:status` keyword.
888
683
 
889
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#call-instance_method)
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
- #### #chain
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
- chain(on: nil) { |result| ... } #=> Cuprum::Command
699
+ ### Errors
894
700
 
895
- 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.
701
+ require 'cuprum/error'
896
702
 
897
- chain(command, on: nil) #=> Cuprum::Command
703
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FError)
898
704
 
899
- 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).
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
- 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.
707
+ ```ruby
708
+ error = Cuprum::Error.new
709
+ error.message => # nil
902
710
 
903
- 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.
711
+ error = Cuprum::Error.new(message: 'Something went wrong.')
712
+ error.message => # 'Something went wrong.'
713
+ ```
904
714
 
905
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#chain-instance_method)
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
- #### `#else`
717
+ ```ruby
718
+ class NotFoundError < Cuprum::Error
719
+ def initialize(resource:, resource_id:)
720
+ @resource = resource
721
+ @resource_id = resource_id
908
722
 
909
- else(command) #=> Cuprum::Command
723
+ super(message: "#{resource} not found with id #{resource_id}")
724
+ end
910
725
 
911
- 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.
726
+ attr_reader :resource, :resource_id
727
+ end
912
728
 
913
- 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).
729
+ class ValidationError < Cuprum::Error
730
+ def initialize(resource:, errors:)
731
+ @resource = resource
732
+ @errors = errors
914
733
 
915
- else() { |result| ... } #=> Cuprum::Command
734
+ super(message: "#{resource} was invalid")
735
+ end
916
736
 
917
- 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.
737
+ attr_reader :resource, :errors
738
+ end
739
+ ```
918
740
 
919
- 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.
741
+ It is optional but recommended to use a `Cuprum::Error` when returning a failed result from a command.
920
742
 
921
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#else-instance_method)
743
+ ### Operations
922
744
 
923
- #### `#errors`
745
+ require 'cuprum'
924
746
 
925
- *(Private Method)*
747
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
926
748
 
927
- errors() #=> Array
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
- Only available while the Command is being called. Provides access to the errors object of the generated Cuprum::Result, which is by default an instance of Array.
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
- Inside of the Command block or the `#process` method, you can add errors to the result.
753
+ ```ruby
754
+ class CreateBookOperation < Cuprum::Operation
755
+ def process
756
+ # Implementation here.
757
+ end
758
+ end
932
759
 
933
- command =
934
- Cuprum::Command.new do
935
- errors << "I'm sorry, something went wrong."
760
+ # Defining a controller action using an operation.
761
+ def create
762
+ operation = CreateBookOperation.new.call(book_params)
936
763
 
937
- nil
938
- end # command
764
+ if operation.success?
765
+ redirect_to(operation.value)
766
+ else
767
+ @book = operation.value
939
768
 
940
- result = command.call
941
- result.failure?
942
- #=> true
943
- result.errors
944
- #=> ["I'm sorry, something went wrong."]
769
+ render :new
770
+ end
771
+ end
772
+ ```
945
773
 
946
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#errors-instance_method)
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
- #### `#failure!`
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
- failure!() #=> NilClass
778
+ #### The Operation Mixin
951
779
 
952
- *(Private Method)*
780
+ [Module Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation%2FMixin)
953
781
 
954
- Only available while the Command is being called. If called, marks the result object as failing, even if the result does not have errors.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#failure!-instance_method)
784
+ ### Command Factories
957
785
 
958
- #### `#halt!`
786
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FCommandFactory)
959
787
 
960
- halt!() #=> NilClass
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
- *(Private Method)*
790
+ For example, consider a basic entity command:
963
791
 
964
- Only available while the Command is being called. If called, halts the command chain (see Chaining Commands, below). Subsequent chained commands will not be called unless they were chained with the `:on => :always` option.
792
+ ```ruby
793
+ class Book
794
+ def initialize(attributes = {})
795
+ @title = attributes[:title]
796
+ @author = attributes[:author]
797
+ end
965
798
 
966
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#halt!-instance_method)
799
+ attr_accessor :author, :publisher, :title
800
+ end
967
801
 
968
- #### `#success!`
802
+ class BuildBookCommand < Cuprum::Command
803
+ private
969
804
 
970
- success!() #=> NilClass
805
+ def process(attributes = {})
806
+ Book.new(attributes)
807
+ end
808
+ end
971
809
 
972
- *(Private Method)*
810
+ class BookFactory < Cuprum::CommandFactory
811
+ command :build, BuildBookCommand
812
+ end
813
+ ```
973
814
 
974
- Only available while the Command is being called. If called, marks the result object as passing, even if the result has errors.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#success!-instance_method)
817
+ First, the command class can be accessed directly as a constant on the factory instance.
977
818
 
978
- #### `#tap_result`
819
+ ```ruby
820
+ factory = BookFactory.new
821
+ factory::Build #=> BuildBookCommand
822
+ ```
979
823
 
980
- tap_result(on: nil) { |previous_result| } #=> Cuprum::Result
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
- 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.
826
+ ```ruby
827
+ factory = BookFactory.new
983
828
 
984
- 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.
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
- #### `#then`
834
+ book.title #=> 'A Wizard of Earthsea'
835
+ book.author #=> 'Ursula K. Le Guin'
836
+ book.publisher #=> nil
837
+ ```
987
838
 
988
- then(command) #=> Cuprum::Command
839
+ #### The ::command Method And A Command Class
989
840
 
990
- 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.
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
- 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).
843
+ ```ruby
844
+ class BookFactory < Cuprum::CommandFactory
845
+ command :build, BuildBookCommand
846
+ end
847
+ ```
993
848
 
994
- then() { |result| ... } #=> Cuprum::Command
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 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.
851
+ #### The ::command Method And A Block
997
852
 
998
- 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.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#then-instance_method)
855
+ ```ruby
856
+ class PublishBookCommand < Cuprum::Command
857
+ def initialize(publisher:)
858
+ @publisher = publisher
859
+ end
1001
860
 
1002
- #### `#yield_result`
861
+ attr_reader :publisher
1003
862
 
1004
- yield_result(on: nil) { |previous_result| } #=> Cuprum::Result
863
+ private
1005
864
 
1006
- 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.
865
+ def process(book)
866
+ book.publisher = publisher
1007
867
 
1008
- 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.
868
+ book
869
+ end
870
+ end
1009
871
 
1010
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Command#yield_result-instance_method)
872
+ class BookFactory < Cuprum::CommandFactory
873
+ command :publish do |publisher|
874
+ PublishBookCommand.new(publisher: publisher)
875
+ end
876
+ end
877
+ ```
1011
878
 
1012
- ### Cuprum::Operation
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
- require 'cuprum'
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
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FOperation)
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
- A Cuprum::Operation inherits the methods from Cuprum::Command (see above), and defines the following additional methods:
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
- #### `#called?`
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
- called?() #=> true, false
895
+ ```ruby
896
+ class PublishedBooksCommand < Cuprum::Command
897
+ def initialize(collection = [])
898
+ @collection = collection
899
+ end
1023
900
 
1024
- True if the operation has been called and there is a result available by calling `#result` or one of the delegated methods, otherwise false.
901
+ attr_reader :collection
1025
902
 
1026
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#called%3F-instance_method)
903
+ private
1027
904
 
1028
- #### `#reset!`
905
+ def process
906
+ books.reject { |book| book.publisher.nil? }
907
+ end
908
+ end
1029
909
 
1030
- reset!()
910
+ class BookFactory < Cuprum::CommandFactory
911
+ def initialize(books)
912
+ @books_collection = books
913
+ end
1031
914
 
1032
- 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.
915
+ attr_reader :books_collection
1033
916
 
1034
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#reset!-instance_method)
917
+ command :published do
918
+ PublishedBooksCommand.new(books_collection)
919
+ end
920
+ end
921
+ ```
1035
922
 
1036
- #### `#result`
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
- result() #=> Cuprum::Result
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
- The most recent result, from the previous time `#call` was executed for the operation.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Operation#result-instance_method)
934
+ ary.count #=> 2
935
+ ary.any? { |book| book.publisher == 'Baen' } #=> true
936
+ ary.any? { |book| book.publisher.nil? } #=> false
937
+ ```
1043
938
 
1044
- ### Cuprum::Result
939
+ Simple commands can be defined directly in the block, rather than referencing an existing command class:
1045
940
 
1046
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FResult)
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
- A Cuprum::Result defines the following methods:
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
- ==(other) #=> true, false
957
+ ary.count #=> 1
958
+ ```
1053
959
 
1054
- 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.
960
+ #### The ::command_class Method
1055
961
 
1056
- #### `#empty?`
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
- empty?() #=> true, false
964
+ ```ruby
965
+ class SelectByAuthorCommand < Cuprum::Command
966
+ def initialize(author)
967
+ @author = author
968
+ end
1059
969
 
1060
- 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.
970
+ attr_reader :author
1061
971
 
1062
- #### `#errors`
972
+ private
1063
973
 
1064
- errors() #=> Array
974
+ def process(books)
975
+ books.select { |book| book.author == author }
976
+ end
977
+ end
1065
978
 
1066
- The errors generated by the command, or an empty array if no errors were generated.
979
+ class BooksFactory < Cuprum::CommandFactory
980
+ command_class :select_by_author do
981
+ SelectByAuthorCommand
982
+ end
983
+ end
984
+ ```
1067
985
 
1068
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#errors-instance_method)
986
+ The command class can be accessed directly as a constant on the factory instance:
1069
987
 
1070
- #### `#failure!`
988
+ ```ruby
989
+ factory = BookFactory.new
990
+ factory::SelectByAuthor #=> SelectByAuthorCommand
991
+ ```
1071
992
 
1072
- failure!() #=> Cuprum::Result
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
- Marks the result as failing and returns the result. Calling `#failure?` will return true, even if the result has no errors.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#failure!-instance_method)
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
- #### `#failure?`
1007
+ result = command.call(books) #=> an instance of Cuprum::Result
1008
+ ary = result.value #=> an array with the selected books
1079
1009
 
1080
- failure?() #=> true, false
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
- True if the command generated one or more errors or was marked as failing. Otherwise false.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#failure%3F-instance_method)
1018
+ ```ruby
1019
+ class SaveBookCommand < Cuprum::Command
1020
+ def initialize(collection = [])
1021
+ @collection = collection
1022
+ end
1085
1023
 
1086
- #### `#halt!`
1024
+ attr_reader :collection
1087
1025
 
1088
- halt!() #=> Cuprum::Result
1026
+ private
1089
1027
 
1090
- Marks the result as halted and returns the result. Calling `#halted?` will return true.
1028
+ def process(book)
1029
+ books << book
1091
1030
 
1092
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#halt!-instance_method)
1031
+ book
1032
+ end
1033
+ end
1093
1034
 
1094
- #### `#halted?`
1035
+ class BookFactory < Cuprum::CommandFactory
1036
+ command :save do
1037
+ collection = self.books_collection
1095
1038
 
1096
- halted?() #=> true, false
1039
+ Class.new(SaveBookCommand) do
1040
+ define_method(:initialize) do
1041
+ @books = collection
1042
+ end
1043
+ end
1044
+ end
1097
1045
 
1098
- True if the result is halted, which prevents chained commands from executing.
1046
+ def initialize(books)
1047
+ @books_collection = books
1048
+ end
1099
1049
 
1100
- #### `#success!`
1050
+ attr_reader :books_collection
1051
+ end
1052
+ ```
1101
1053
 
1102
- success!() #=> Cuprum::Result
1054
+ The custom command subclass can be accessed directly as a constant on the factory instance:
1103
1055
 
1104
- Marks the result as passing and returns the result. Calling `#success?` will return true, even if the result has errors.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#success!-instance_method)
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
- #### `#success?`
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
- success?() #=> true, false
1068
+ The custom command subclass can be accessed directly as a constant on the factory instance:
1111
1069
 
1112
- True if the command did not generate any errors, or the result has errors but was marked as passing. Otherwise false.
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
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#success%3F-instance_method)
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
- #### `#value`
1080
+ books.count #=> 4
1081
+ books.include?(book) #=> true
1082
+ ```
1117
1083
 
1118
- value() #=> Object
1084
+ ### Built In Commands
1119
1085
 
1120
- 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.
1086
+ Cuprum includes a small number of predefined commands and their equivalent operations.
1121
1087
 
1122
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Result#value-instance_method)
1088
+ #### IdentityCommand
1123
1089
 
1124
- ### Cuprum::Utilities::InstanceSpy
1090
+ require 'cuprum/built_in/identity_command'
1125
1091
 
1126
- require 'cuprum/utils/instance_spy'
1092
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityCommand)
1127
1093
 
1128
- [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FUtils%2FInstanceSpy)
1094
+ A pregenerated command that returns the value or result with which it was called.
1129
1095
 
1130
- 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.
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
- #### `::clear_spies`
1103
+ #### IdentityOperation
1133
1104
 
1134
- clear_spies() #=> nil
1105
+ require 'cuprum/built_in/identity_operation'
1135
1106
 
1136
- 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.
1107
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FIdentityOperation)
1137
1108
 
1138
- after(:example) { Cuprum::Utils::InstanceSpy.clear_spies }
1109
+ A pregenerated operation that sets its result to the value or result with which it was called.
1139
1110
 
1140
- [Method Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum/Utils/InstanceSpy#clear_spies%3F-instance_method)
1111
+ ```ruby
1112
+ operation = Cuprum::BuiltIn::IdentityOperation.new.call('expected value')
1113
+ operation.value #=> 'expected value'
1114
+ operation.success? #=> true
1115
+ ```
1141
1116
 
1142
- #### `::spy_on`
1117
+ #### NullCommand
1143
1118
 
1144
- spy_on(command_class) #=> InstanceSpy
1145
- spy_on(command_class) { |spy| ... } #=> nil
1119
+ require 'cuprum/built_in/null_command'
1146
1120
 
1147
- 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.
1121
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullCommand)
1148
1122
 
1149
- # Observing calls to instances of a command.
1150
- spy = Cuprum::Utils::InstanceSpy.spy_on(CustomCommand)
1123
+ A pregenerated command that does nothing when called. Accepts any arguments.
1151
1124
 
1152
- expect(spy).to receive(:call).with(1, 2, 3, :four => '4')
1125
+ ```ruby
1126
+ command = Cuprum::BuiltIn::NullCommand.new
1127
+ result = command.call
1128
+ result.value #=> nil
1129
+ result.success? #=> true
1130
+ ```
1153
1131
 
1154
- CustomCommand.new.call(1, 2, 3, :four => '4')
1132
+ #### NullOperation
1155
1133
 
1156
- # Observing calls to a chained command.
1157
- spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedCommand)
1134
+ require 'cuprum/built_in/null_operation'
1158
1135
 
1159
- expect(spy).to receive(:call)
1136
+ [Class Documentation](http://www.rubydoc.info/github/sleepingkingstudios/cuprum/master/Cuprum%2FBuiltIn%2FNullOperation)
1160
1137
 
1161
- Cuprum::Command.new {}.
1162
- chain { |result| ChainedCommand.new.call(result) }.
1163
- call
1138
+ A pregenerated operation that does nothing when called. Accepts any arguments.
1164
1139
 
1165
- # Block syntax
1166
- Cuprum::Utils::InstanceSpy.spy_on(CustomCommand) do |spy|
1167
- expect(spy).to receive(:call)
1140
+ ```ruby
1141
+ operation = Cuprum::BuiltIn::NullOperation.new.call
1142
+ operation.value #=> nil
1143
+ operation.success? #=> true
1144
+ ```
1168
1145
 
1169
- CustomCommand.new.call
1170
- end # spy_on
1146
+ ## Reference
1171
1147
 
1172
- [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).