cuprum 0.7.0 → 0.10.0.rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +101 -9
- data/DEVELOPMENT.md +67 -45
- data/README.md +669 -693
- data/lib/cuprum.rb +1 -25
- data/lib/cuprum/built_in/identity_command.rb +4 -4
- data/lib/cuprum/chaining.rb +235 -144
- data/lib/cuprum/command.rb +24 -18
- data/lib/cuprum/command_factory.rb +300 -0
- data/lib/cuprum/currying.rb +78 -0
- data/lib/cuprum/currying/curried_command.rb +109 -0
- data/lib/cuprum/error.rb +37 -0
- data/lib/cuprum/errors.rb +6 -0
- data/lib/cuprum/errors/command_not_implemented.rb +35 -0
- data/lib/cuprum/errors/operation_not_called.rb +35 -0
- data/lib/cuprum/operation.rb +37 -28
- data/lib/cuprum/processing.rb +45 -96
- data/lib/cuprum/result.rb +52 -115
- data/lib/cuprum/result_helpers.rb +14 -105
- data/lib/cuprum/rspec.rb +8 -0
- data/lib/cuprum/rspec/be_a_result.rb +19 -0
- data/lib/cuprum/rspec/be_a_result_matcher.rb +286 -0
- data/lib/cuprum/steps.rb +275 -0
- data/lib/cuprum/utils/instance_spy.rb +9 -2
- data/lib/cuprum/version.rb +3 -3
- metadata +30 -8
- data/lib/cuprum/not_implemented_error.rb +0 -14
- data/lib/cuprum/utils/result_not_empty_warning.rb +0 -72
data/lib/cuprum/steps.rb
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/result_helpers'
|
4
|
+
|
5
|
+
module Cuprum
|
6
|
+
# The Steps supports step by step processes that halt on a failed step.
|
7
|
+
#
|
8
|
+
# After including Cuprum::Steps, use the #steps instance method to wrap a
|
9
|
+
# series of instructions. Each instruction is then defined using the #step
|
10
|
+
# method. Steps can be defined either as a block or as a method invocation.
|
11
|
+
#
|
12
|
+
# When the steps block is evaluated, each step is called in sequence. If the
|
13
|
+
# step resolves to a passing result, the result value is returned and
|
14
|
+
# execution continues to the next step. If all of the steps pass, then the
|
15
|
+
# result of the final step is returned from the #steps block.
|
16
|
+
#
|
17
|
+
# Conversely, if any step resolves to a failing result, that failing result is
|
18
|
+
# immediately returned from the #steps block. No further steps will be called.
|
19
|
+
#
|
20
|
+
# For example, consider updating a database record using a primary key and an
|
21
|
+
# attributes hash. Broken down into its basics, this requires the following
|
22
|
+
# instructions:
|
23
|
+
#
|
24
|
+
# - Using the primary key, find the existing record in the database.
|
25
|
+
# - Update the record object with the given attributes.
|
26
|
+
# - Save the updated record back to the database.
|
27
|
+
#
|
28
|
+
# Note that each of these steps can fail for different reasons. For example,
|
29
|
+
# if a record with the given primary key does not exist in the database, then
|
30
|
+
# the first instruction will fail, and the follow up steps should not be
|
31
|
+
# executed. Further, whatever context is executing these steps probably wants
|
32
|
+
# to know which step failed, and why.
|
33
|
+
#
|
34
|
+
# @example Defining Methods As Steps
|
35
|
+
# def assign_attributes(record, attributes); end
|
36
|
+
#
|
37
|
+
# def find_record(primary_key); end
|
38
|
+
#
|
39
|
+
# def save_record(record); end
|
40
|
+
#
|
41
|
+
# def update_record(primary_key, attributes)
|
42
|
+
# steps do
|
43
|
+
# record = step :find_record, primary_key
|
44
|
+
# record = step :assign_attributes, record, attributes
|
45
|
+
# step :save_record, record
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# @example Defining Blocks As Steps
|
50
|
+
# class AssignAttributes < Cuprum::Command; end
|
51
|
+
#
|
52
|
+
# class FindRecord < Cuprum::Command; end
|
53
|
+
#
|
54
|
+
# class SaveRecord < Cuprum::Command; end
|
55
|
+
#
|
56
|
+
# def update_record(primary_key, attributes)
|
57
|
+
# steps do
|
58
|
+
# record = step { FindRecord.new.call(primary_key) }
|
59
|
+
# record = step { AssignAttributes.new.call(record, attributes) }
|
60
|
+
# step { SaveRecord.new.call(record) }
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
module Steps
|
64
|
+
include Cuprum::ResultHelpers
|
65
|
+
|
66
|
+
class << self
|
67
|
+
# @!visibility private
|
68
|
+
def execute_method(receiver, method_name, *args, **kwargs, &block)
|
69
|
+
if block_given? && kwargs.empty?
|
70
|
+
receiver.send(method_name, *args, &block)
|
71
|
+
elsif block_given?
|
72
|
+
receiver.send(method_name, *args, **kwargs, &block)
|
73
|
+
elsif kwargs.empty?
|
74
|
+
receiver.send(method_name, *args)
|
75
|
+
else
|
76
|
+
receiver.send(method_name, *args, **kwargs)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @!visibility private
|
81
|
+
def extract_result_value(result)
|
82
|
+
return result unless result.respond_to?(:to_cuprum_result)
|
83
|
+
|
84
|
+
result = result.to_cuprum_result
|
85
|
+
|
86
|
+
return result.value if result.success?
|
87
|
+
|
88
|
+
throw :cuprum_failed_step, result
|
89
|
+
end
|
90
|
+
|
91
|
+
# rubocop:disable Metrics/MethodLength
|
92
|
+
# @!visibility private
|
93
|
+
def validate_method_name(method_name)
|
94
|
+
if method_name.nil?
|
95
|
+
raise ArgumentError,
|
96
|
+
'expected a block or a method name',
|
97
|
+
caller(1..-1)
|
98
|
+
end
|
99
|
+
|
100
|
+
unless method_name.is_a?(String) || method_name.is_a?(Symbol)
|
101
|
+
raise ArgumentError,
|
102
|
+
'expected method name to be a String or Symbol',
|
103
|
+
caller(1..-1)
|
104
|
+
end
|
105
|
+
|
106
|
+
return unless method_name.empty?
|
107
|
+
|
108
|
+
raise ArgumentError, "method name can't be blank", caller(1..-1)
|
109
|
+
end
|
110
|
+
# rubocop:enable Metrics/MethodLength
|
111
|
+
end
|
112
|
+
|
113
|
+
# @overload step()
|
114
|
+
# Executes the block and returns the value, or halts on a failure.
|
115
|
+
#
|
116
|
+
# @yield Called with no parameters.
|
117
|
+
#
|
118
|
+
# @return [Object] the #value of the result, or the returned object.
|
119
|
+
#
|
120
|
+
# The #step method is used to evaluate a sequence of processes, and to
|
121
|
+
# fail fast and halt processing if any of the steps returns a failing
|
122
|
+
# result. Each invocation of #step should be wrapped in a #steps block,
|
123
|
+
# or used inside the #process method of a Command.
|
124
|
+
#
|
125
|
+
# If the object returned by the block is a Cuprum result or compatible
|
126
|
+
# object (such as a called operation), the value is converted to a Cuprum
|
127
|
+
# result via the #to_cuprum_result method. Otherwise, the object is
|
128
|
+
# returned directly from #step.
|
129
|
+
#
|
130
|
+
# If the returned object is a passing result, the #value of the result is
|
131
|
+
# returned by #step.
|
132
|
+
#
|
133
|
+
# If the returned object is a failing result, then #step will throw
|
134
|
+
# :cuprum_failed_result and the failing result. This is caught by the
|
135
|
+
# #steps block, and halts execution of any subsequent steps.
|
136
|
+
#
|
137
|
+
# @example Calling a Step
|
138
|
+
# # The #do_something method returns the string 'some value'.
|
139
|
+
# step { do_something() } #=> 'some value'
|
140
|
+
#
|
141
|
+
# value = step { do_something() }
|
142
|
+
# value #=> 'some value'
|
143
|
+
#
|
144
|
+
# @example Calling a Step with a Passing Result
|
145
|
+
# # The #do_something_else method returns a Cuprum result with a value
|
146
|
+
# # of 'another value'.
|
147
|
+
# step { do_something_else() } #=> 'another value'
|
148
|
+
#
|
149
|
+
# # The result is passing, so the value is extracted and returned.
|
150
|
+
# value = step { do_something_else() }
|
151
|
+
# value #=> 'another value'
|
152
|
+
#
|
153
|
+
# @example Calling a Step with a Failing Result
|
154
|
+
# # The #do_something_wrong method returns a failing Cuprum result.
|
155
|
+
# step { do_something_wrong() } # Throws the :cuprum_failed_step symbol.
|
156
|
+
#
|
157
|
+
# @overload step(method_name, *arguments, **keywords)
|
158
|
+
# Calls the method and returns the value, or halts on a failure.
|
159
|
+
#
|
160
|
+
# @param method_name [String, Symbol] The name of the method to call. Must
|
161
|
+
# be the name of a method on the current object.
|
162
|
+
# @param arguments [Array] Positional arguments to pass to the method.
|
163
|
+
# @param keywords [Hash] Keyword arguments to pass to the method.
|
164
|
+
#
|
165
|
+
# @yield A block to pass to the method.
|
166
|
+
#
|
167
|
+
# @return [Object] the #value of the result, or the returned object.
|
168
|
+
#
|
169
|
+
# The #step method is used to evaluate a sequence of processes, and to
|
170
|
+
# fail fast and halt processing if any of the steps returns a failing
|
171
|
+
# result. Each invocation of #step should be wrapped in a #steps block,
|
172
|
+
# or used inside the #process method of a Command.
|
173
|
+
#
|
174
|
+
# If the object returned by the block is a Cuprum result or compatible
|
175
|
+
# object (such as a called operation), the value is converted to a Cuprum
|
176
|
+
# result via the #to_cuprum_result method. Otherwise, the object is
|
177
|
+
# returned directly from #step.
|
178
|
+
#
|
179
|
+
# If the returned object is a passing result, the #value of the result is
|
180
|
+
# returned by #step.
|
181
|
+
#
|
182
|
+
# If the returned object is a failing result, then #step will throw
|
183
|
+
# :cuprum_failed_result and the failing result. This is caught by the
|
184
|
+
# #steps block, and halts execution of any subsequent steps.
|
185
|
+
#
|
186
|
+
# @example Calling a Step
|
187
|
+
# # The #zero method returns the integer 0.
|
188
|
+
# step :zero #=> 0
|
189
|
+
#
|
190
|
+
# value = step :zero
|
191
|
+
# value #=> 0
|
192
|
+
#
|
193
|
+
# @example Calling a Step with a Passing Result
|
194
|
+
# # The #add method adds the numbers and returns a Cuprum result with a
|
195
|
+
# # value equal to the sum.
|
196
|
+
# step :add, 2, 2
|
197
|
+
# #=> 4
|
198
|
+
#
|
199
|
+
# # The result is passing, so the value is extracted and returned.
|
200
|
+
# value = step :add, 2, 2
|
201
|
+
# value #=> 4
|
202
|
+
#
|
203
|
+
# @example Calling a Step with a Failing Result
|
204
|
+
# # The #divide method returns a failing Cuprum result when the second
|
205
|
+
# # argument is zero.
|
206
|
+
# step :divide, 1, 0
|
207
|
+
# # Throws the :cuprum_failed_step symbol, which should be caught by the
|
208
|
+
# # enclosing #steps block.
|
209
|
+
def step(method_name = nil, *args, **kwargs, &block)
|
210
|
+
result =
|
211
|
+
if !block_given? || method_name || !args.empty? || !kwargs.empty?
|
212
|
+
Cuprum::Steps.validate_method_name(method_name)
|
213
|
+
|
214
|
+
Cuprum::Steps
|
215
|
+
.execute_method(self, method_name, *args, **kwargs, &block)
|
216
|
+
else
|
217
|
+
block.call
|
218
|
+
end
|
219
|
+
|
220
|
+
Cuprum::Steps.extract_result_value(result)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Returns the first failing #step result, or the final result if none fail.
|
224
|
+
#
|
225
|
+
# The #steps method is used to wrap a series of #step calls. Each step is
|
226
|
+
# executed in sequence. If any of the steps returns a failing result, that
|
227
|
+
# result is immediately returned from #steps. Otherwise, #steps wraps the
|
228
|
+
# value returned by a block in a Cuprum result.
|
229
|
+
#
|
230
|
+
# @yield Called with no parameters.
|
231
|
+
#
|
232
|
+
# @yieldreturn A Cuprum result, or an object to be wrapped in a result.
|
233
|
+
#
|
234
|
+
# @return [Cuprum::Result] the result or object returned by the block,
|
235
|
+
# wrapped in a Cuprum result.
|
236
|
+
#
|
237
|
+
# @example With A Passing Step
|
238
|
+
# result = steps do
|
239
|
+
# step { success('some value') }
|
240
|
+
# end
|
241
|
+
# result.class #=> Cuprum::Result
|
242
|
+
# result.success? #=> true
|
243
|
+
# result.value #=> 'some value'
|
244
|
+
#
|
245
|
+
# @example With A Failing Step
|
246
|
+
# result = steps do
|
247
|
+
# step { failure('something went wrong') }
|
248
|
+
# end
|
249
|
+
# result.class #=> Cuprum::Result
|
250
|
+
# result.success? #=> false
|
251
|
+
# result.error #=> 'something went wrong'
|
252
|
+
#
|
253
|
+
# @example With Multiple Steps
|
254
|
+
# result = steps do
|
255
|
+
# # This step is passing, so execution continues on to the next step.
|
256
|
+
# step { success('first step') }
|
257
|
+
#
|
258
|
+
# # This step is failing, so execution halts and returns this result.
|
259
|
+
# step { failure('second step') }
|
260
|
+
#
|
261
|
+
# # This step will never be called.
|
262
|
+
# step { success('third step') }
|
263
|
+
# end
|
264
|
+
# result.class #=> Cuprum::Result
|
265
|
+
# result.success? #=> false
|
266
|
+
# result.error #=> 'second step'
|
267
|
+
def steps
|
268
|
+
result = catch(:cuprum_failed_step) { yield }
|
269
|
+
|
270
|
+
return result if result.respond_to?(:to_cuprum_result)
|
271
|
+
|
272
|
+
success(result)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -129,8 +129,15 @@ module Cuprum::Utils
|
|
129
129
|
end # eigenclass
|
130
130
|
|
131
131
|
# (see Cuprum::Command#call)
|
132
|
-
def call *args, &block
|
133
|
-
|
132
|
+
def call *args, **kwargs, &block
|
133
|
+
if kwargs.empty?
|
134
|
+
Cuprum::Utils::InstanceSpy.send(:call_spies_for, self, *args, &block)
|
135
|
+
else
|
136
|
+
# :nocov:
|
137
|
+
Cuprum::Utils::InstanceSpy
|
138
|
+
.send(:call_spies_for, self, *args, **kwargs, &block)
|
139
|
+
# :nocov:
|
140
|
+
end
|
134
141
|
|
135
142
|
super
|
136
143
|
end # method call
|
data/lib/cuprum/version.rb
CHANGED
@@ -8,13 +8,13 @@ module Cuprum
|
|
8
8
|
# Major version.
|
9
9
|
MAJOR = 0
|
10
10
|
# Minor version.
|
11
|
-
MINOR =
|
11
|
+
MINOR = 10
|
12
12
|
# Patch version.
|
13
13
|
PATCH = 0
|
14
14
|
# Prerelease version.
|
15
|
-
PRERELEASE =
|
15
|
+
PRERELEASE = :rc
|
16
16
|
# Build metadata.
|
17
|
-
BUILD =
|
17
|
+
BUILD = 0
|
18
18
|
|
19
19
|
class << self
|
20
20
|
# Generates the gem version string from the Version constants.
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cuprum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0.rc.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob "Merlin" Smith
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sleeping_king_studios-tools
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: rspec
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,14 +115,23 @@ files:
|
|
101
115
|
- lib/cuprum/built_in/null_operation.rb
|
102
116
|
- lib/cuprum/chaining.rb
|
103
117
|
- lib/cuprum/command.rb
|
104
|
-
- lib/cuprum/
|
118
|
+
- lib/cuprum/command_factory.rb
|
119
|
+
- lib/cuprum/currying.rb
|
120
|
+
- lib/cuprum/currying/curried_command.rb
|
121
|
+
- lib/cuprum/error.rb
|
122
|
+
- lib/cuprum/errors.rb
|
123
|
+
- lib/cuprum/errors/command_not_implemented.rb
|
124
|
+
- lib/cuprum/errors/operation_not_called.rb
|
105
125
|
- lib/cuprum/operation.rb
|
106
126
|
- lib/cuprum/processing.rb
|
107
127
|
- lib/cuprum/result.rb
|
108
128
|
- lib/cuprum/result_helpers.rb
|
129
|
+
- lib/cuprum/rspec.rb
|
130
|
+
- lib/cuprum/rspec/be_a_result.rb
|
131
|
+
- lib/cuprum/rspec/be_a_result_matcher.rb
|
132
|
+
- lib/cuprum/steps.rb
|
109
133
|
- lib/cuprum/utils.rb
|
110
134
|
- lib/cuprum/utils/instance_spy.rb
|
111
|
-
- lib/cuprum/utils/result_not_empty_warning.rb
|
112
135
|
- lib/cuprum/version.rb
|
113
136
|
homepage: http://sleepingkingstudios.com
|
114
137
|
licenses:
|
@@ -125,12 +148,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
125
148
|
version: '0'
|
126
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
150
|
requirements:
|
128
|
-
- - "
|
151
|
+
- - ">"
|
129
152
|
- !ruby/object:Gem::Version
|
130
|
-
version:
|
153
|
+
version: 1.3.1
|
131
154
|
requirements: []
|
132
|
-
|
133
|
-
rubygems_version: 2.6.13
|
155
|
+
rubygems_version: 3.1.2
|
134
156
|
signing_key:
|
135
157
|
specification_version: 4
|
136
158
|
summary: An opinionated implementation of the Command pattern.
|
@@ -1,14 +0,0 @@
|
|
1
|
-
require 'cuprum'
|
2
|
-
|
3
|
-
module Cuprum
|
4
|
-
# Error class for calling a Command that was not given a definition block
|
5
|
-
# or have a #process method defined.
|
6
|
-
class NotImplementedError < StandardError
|
7
|
-
# Error message for a NotImplementedError.
|
8
|
-
DEFAULT_MESSAGE = 'no implementation defined for command'.freeze
|
9
|
-
|
10
|
-
def initialize message = nil
|
11
|
-
super(message || DEFAULT_MESSAGE)
|
12
|
-
end # constructor
|
13
|
-
end # class
|
14
|
-
end # module
|
@@ -1,72 +0,0 @@
|
|
1
|
-
require 'cuprum/utils'
|
2
|
-
|
3
|
-
module Cuprum::Utils
|
4
|
-
# Helper class for building a warning message when a command returns a result,
|
5
|
-
# but the command's current result already has errors, a set status, or is
|
6
|
-
# halted.
|
7
|
-
class ResultNotEmptyWarning
|
8
|
-
MESSAGE = '#process returned a result, but '.freeze
|
9
|
-
private_constant :MESSAGE
|
10
|
-
|
11
|
-
# @param result [Cuprum::Result] The result for which to generate the
|
12
|
-
# warning message.
|
13
|
-
def initialize result
|
14
|
-
@result = result
|
15
|
-
end # constructor
|
16
|
-
|
17
|
-
# @return [String] The warning message for the given result.
|
18
|
-
def message
|
19
|
-
return ''.freeze if warnings.empty?
|
20
|
-
|
21
|
-
MESSAGE + humanize_list(warnings).freeze
|
22
|
-
end # method message
|
23
|
-
|
24
|
-
# @return [Boolean] True if a warning is generated, otherwise false.
|
25
|
-
def warning?
|
26
|
-
!warnings.empty?
|
27
|
-
end # method warning?
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
attr_reader :result
|
32
|
-
|
33
|
-
def errors_not_empty_warning
|
34
|
-
return nil if result.errors.empty?
|
35
|
-
|
36
|
-
"there were already errors #{@result.errors.inspect}".freeze
|
37
|
-
end # method errors_not_empty_warning
|
38
|
-
|
39
|
-
def halted_warning
|
40
|
-
return nil unless result.halted?
|
41
|
-
|
42
|
-
'the command was halted'.freeze
|
43
|
-
end # method halted_warning
|
44
|
-
|
45
|
-
def humanize_list list, empty_value: ''
|
46
|
-
return empty_value if list.size.zero?
|
47
|
-
|
48
|
-
return list.first.to_s if list.size == 1
|
49
|
-
|
50
|
-
return "#{list.first} and #{list.last}" if list.size == 2
|
51
|
-
|
52
|
-
"#{list[0...-1].join ', '}, and #{list.last}"
|
53
|
-
end # method humanize_list
|
54
|
-
|
55
|
-
def status_set_warning
|
56
|
-
status = result.send(:status)
|
57
|
-
|
58
|
-
return nil if status.nil?
|
59
|
-
|
60
|
-
"the status was set to #{status.inspect}".freeze
|
61
|
-
end # method status_set_warning
|
62
|
-
|
63
|
-
def warnings
|
64
|
-
@warnings ||=
|
65
|
-
[
|
66
|
-
errors_not_empty_warning,
|
67
|
-
status_set_warning,
|
68
|
-
halted_warning
|
69
|
-
].compact
|
70
|
-
end # method warnings
|
71
|
-
end # class
|
72
|
-
end # module
|