cuprum 0.9.1 → 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 +4 -4
- data/CHANGELOG.md +20 -0
- data/DEVELOPMENT.md +53 -47
- data/README.md +283 -483
- data/lib/cuprum.rb +1 -0
- data/lib/cuprum/chaining.rb +22 -1
- data/lib/cuprum/command.rb +8 -1
- data/lib/cuprum/command_factory.rb +43 -19
- data/lib/cuprum/currying.rb +78 -0
- data/lib/cuprum/currying/curried_command.rb +109 -0
- data/lib/cuprum/operation.rb +1 -1
- data/lib/cuprum/processing.rb +10 -14
- data/lib/cuprum/result.rb +1 -1
- data/lib/cuprum/result_helpers.rb +22 -0
- data/lib/cuprum/rspec/be_a_result_matcher.rb +3 -3
- data/lib/cuprum/steps.rb +275 -0
- data/lib/cuprum/utils/instance_spy.rb +9 -2
- data/lib/cuprum/version.rb +4 -4
- metadata +11 -7
data/lib/cuprum.rb
CHANGED
data/lib/cuprum/chaining.rb
CHANGED
@@ -197,7 +197,7 @@ module Cuprum
|
|
197
197
|
# @see Cuprum::Command
|
198
198
|
module Chaining
|
199
199
|
# (see Cuprum::Processing#call)
|
200
|
-
def call(*args, &block)
|
200
|
+
def call(*args, **kwargs, &block)
|
201
201
|
yield_chain(super)
|
202
202
|
end
|
203
203
|
|
@@ -282,6 +282,8 @@ module Cuprum
|
|
282
282
|
|
283
283
|
protected
|
284
284
|
|
285
|
+
# rubocop:disable Metrics/MethodLength
|
286
|
+
|
285
287
|
# @!visibility public
|
286
288
|
#
|
287
289
|
# As #chain, but modifies the current command instead of creating a clone.
|
@@ -319,6 +321,11 @@ module Cuprum
|
|
319
321
|
#
|
320
322
|
# @yieldparam value [Object] The value of the previous result.
|
321
323
|
def chain!(command = nil, on: nil, &block)
|
324
|
+
SleepingKingStudios::Tools::CoreTools.deprecate(
|
325
|
+
"#{self.class}#chain",
|
326
|
+
message: 'Use the #step method to compose commands.'
|
327
|
+
)
|
328
|
+
|
322
329
|
command ||= Cuprum::Command.new(&block)
|
323
330
|
|
324
331
|
chained_procs <<
|
@@ -329,11 +336,14 @@ module Cuprum
|
|
329
336
|
|
330
337
|
self
|
331
338
|
end
|
339
|
+
# rubocop:enable Metrics/MethodLength
|
332
340
|
|
333
341
|
def chained_procs
|
334
342
|
@chained_procs ||= []
|
335
343
|
end
|
336
344
|
|
345
|
+
# rubocop:disable Metrics/MethodLength
|
346
|
+
|
337
347
|
# @!visibility public
|
338
348
|
#
|
339
349
|
# As #tap_result, but modifies the current command instead of creating a
|
@@ -348,6 +358,11 @@ module Cuprum
|
|
348
358
|
#
|
349
359
|
# @see #tap_result
|
350
360
|
def tap_result!(on: nil, &block)
|
361
|
+
SleepingKingStudios::Tools::CoreTools.deprecate(
|
362
|
+
"#{self.class}#tap_result",
|
363
|
+
message: 'Use the #step method to compose commands.'
|
364
|
+
)
|
365
|
+
|
351
366
|
tapped = ->(result) { result.tap { block.call(result) } }
|
352
367
|
|
353
368
|
chained_procs <<
|
@@ -358,6 +373,7 @@ module Cuprum
|
|
358
373
|
|
359
374
|
self
|
360
375
|
end
|
376
|
+
# rubocop:enable Metrics/MethodLength
|
361
377
|
|
362
378
|
# @!visibility public
|
363
379
|
#
|
@@ -373,6 +389,11 @@ module Cuprum
|
|
373
389
|
#
|
374
390
|
# @see #yield_result
|
375
391
|
def yield_result!(on: nil, &block)
|
392
|
+
SleepingKingStudios::Tools::CoreTools.deprecate(
|
393
|
+
"#{self.class}#yield_result",
|
394
|
+
message: 'Use the #step method to compose commands.'
|
395
|
+
)
|
396
|
+
|
376
397
|
chained_procs <<
|
377
398
|
{
|
378
399
|
proc: block,
|
data/lib/cuprum/command.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'cuprum/chaining'
|
2
|
+
require 'cuprum/currying'
|
2
3
|
require 'cuprum/processing'
|
4
|
+
require 'cuprum/steps'
|
3
5
|
|
4
6
|
module Cuprum
|
5
7
|
# Functional object that encapsulates a business logic operation with a
|
@@ -111,7 +113,8 @@ module Cuprum
|
|
111
113
|
# @see Cuprum::Processing
|
112
114
|
class Command
|
113
115
|
include Cuprum::Processing
|
114
|
-
include Cuprum::
|
116
|
+
include Cuprum::Currying
|
117
|
+
include Cuprum::Steps
|
115
118
|
|
116
119
|
# Returns a new instance of Cuprum::Command.
|
117
120
|
#
|
@@ -126,5 +129,9 @@ module Cuprum
|
|
126
129
|
|
127
130
|
singleton_class.send(:private, :process)
|
128
131
|
end # method initialize
|
132
|
+
|
133
|
+
def call(*args, **kwargs, &block)
|
134
|
+
steps { super }
|
135
|
+
end
|
129
136
|
end # class
|
130
137
|
end # module
|
@@ -28,7 +28,7 @@ module Cuprum
|
|
28
28
|
#
|
29
29
|
# factory::Dream #=> DreamCommand
|
30
30
|
# factory.dream #=> an instance of DreamCommand
|
31
|
-
class CommandFactory < Module
|
31
|
+
class CommandFactory < Module # rubocop:disable Metrics/ClassLength
|
32
32
|
# Defines the Domain-Specific Language and helper methods for dynamically
|
33
33
|
# defined commands.
|
34
34
|
class << self
|
@@ -149,18 +149,12 @@ module Cuprum
|
|
149
149
|
|
150
150
|
raise ArgumentError, 'must provide a block'.freeze unless block_given?
|
151
151
|
|
152
|
-
|
152
|
+
method_name = normalize_command_name(name)
|
153
153
|
|
154
|
-
(@command_definitions ||= {})[
|
154
|
+
(@command_definitions ||= {})[method_name] =
|
155
155
|
metadata.merge(__const_defn__: defn)
|
156
156
|
|
157
|
-
|
158
|
-
|
159
|
-
define_method(name) do |*args, &block|
|
160
|
-
command_class = const_get(const_name)
|
161
|
-
|
162
|
-
build_command(command_class, *args, &block)
|
163
|
-
end
|
157
|
+
define_lazy_command_method(method_name)
|
164
158
|
end
|
165
159
|
|
166
160
|
protected
|
@@ -184,21 +178,47 @@ module Cuprum
|
|
184
178
|
|
185
179
|
(@command_definitions ||= {})[command_name] = metadata
|
186
180
|
|
187
|
-
define_method(command_name) do |*args|
|
188
|
-
|
181
|
+
define_method(command_name) do |*args, **kwargs|
|
182
|
+
if kwargs.empty?
|
183
|
+
instance_exec(*args, &builder)
|
184
|
+
else
|
185
|
+
instance_exec(*args, **kwargs, &builder)
|
186
|
+
end
|
189
187
|
end
|
190
188
|
end
|
191
189
|
|
192
190
|
def define_command_from_class(command_class, name:, metadata: {})
|
193
191
|
guard_invalid_definition!(command_class)
|
194
192
|
|
195
|
-
|
193
|
+
method_name = normalize_command_name(name)
|
196
194
|
|
197
|
-
(@command_definitions ||= {})[
|
195
|
+
(@command_definitions ||= {})[method_name] =
|
198
196
|
metadata.merge(__const_defn__: command_class)
|
199
197
|
|
200
|
-
|
201
|
-
|
198
|
+
define_command_method(method_name, command_class)
|
199
|
+
end
|
200
|
+
|
201
|
+
def define_command_method(method_name, command_class)
|
202
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
203
|
+
if kwargs.empty?
|
204
|
+
build_command(command_class, *args, &block)
|
205
|
+
else
|
206
|
+
build_command(command_class, *args, **kwargs, &block)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def define_lazy_command_method(method_name)
|
212
|
+
const_name = tools.string_tools.camelize(method_name)
|
213
|
+
|
214
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
215
|
+
command_class = const_get(const_name)
|
216
|
+
|
217
|
+
if kwargs.empty?
|
218
|
+
build_command(command_class, *args, &block)
|
219
|
+
else
|
220
|
+
build_command(command_class, *args, **kwargs, &block)
|
221
|
+
end
|
202
222
|
end
|
203
223
|
end
|
204
224
|
|
@@ -217,7 +237,7 @@ module Cuprum
|
|
217
237
|
end
|
218
238
|
|
219
239
|
def normalize_command_name(command_name)
|
220
|
-
tools.
|
240
|
+
tools.string_tools.underscore(command_name).intern
|
221
241
|
end
|
222
242
|
|
223
243
|
def require_definition!
|
@@ -265,8 +285,12 @@ module Cuprum
|
|
265
285
|
|
266
286
|
private
|
267
287
|
|
268
|
-
def build_command(command_class, *args, &block)
|
269
|
-
|
288
|
+
def build_command(command_class, *args, **kwargs, &block)
|
289
|
+
if kwargs.empty?
|
290
|
+
command_class.new(*args, &block)
|
291
|
+
else
|
292
|
+
command_class.new(*args, **kwargs, &block)
|
293
|
+
end
|
270
294
|
end
|
271
295
|
|
272
296
|
def normalize_command_name(command_name)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum'
|
4
|
+
|
5
|
+
module Cuprum
|
6
|
+
# Implements partial application for command objects.
|
7
|
+
#
|
8
|
+
# Partial application (more commonly referred to, if imprecisely, as currying)
|
9
|
+
# refers to fixing some number of arguments to a function, resulting in a
|
10
|
+
# function with a smaller number of arguments.
|
11
|
+
#
|
12
|
+
# In Cuprum's case, a curried (partially applied) command takes an original
|
13
|
+
# command and pre-defines some of its arguments. When the curried command is
|
14
|
+
# called, the predefined arguments and/or keywords will be combined with the
|
15
|
+
# arguments passed to #call.
|
16
|
+
#
|
17
|
+
# @example Currying Arguments
|
18
|
+
# # Our base command takes two arguments.
|
19
|
+
# say_command = Cuprum::Command.new do |greeting, person|
|
20
|
+
# "#{greeting}, #{person}!"
|
21
|
+
# end
|
22
|
+
# say_command.call('Hello', 'world')
|
23
|
+
# #=> returns a result with value 'Hello, world!'
|
24
|
+
#
|
25
|
+
# # Next, we create a curried command. This sets the first argument to
|
26
|
+
# # always be 'Greetings', so our curried command only takes one argument,
|
27
|
+
# # namely the name of the person being greeted.
|
28
|
+
# greet_command = say_command.curry('Greetings')
|
29
|
+
# greet_command.call('programs')
|
30
|
+
# #=> returns a result with value 'Greetings, programs!'
|
31
|
+
#
|
32
|
+
# # Here, we are creating a curried command that passes both arguments.
|
33
|
+
# # Therefore, our curried command does not take any arguments.
|
34
|
+
# recruit_command = say_command.curry('Greetings', 'starfighter')
|
35
|
+
# recruit_command.call
|
36
|
+
# #=> returns a result with value 'Greetings, starfighter!'
|
37
|
+
#
|
38
|
+
# @example Currying Keywords
|
39
|
+
# # Our base command takes two keywords: a math operation and an array of
|
40
|
+
# # integers.
|
41
|
+
# math_command = Cuprum::Command.new do |operands:, operation:|
|
42
|
+
# operations.reduce(&operation)
|
43
|
+
# end
|
44
|
+
# math_command.call(operands: [2, 2], operation: :+)
|
45
|
+
# #=> returns a result with value 4
|
46
|
+
#
|
47
|
+
# # Our curried command still takes two keywords, but now the operation
|
48
|
+
# # keyword is optional. It now defaults to :*, for multiplication.
|
49
|
+
# multiply_command = math_command.curry(operation: :*)
|
50
|
+
# multiply_command.call(operands: [3, 3])
|
51
|
+
# #=> returns a result with value 9
|
52
|
+
module Currying
|
53
|
+
autoload :CurriedCommand, 'cuprum/currying/curried_command'
|
54
|
+
|
55
|
+
# Returns a CurriedCommand that wraps this command with pre-set arguments.
|
56
|
+
#
|
57
|
+
# When the curried command is called, the predefined arguments and/or
|
58
|
+
# keywords will be combined with the arguments passed to #call.
|
59
|
+
#
|
60
|
+
# The original command is unchanged.
|
61
|
+
#
|
62
|
+
# @param arguments [Array] The arguments to pass to the curried command.
|
63
|
+
# @param keywords [Hash] The keywords to pass to the curried command.
|
64
|
+
#
|
65
|
+
# @return [Cuprum::Currying::CurriedCommand] the curried command.
|
66
|
+
#
|
67
|
+
# @see Cuprum::Currying::CurriedCommand#call
|
68
|
+
def curry(*arguments, **keywords)
|
69
|
+
return self if arguments.empty? && keywords.empty?
|
70
|
+
|
71
|
+
Cuprum::Currying::CurriedCommand.new(
|
72
|
+
arguments: arguments,
|
73
|
+
command: self,
|
74
|
+
keywords: keywords
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/currying'
|
4
|
+
|
5
|
+
module Cuprum::Currying
|
6
|
+
# A CurriedCommand wraps another command and passes preset args to #call.
|
7
|
+
#
|
8
|
+
# @example Currying Arguments
|
9
|
+
# # Our base command takes two arguments.
|
10
|
+
# say_command = Cuprum::Command.new do |greeting, person|
|
11
|
+
# "#{greeting}, #{person}!"
|
12
|
+
# end
|
13
|
+
# say_command.call('Hello', 'world')
|
14
|
+
# #=> returns a result with value 'Hello, world!'
|
15
|
+
#
|
16
|
+
# # Next, we create a curried command. This sets the first argument to
|
17
|
+
# # always be 'Greetings', so our curried command only takes one argument,
|
18
|
+
# # namely the name of the person being greeted.
|
19
|
+
# greet_command =
|
20
|
+
# Cuprum::CurriedCommand.new(
|
21
|
+
# arguments: ['Greetings'],
|
22
|
+
# command: say_command
|
23
|
+
# )
|
24
|
+
# greet_command.call('programs')
|
25
|
+
# #=> returns a result with value 'Greetings, programs!'
|
26
|
+
#
|
27
|
+
# # Here, we are creating a curried command that passes both arguments.
|
28
|
+
# # Therefore, our curried command does not take any arguments.
|
29
|
+
# recruit_command =
|
30
|
+
# Cuprum::CurriedCommand.new(
|
31
|
+
# arguments: ['Greetings', 'starfighter'],
|
32
|
+
# command: say_command
|
33
|
+
# )
|
34
|
+
# recruit_command.call
|
35
|
+
# #=> returns a result with value 'Greetings, starfighter!'
|
36
|
+
#
|
37
|
+
# @example Currying Keywords
|
38
|
+
# # Our base command takes two keywords: a math operation and an array of
|
39
|
+
# # integers.
|
40
|
+
# math_command = Cuprum::Command.new do |operands:, operation:|
|
41
|
+
# operations.reduce(&operation)
|
42
|
+
# end
|
43
|
+
# math_command.call(operands: [2, 2], operation: :+)
|
44
|
+
# #=> returns a result with value 4
|
45
|
+
#
|
46
|
+
# # Our curried command still takes two keywords, but now the operation
|
47
|
+
# # keyword is optional. It now defaults to :*, for multiplication.
|
48
|
+
# multiply_command =
|
49
|
+
# Cuprum::CurriedCommand.new(
|
50
|
+
# command: math_command,
|
51
|
+
# keywords: { operation: :* }
|
52
|
+
# )
|
53
|
+
# multiply_command.call(operands: [3, 3])
|
54
|
+
# #=> returns a result with value 9
|
55
|
+
class CurriedCommand < Cuprum::Command
|
56
|
+
# @param arguments [Array] The arguments to pass to the curried command.
|
57
|
+
# @param command [Cuprum::Command] The original command to curry.
|
58
|
+
# @param keywords [Hash] The keywords to pass to the curried command.
|
59
|
+
def initialize(arguments: [], command:, keywords: {})
|
60
|
+
@arguments = arguments
|
61
|
+
@command = command
|
62
|
+
@keywords = keywords
|
63
|
+
end
|
64
|
+
|
65
|
+
# @!method call(*args, **kwargs)
|
66
|
+
# Merges the arguments and keywords and calls the wrapped command.
|
67
|
+
#
|
68
|
+
# First, the arguments array is created starting with the :arguments
|
69
|
+
# passed to #initialize. Any positional arguments passed directly to #call
|
70
|
+
# are then appended.
|
71
|
+
#
|
72
|
+
# Second, the keyword arguments are created by merging the keywords passed
|
73
|
+
# directly into #call into the keywods passed to #initialize. This means
|
74
|
+
# that if a key is passed in both places, the value passed into #call will
|
75
|
+
# take precedence.
|
76
|
+
#
|
77
|
+
# Finally, the merged arguments and keywords are passed into the original
|
78
|
+
# command's #call method.
|
79
|
+
#
|
80
|
+
# @param args [Array] Additional arguments to pass to the curried command.
|
81
|
+
# @param kwargs [Hash] Additional keywords to pass to the curried command.
|
82
|
+
#
|
83
|
+
# @return [Cuprum::Result]
|
84
|
+
#
|
85
|
+
# @see Cuprum::Processing#call
|
86
|
+
|
87
|
+
# @return [Array] the arguments to pass to the curried command.
|
88
|
+
attr_reader :arguments
|
89
|
+
|
90
|
+
# @return [Cuprum::Command] the original command to curry.
|
91
|
+
attr_reader :command
|
92
|
+
|
93
|
+
# @return [Hash] the keywords to pass to the curried command.
|
94
|
+
attr_reader :keywords
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def process(*args, **kwargs, &block)
|
99
|
+
args = [*arguments, *args]
|
100
|
+
kwargs = keywords.merge(kwargs)
|
101
|
+
|
102
|
+
if kwargs.empty?
|
103
|
+
command.call(*args, &block)
|
104
|
+
else
|
105
|
+
command.call(*args, **kwargs, &block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/cuprum/operation.rb
CHANGED
data/lib/cuprum/processing.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'cuprum/errors/command_not_implemented'
|
4
|
+
require 'cuprum/result_helpers'
|
4
5
|
|
5
6
|
module Cuprum
|
6
7
|
# Functional implementation for creating a command object. Cuprum::Processing
|
@@ -57,6 +58,8 @@ module Cuprum
|
|
57
58
|
#
|
58
59
|
# @see Cuprum::Command
|
59
60
|
module Processing
|
61
|
+
include Cuprum::ResultHelpers
|
62
|
+
|
60
63
|
# Returns a nonnegative integer for commands that take a fixed number of
|
61
64
|
# arguments. For commands that take a variable number of arguments, returns
|
62
65
|
# -n-1, where n is the number of required arguments.
|
@@ -87,8 +90,13 @@ module Cuprum
|
|
87
90
|
#
|
88
91
|
# @yield If a block argument is given, it will be passed to the
|
89
92
|
# implementation.
|
90
|
-
def call(*args, &block)
|
91
|
-
value =
|
93
|
+
def call(*args, **kwargs, &block)
|
94
|
+
value =
|
95
|
+
if kwargs.empty?
|
96
|
+
process(*args, &block)
|
97
|
+
else
|
98
|
+
process(*args, **kwargs, &block)
|
99
|
+
end
|
92
100
|
|
93
101
|
return value.to_cuprum_result if value_is_result?(value)
|
94
102
|
|
@@ -97,14 +105,6 @@ module Cuprum
|
|
97
105
|
|
98
106
|
private
|
99
107
|
|
100
|
-
def build_result(error: nil, status: nil, value: nil)
|
101
|
-
Cuprum::Result.new(error: error, status: status, value: value)
|
102
|
-
end
|
103
|
-
|
104
|
-
def failure(error)
|
105
|
-
build_result(error: error)
|
106
|
-
end
|
107
|
-
|
108
108
|
# @!visibility public
|
109
109
|
# @overload process(*arguments, **keywords, &block)
|
110
110
|
# The implementation of the command, to be executed when the #call method
|
@@ -128,10 +128,6 @@ module Cuprum
|
|
128
128
|
build_result(error: error)
|
129
129
|
end
|
130
130
|
|
131
|
-
def success(value)
|
132
|
-
build_result(value: value)
|
133
|
-
end
|
134
|
-
|
135
131
|
def value_is_result?(value)
|
136
132
|
value.respond_to?(:to_cuprum_result)
|
137
133
|
end
|