cuprum 0.9.1 → 0.11.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 +70 -0
- data/DEVELOPMENT.md +42 -53
- data/README.md +728 -536
- data/lib/cuprum.rb +12 -6
- data/lib/cuprum/built_in.rb +3 -1
- data/lib/cuprum/built_in/identity_command.rb +6 -4
- data/lib/cuprum/built_in/identity_operation.rb +4 -2
- data/lib/cuprum/built_in/null_command.rb +5 -3
- data/lib/cuprum/built_in/null_operation.rb +4 -2
- data/lib/cuprum/command.rb +37 -59
- data/lib/cuprum/command_factory.rb +50 -24
- data/lib/cuprum/currying.rb +79 -0
- data/lib/cuprum/currying/curried_command.rb +116 -0
- data/lib/cuprum/error.rb +44 -10
- data/lib/cuprum/errors.rb +2 -0
- data/lib/cuprum/errors/command_not_implemented.rb +6 -3
- data/lib/cuprum/errors/operation_not_called.rb +6 -6
- data/lib/cuprum/errors/uncaught_exception.rb +55 -0
- data/lib/cuprum/exception_handling.rb +50 -0
- data/lib/cuprum/matcher.rb +90 -0
- data/lib/cuprum/matcher_list.rb +150 -0
- data/lib/cuprum/matching.rb +232 -0
- data/lib/cuprum/matching/match_clause.rb +65 -0
- data/lib/cuprum/middleware.rb +210 -0
- data/lib/cuprum/operation.rb +17 -15
- data/lib/cuprum/processing.rb +10 -14
- data/lib/cuprum/result.rb +2 -4
- data/lib/cuprum/result_helpers.rb +22 -0
- data/lib/cuprum/rspec/be_a_result.rb +10 -1
- data/lib/cuprum/rspec/be_a_result_matcher.rb +5 -7
- data/lib/cuprum/rspec/be_callable.rb +14 -0
- data/lib/cuprum/steps.rb +233 -0
- data/lib/cuprum/utils.rb +3 -1
- data/lib/cuprum/utils/instance_spy.rb +37 -30
- data/lib/cuprum/version.rb +13 -10
- metadata +34 -19
- data/lib/cuprum/chaining.rb +0 -420
data/lib/cuprum.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# A lightweight, functional-lite toolkit for making business logic a first-class
|
2
4
|
# citizen of your application.
|
3
5
|
module Cuprum
|
4
|
-
autoload :Command,
|
5
|
-
autoload :
|
6
|
-
autoload :
|
6
|
+
autoload :Command, 'cuprum/command'
|
7
|
+
autoload :Error, 'cuprum/error'
|
8
|
+
autoload :Matcher, 'cuprum/matcher'
|
9
|
+
autoload :Middleware, 'cuprum/middleware'
|
10
|
+
autoload :Operation, 'cuprum/operation'
|
11
|
+
autoload :Result, 'cuprum/result'
|
12
|
+
autoload :Steps, 'cuprum/steps'
|
7
13
|
|
8
14
|
class << self
|
9
15
|
# @return [String] The current version of the gem.
|
10
16
|
def version
|
11
17
|
VERSION
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/cuprum/built_in.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cuprum/built_in'
|
2
4
|
require 'cuprum/command'
|
3
5
|
|
@@ -24,8 +26,8 @@ module Cuprum::BuiltIn
|
|
24
26
|
class IdentityCommand < Cuprum::Command
|
25
27
|
private
|
26
28
|
|
27
|
-
def process
|
29
|
+
def process(value = nil)
|
28
30
|
value
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cuprum/built_in/identity_command'
|
2
4
|
require 'cuprum/operation'
|
3
5
|
|
@@ -23,5 +25,5 @@ module Cuprum::BuiltIn
|
|
23
25
|
# #=> ['errors.messages.unknown']
|
24
26
|
class IdentityOperation < Cuprum::BuiltIn::IdentityCommand
|
25
27
|
include Cuprum::Operation::Mixin
|
26
|
-
end
|
27
|
-
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cuprum/built_in'
|
2
4
|
require 'cuprum/command'
|
3
5
|
|
@@ -13,6 +15,6 @@ module Cuprum::BuiltIn
|
|
13
15
|
class NullCommand < Cuprum::Command
|
14
16
|
private
|
15
17
|
|
16
|
-
def process
|
17
|
-
end
|
18
|
-
end
|
18
|
+
def process(*_args); end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cuprum/built_in/null_command'
|
2
4
|
require 'cuprum/operation'
|
3
5
|
|
@@ -12,5 +14,5 @@ module Cuprum::BuiltIn
|
|
12
14
|
# #=> true
|
13
15
|
class NullOperation < Cuprum::BuiltIn::NullCommand
|
14
16
|
include Cuprum::Operation::Mixin
|
15
|
-
end
|
16
|
-
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/cuprum/command.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/currying'
|
2
4
|
require 'cuprum/processing'
|
5
|
+
require 'cuprum/steps'
|
3
6
|
|
4
7
|
module Cuprum
|
5
8
|
# Functional object that encapsulates a business logic operation with a
|
@@ -18,14 +21,14 @@ module Cuprum
|
|
18
21
|
# class MultiplyCommand < Cuprum::Command
|
19
22
|
# def initialize multiplier
|
20
23
|
# @multiplier = multiplier
|
21
|
-
# end
|
24
|
+
# end
|
22
25
|
#
|
23
26
|
# private
|
24
27
|
#
|
25
28
|
# def process int
|
26
29
|
# int * @multiplier
|
27
|
-
# end
|
28
|
-
# end
|
30
|
+
# end
|
31
|
+
# end
|
29
32
|
#
|
30
33
|
# triple_command = MultiplyCommand.new(3)
|
31
34
|
# result = command_command.call(5)
|
@@ -36,7 +39,7 @@ module Cuprum
|
|
36
39
|
# class DivideCommand < Cuprum::Command
|
37
40
|
# def initialize divisor
|
38
41
|
# @divisor = divisor
|
39
|
-
# end
|
42
|
+
# end
|
40
43
|
#
|
41
44
|
# private
|
42
45
|
#
|
@@ -46,8 +49,8 @@ module Cuprum
|
|
46
49
|
# end
|
47
50
|
#
|
48
51
|
# int / @divisor
|
49
|
-
# end
|
50
|
-
# end
|
52
|
+
# end
|
53
|
+
# end
|
51
54
|
#
|
52
55
|
# halve_command = DivideCommand.new(2)
|
53
56
|
# result = halve_command.call(10)
|
@@ -61,57 +64,11 @@ module Cuprum
|
|
61
64
|
# result.error #=> 'errors.messages.divide_by_zero'
|
62
65
|
# result.value #=> nil
|
63
66
|
#
|
64
|
-
# @example Command Chaining
|
65
|
-
# class AddCommand < Cuprum::Command
|
66
|
-
# def initialize addend
|
67
|
-
# @addend = addend
|
68
|
-
# end # constructor
|
69
|
-
#
|
70
|
-
# private
|
71
|
-
#
|
72
|
-
# def process int
|
73
|
-
# int + @addend
|
74
|
-
# end # method process
|
75
|
-
# end # class
|
76
|
-
#
|
77
|
-
# double_and_add_one = MultiplyCommand.new(2).chain(AddCommand.new(1))
|
78
|
-
# result = double_and_add_one(5)
|
79
|
-
#
|
80
|
-
# result.value #=> 5
|
81
|
-
#
|
82
|
-
# @example Conditional Chaining
|
83
|
-
# class EvenCommand < Cuprum::Command
|
84
|
-
# private
|
85
|
-
#
|
86
|
-
# def process int
|
87
|
-
# return int if int.even?
|
88
|
-
#
|
89
|
-
# Cuprum::Errors.new(error: 'errors.messages.not_even')
|
90
|
-
# end # method process
|
91
|
-
# end # class
|
92
|
-
#
|
93
|
-
# # The next step in a Collatz sequence is determined as follows:
|
94
|
-
# # - If the number is even, divide it by 2.
|
95
|
-
# # - If the number is odd, multiply it by 3 and add 1.
|
96
|
-
# collatz_command =
|
97
|
-
# EvenCommand.new.
|
98
|
-
# chain(DivideCommand.new(2), :on => :success).
|
99
|
-
# chain(
|
100
|
-
# MultiplyCommand.new(3).chain(AddCommand.new(1),
|
101
|
-
# :on => :failure
|
102
|
-
# )
|
103
|
-
#
|
104
|
-
# result = collatz_command.new(5)
|
105
|
-
# result.value #=> 16
|
106
|
-
#
|
107
|
-
# result = collatz_command.new(16)
|
108
|
-
# result.value #=> 8
|
109
|
-
#
|
110
|
-
# @see Cuprum::Chaining
|
111
67
|
# @see Cuprum::Processing
|
112
68
|
class Command
|
113
69
|
include Cuprum::Processing
|
114
|
-
include Cuprum::
|
70
|
+
include Cuprum::Currying
|
71
|
+
include Cuprum::Steps
|
115
72
|
|
116
73
|
# Returns a new instance of Cuprum::Command.
|
117
74
|
#
|
@@ -119,12 +76,33 @@ module Cuprum
|
|
119
76
|
# #call method will wrap the block and set the result #value to the return
|
120
77
|
# value of the block. This overrides the implementation in #process, if
|
121
78
|
# any.
|
122
|
-
def initialize
|
79
|
+
def initialize(&implementation)
|
123
80
|
return unless implementation
|
124
81
|
|
125
82
|
define_singleton_method :process, &implementation
|
126
83
|
|
127
84
|
singleton_class.send(:private, :process)
|
128
|
-
end
|
129
|
-
|
130
|
-
|
85
|
+
end
|
86
|
+
|
87
|
+
# (see Cuprum::Processing#call)
|
88
|
+
def call(*args, **kwargs, &block)
|
89
|
+
steps { super }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Wraps the command in a proc.
|
93
|
+
#
|
94
|
+
# Calling the proc will call the command with the given arguments, keywords,
|
95
|
+
# and block.
|
96
|
+
#
|
97
|
+
# @return [Proc] the wrapping proc.
|
98
|
+
def to_proc
|
99
|
+
@to_proc ||= lambda do |*args, **kwargs, &block|
|
100
|
+
if kwargs.empty?
|
101
|
+
call(*args, &block)
|
102
|
+
else
|
103
|
+
call(*args, **kwargs, &block)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cuprum'
|
2
4
|
|
3
5
|
require 'sleeping_king_studios/tools/toolbelt'
|
@@ -28,7 +30,7 @@ module Cuprum
|
|
28
30
|
#
|
29
31
|
# factory::Dream #=> DreamCommand
|
30
32
|
# factory.dream #=> an instance of DreamCommand
|
31
|
-
class CommandFactory < Module
|
33
|
+
class CommandFactory < Module # rubocop:disable Metrics/ClassLength
|
32
34
|
# Defines the Domain-Specific Language and helper methods for dynamically
|
33
35
|
# defined commands.
|
34
36
|
class << self
|
@@ -147,20 +149,14 @@ module Cuprum
|
|
147
149
|
def command_class(name, **metadata, &defn)
|
148
150
|
guard_abstract_factory!
|
149
151
|
|
150
|
-
raise ArgumentError, 'must provide a block'
|
152
|
+
raise ArgumentError, 'must provide a block' unless block_given?
|
151
153
|
|
152
|
-
|
154
|
+
method_name = normalize_command_name(name)
|
153
155
|
|
154
|
-
(@command_definitions ||= {})[
|
156
|
+
(@command_definitions ||= {})[method_name] =
|
155
157
|
metadata.merge(__const_defn__: defn)
|
156
158
|
|
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
|
159
|
+
define_lazy_command_method(method_name)
|
164
160
|
end
|
165
161
|
|
166
162
|
protected
|
@@ -184,21 +180,47 @@ module Cuprum
|
|
184
180
|
|
185
181
|
(@command_definitions ||= {})[command_name] = metadata
|
186
182
|
|
187
|
-
define_method(command_name) do |*args|
|
188
|
-
|
183
|
+
define_method(command_name) do |*args, **kwargs|
|
184
|
+
if kwargs.empty?
|
185
|
+
instance_exec(*args, &builder)
|
186
|
+
else
|
187
|
+
instance_exec(*args, **kwargs, &builder)
|
188
|
+
end
|
189
189
|
end
|
190
190
|
end
|
191
191
|
|
192
192
|
def define_command_from_class(command_class, name:, metadata: {})
|
193
193
|
guard_invalid_definition!(command_class)
|
194
194
|
|
195
|
-
|
195
|
+
method_name = normalize_command_name(name)
|
196
196
|
|
197
|
-
(@command_definitions ||= {})[
|
197
|
+
(@command_definitions ||= {})[method_name] =
|
198
198
|
metadata.merge(__const_defn__: command_class)
|
199
199
|
|
200
|
-
|
201
|
-
|
200
|
+
define_command_method(method_name, command_class)
|
201
|
+
end
|
202
|
+
|
203
|
+
def define_command_method(method_name, command_class)
|
204
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
205
|
+
if kwargs.empty?
|
206
|
+
build_command(command_class, *args, &block)
|
207
|
+
else
|
208
|
+
build_command(command_class, *args, **kwargs, &block)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def define_lazy_command_method(method_name)
|
214
|
+
const_name = tools.string_tools.camelize(method_name)
|
215
|
+
|
216
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
217
|
+
command_class = const_get(const_name)
|
218
|
+
|
219
|
+
if kwargs.empty?
|
220
|
+
build_command(command_class, *args, &block)
|
221
|
+
else
|
222
|
+
build_command(command_class, *args, **kwargs, &block)
|
223
|
+
end
|
202
224
|
end
|
203
225
|
end
|
204
226
|
|
@@ -207,21 +229,21 @@ module Cuprum
|
|
207
229
|
|
208
230
|
raise NotImplementedError,
|
209
231
|
'Cuprum::CommandFactory is an abstract class. Create a subclass to ' \
|
210
|
-
'define commands for a factory.'
|
232
|
+
'define commands for a factory.'
|
211
233
|
end
|
212
234
|
|
213
235
|
def guard_invalid_definition!(command_class)
|
214
236
|
return if command_class.is_a?(Class) && command_class < Cuprum::Command
|
215
237
|
|
216
|
-
raise ArgumentError, 'definition must be a command class'
|
238
|
+
raise ArgumentError, 'definition must be a command class'
|
217
239
|
end
|
218
240
|
|
219
241
|
def normalize_command_name(command_name)
|
220
|
-
tools.
|
242
|
+
tools.string_tools.underscore(command_name).intern
|
221
243
|
end
|
222
244
|
|
223
245
|
def require_definition!
|
224
|
-
raise ArgumentError, 'must provide a command class or a block'
|
246
|
+
raise ArgumentError, 'must provide a command class or a block'
|
225
247
|
end
|
226
248
|
|
227
249
|
def tools
|
@@ -243,7 +265,7 @@ module Cuprum
|
|
243
265
|
end
|
244
266
|
|
245
267
|
# @private
|
246
|
-
def const_defined?(const_name, inherit = true)
|
268
|
+
def const_defined?(const_name, inherit = true) # rubocop:disable Style/OptionalBooleanParameter
|
247
269
|
command?(const_name) || super
|
248
270
|
end
|
249
271
|
|
@@ -265,8 +287,12 @@ module Cuprum
|
|
265
287
|
|
266
288
|
private
|
267
289
|
|
268
|
-
def build_command(command_class, *args, &block)
|
269
|
-
|
290
|
+
def build_command(command_class, *args, **kwargs, &block)
|
291
|
+
if kwargs.empty?
|
292
|
+
command_class.new(*args, &block)
|
293
|
+
else
|
294
|
+
command_class.new(*args, **kwargs, &block)
|
295
|
+
end
|
270
296
|
end
|
271
297
|
|
272
298
|
def normalize_command_name(command_name)
|
@@ -0,0 +1,79 @@
|
|
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, &block)
|
69
|
+
return self if arguments.empty? && keywords.empty? && block.nil?
|
70
|
+
|
71
|
+
Cuprum::Currying::CurriedCommand.new(
|
72
|
+
arguments: arguments,
|
73
|
+
block: block,
|
74
|
+
command: self,
|
75
|
+
keywords: keywords
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|