cuprum 0.8.0 → 0.10.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 +85 -9
- data/DEVELOPMENT.md +58 -42
- data/README.md +414 -754
- data/lib/cuprum.rb +1 -25
- data/lib/cuprum/built_in/identity_command.rb +4 -4
- data/lib/cuprum/chaining.rb +139 -148
- data/lib/cuprum/command.rb +24 -15
- 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/error.rb +37 -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 -82
- data/lib/cuprum/result.rb +52 -127
- 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 +1 -1
- metadata +14 -8
- data/lib/cuprum/errors/process_not_implemented_error.rb +0 -14
- data/lib/cuprum/utils/result_not_empty_warning.rb +0 -72
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
|
@@ -32,7 +34,7 @@ module Cuprum
|
|
32
34
|
#
|
33
35
|
# result.value #=> 15
|
34
36
|
#
|
35
|
-
# @example A Command with
|
37
|
+
# @example A Command with an error state
|
36
38
|
# class DivideCommand < Cuprum::Command
|
37
39
|
# def initialize divisor
|
38
40
|
# @divisor = divisor
|
@@ -42,10 +44,8 @@ module Cuprum
|
|
42
44
|
#
|
43
45
|
# def process int
|
44
46
|
# if @divisor.zero?
|
45
|
-
#
|
46
|
-
#
|
47
|
-
# return
|
48
|
-
# end # if
|
47
|
+
# return Cuprum::Result.new(error: 'errors.messages.divide_by_zero')
|
48
|
+
# end
|
49
49
|
#
|
50
50
|
# int / @divisor
|
51
51
|
# end # method process
|
@@ -54,14 +54,14 @@ module Cuprum
|
|
54
54
|
# halve_command = DivideCommand.new(2)
|
55
55
|
# result = halve_command.call(10)
|
56
56
|
#
|
57
|
-
# result.
|
58
|
-
# result.value
|
57
|
+
# result.error #=> nil
|
58
|
+
# result.value #=> 5
|
59
59
|
#
|
60
|
-
#
|
61
|
-
# result
|
60
|
+
# divide_command = DivideCommand.new(0)
|
61
|
+
# result = divide_command.call(10)
|
62
62
|
#
|
63
|
-
# result.
|
64
|
-
# result.value
|
63
|
+
# result.error #=> 'errors.messages.divide_by_zero'
|
64
|
+
# result.value #=> nil
|
65
65
|
#
|
66
66
|
# @example Command Chaining
|
67
67
|
# class AddCommand < Cuprum::Command
|
@@ -86,9 +86,9 @@ module Cuprum
|
|
86
86
|
# private
|
87
87
|
#
|
88
88
|
# def process int
|
89
|
-
#
|
89
|
+
# return int if int.even?
|
90
90
|
#
|
91
|
-
#
|
91
|
+
# Cuprum::Errors.new(error: 'errors.messages.not_even')
|
92
92
|
# end # method process
|
93
93
|
# end # class
|
94
94
|
#
|
@@ -113,7 +113,8 @@ module Cuprum
|
|
113
113
|
# @see Cuprum::Processing
|
114
114
|
class Command
|
115
115
|
include Cuprum::Processing
|
116
|
-
include Cuprum::
|
116
|
+
include Cuprum::Currying
|
117
|
+
include Cuprum::Steps
|
117
118
|
|
118
119
|
# Returns a new instance of Cuprum::Command.
|
119
120
|
#
|
@@ -122,7 +123,15 @@ module Cuprum
|
|
122
123
|
# value of the block. This overrides the implementation in #process, if
|
123
124
|
# any.
|
124
125
|
def initialize &implementation
|
125
|
-
|
126
|
+
return unless implementation
|
127
|
+
|
128
|
+
define_singleton_method :process, &implementation
|
129
|
+
|
130
|
+
singleton_class.send(:private, :process)
|
126
131
|
end # method initialize
|
132
|
+
|
133
|
+
def call(*args, **kwargs, &block)
|
134
|
+
steps { super }
|
135
|
+
end
|
127
136
|
end # class
|
128
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/error.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum'
|
4
|
+
|
5
|
+
module Cuprum
|
6
|
+
# Wrapper class for encapsulating an error state for a failed Cuprum result.
|
7
|
+
# Additional details can be passed by setting the #message or by using a
|
8
|
+
# subclass of Cuprum::Error.
|
9
|
+
class Error
|
10
|
+
COMPARABLE_PROPERTIES = %i[message].freeze
|
11
|
+
private_constant :COMPARABLE_PROPERTIES
|
12
|
+
|
13
|
+
# @param message [String] Optional message describing the nature of the
|
14
|
+
# error.
|
15
|
+
def initialize(message: nil)
|
16
|
+
@message = message
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String] Optional message describing the nature of the error.
|
20
|
+
attr_reader :message
|
21
|
+
|
22
|
+
# @param other [Cuprum::Error] The other object to compare.
|
23
|
+
#
|
24
|
+
# @return [Boolean] true if the other object has the same class and message;
|
25
|
+
# otherwise false.
|
26
|
+
def ==(other)
|
27
|
+
other.instance_of?(self.class) &&
|
28
|
+
comparable_properties.all? { |prop| send(prop) == other.send(prop) }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def comparable_properties
|
34
|
+
self.class.const_get(:COMPARABLE_PROPERTIES)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/error'
|
4
|
+
require 'cuprum/errors'
|
5
|
+
|
6
|
+
module Cuprum::Errors
|
7
|
+
# Error class to be used when a Command is called without defining a #process
|
8
|
+
# method.
|
9
|
+
class CommandNotImplemented < Cuprum::Error
|
10
|
+
COMPARABLE_PROPERTIES = %i[command].freeze
|
11
|
+
private_constant :COMPARABLE_PROPERTIES
|
12
|
+
|
13
|
+
# Format for generating error message.
|
14
|
+
MESSAGE_FORMAT = 'no implementation defined for %s'
|
15
|
+
|
16
|
+
# @param command [Cuprum::Command] The command called without a definition.
|
17
|
+
def initialize(command:)
|
18
|
+
@command = command
|
19
|
+
|
20
|
+
class_name = command&.class&.name || 'command'
|
21
|
+
message = MESSAGE_FORMAT % class_name
|
22
|
+
|
23
|
+
super(message: message)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Cuprum::Command] The command called without a definition.
|
27
|
+
attr_reader :command
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def comparable_properties
|
32
|
+
COMPARABLE_PROPERTIES
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|