cuprum 0.10.0 → 1.0.0.rc.1
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 +60 -2
- data/CODE_OF_CONDUCT.md +132 -0
- data/DEVELOPMENT.md +2 -38
- data/README.md +776 -89
- 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/built_in.rb +3 -1
- data/lib/cuprum/command.rb +29 -58
- data/lib/cuprum/command_factory.rb +7 -5
- data/lib/cuprum/currying/curried_command.rb +11 -4
- data/lib/cuprum/currying.rb +3 -2
- data/lib/cuprum/error.rb +44 -10
- 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/errors.rb +2 -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/match_clause.rb +65 -0
- data/lib/cuprum/matching.rb +232 -0
- data/lib/cuprum/middleware.rb +210 -0
- data/lib/cuprum/operation.rb +17 -15
- data/lib/cuprum/result.rb +1 -3
- data/lib/cuprum/rspec/be_a_result.rb +10 -1
- data/lib/cuprum/rspec/be_a_result_matcher.rb +2 -4
- data/lib/cuprum/rspec/be_callable.rb +14 -0
- data/lib/cuprum/steps.rb +45 -144
- data/lib/cuprum/utils/instance_spy.rb +28 -28
- data/lib/cuprum/utils.rb +3 -1
- data/lib/cuprum/version.rb +15 -12
- data/lib/cuprum.rb +12 -7
- metadata +33 -21
- data/lib/cuprum/chaining.rb +0 -441
@@ -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/built_in.rb
CHANGED
data/lib/cuprum/command.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'cuprum/currying'
|
3
4
|
require 'cuprum/processing'
|
4
5
|
require 'cuprum/steps'
|
@@ -20,14 +21,14 @@ module Cuprum
|
|
20
21
|
# class MultiplyCommand < Cuprum::Command
|
21
22
|
# def initialize multiplier
|
22
23
|
# @multiplier = multiplier
|
23
|
-
# end
|
24
|
+
# end
|
24
25
|
#
|
25
26
|
# private
|
26
27
|
#
|
27
28
|
# def process int
|
28
29
|
# int * @multiplier
|
29
|
-
# end
|
30
|
-
# end
|
30
|
+
# end
|
31
|
+
# end
|
31
32
|
#
|
32
33
|
# triple_command = MultiplyCommand.new(3)
|
33
34
|
# result = command_command.call(5)
|
@@ -38,7 +39,7 @@ module Cuprum
|
|
38
39
|
# class DivideCommand < Cuprum::Command
|
39
40
|
# def initialize divisor
|
40
41
|
# @divisor = divisor
|
41
|
-
# end
|
42
|
+
# end
|
42
43
|
#
|
43
44
|
# private
|
44
45
|
#
|
@@ -48,8 +49,8 @@ module Cuprum
|
|
48
49
|
# end
|
49
50
|
#
|
50
51
|
# int / @divisor
|
51
|
-
# end
|
52
|
-
# end
|
52
|
+
# end
|
53
|
+
# end
|
53
54
|
#
|
54
55
|
# halve_command = DivideCommand.new(2)
|
55
56
|
# result = halve_command.call(10)
|
@@ -63,53 +64,6 @@ module Cuprum
|
|
63
64
|
# result.error #=> 'errors.messages.divide_by_zero'
|
64
65
|
# result.value #=> nil
|
65
66
|
#
|
66
|
-
# @example Command Chaining
|
67
|
-
# class AddCommand < Cuprum::Command
|
68
|
-
# def initialize addend
|
69
|
-
# @addend = addend
|
70
|
-
# end # constructor
|
71
|
-
#
|
72
|
-
# private
|
73
|
-
#
|
74
|
-
# def process int
|
75
|
-
# int + @addend
|
76
|
-
# end # method process
|
77
|
-
# end # class
|
78
|
-
#
|
79
|
-
# double_and_add_one = MultiplyCommand.new(2).chain(AddCommand.new(1))
|
80
|
-
# result = double_and_add_one(5)
|
81
|
-
#
|
82
|
-
# result.value #=> 5
|
83
|
-
#
|
84
|
-
# @example Conditional Chaining
|
85
|
-
# class EvenCommand < Cuprum::Command
|
86
|
-
# private
|
87
|
-
#
|
88
|
-
# def process int
|
89
|
-
# return int if int.even?
|
90
|
-
#
|
91
|
-
# Cuprum::Errors.new(error: 'errors.messages.not_even')
|
92
|
-
# end # method process
|
93
|
-
# end # class
|
94
|
-
#
|
95
|
-
# # The next step in a Collatz sequence is determined as follows:
|
96
|
-
# # - If the number is even, divide it by 2.
|
97
|
-
# # - If the number is odd, multiply it by 3 and add 1.
|
98
|
-
# collatz_command =
|
99
|
-
# EvenCommand.new.
|
100
|
-
# chain(DivideCommand.new(2), :on => :success).
|
101
|
-
# chain(
|
102
|
-
# MultiplyCommand.new(3).chain(AddCommand.new(1),
|
103
|
-
# :on => :failure
|
104
|
-
# )
|
105
|
-
#
|
106
|
-
# result = collatz_command.new(5)
|
107
|
-
# result.value #=> 16
|
108
|
-
#
|
109
|
-
# result = collatz_command.new(16)
|
110
|
-
# result.value #=> 8
|
111
|
-
#
|
112
|
-
# @see Cuprum::Chaining
|
113
67
|
# @see Cuprum::Processing
|
114
68
|
class Command
|
115
69
|
include Cuprum::Processing
|
@@ -122,16 +76,33 @@ module Cuprum
|
|
122
76
|
# #call method will wrap the block and set the result #value to the return
|
123
77
|
# value of the block. This overrides the implementation in #process, if
|
124
78
|
# any.
|
125
|
-
def initialize
|
79
|
+
def initialize(&implementation)
|
126
80
|
return unless implementation
|
127
81
|
|
128
82
|
define_singleton_method :process, &implementation
|
129
83
|
|
130
84
|
singleton_class.send(:private, :process)
|
131
|
-
end
|
85
|
+
end
|
132
86
|
|
87
|
+
# (see Cuprum::Processing#call)
|
133
88
|
def call(*args, **kwargs, &block)
|
134
89
|
steps { super }
|
135
90
|
end
|
136
|
-
|
137
|
-
|
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'
|
@@ -147,7 +149,7 @@ 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
|
|
@@ -227,13 +229,13 @@ module Cuprum
|
|
227
229
|
|
228
230
|
raise NotImplementedError,
|
229
231
|
'Cuprum::CommandFactory is an abstract class. Create a subclass to ' \
|
230
|
-
'define commands for a factory.'
|
232
|
+
'define commands for a factory.'
|
231
233
|
end
|
232
234
|
|
233
235
|
def guard_invalid_definition!(command_class)
|
234
236
|
return if command_class.is_a?(Class) && command_class < Cuprum::Command
|
235
237
|
|
236
|
-
raise ArgumentError, 'definition must be a command class'
|
238
|
+
raise ArgumentError, 'definition must be a command class'
|
237
239
|
end
|
238
240
|
|
239
241
|
def normalize_command_name(command_name)
|
@@ -241,7 +243,7 @@ module Cuprum
|
|
241
243
|
end
|
242
244
|
|
243
245
|
def require_definition!
|
244
|
-
raise ArgumentError, 'must provide a command class or a block'
|
246
|
+
raise ArgumentError, 'must provide a command class or a block'
|
245
247
|
end
|
246
248
|
|
247
249
|
def tools
|
@@ -263,7 +265,7 @@ module Cuprum
|
|
263
265
|
end
|
264
266
|
|
265
267
|
# @private
|
266
|
-
def const_defined?(const_name, inherit = true)
|
268
|
+
def const_defined?(const_name, inherit = true) # rubocop:disable Style/OptionalBooleanParameter
|
267
269
|
command?(const_name) || super
|
268
270
|
end
|
269
271
|
|
@@ -56,8 +56,12 @@ module Cuprum::Currying
|
|
56
56
|
# @param arguments [Array] The arguments to pass to the curried command.
|
57
57
|
# @param command [Cuprum::Command] The original command to curry.
|
58
58
|
# @param keywords [Hash] The keywords to pass to the curried command.
|
59
|
-
|
59
|
+
# @yield A block to pass to the curried command.
|
60
|
+
def initialize(command:, arguments: [], block: nil, keywords: {})
|
61
|
+
super()
|
62
|
+
|
60
63
|
@arguments = arguments
|
64
|
+
@block = block
|
61
65
|
@command = command
|
62
66
|
@keywords = keywords
|
63
67
|
end
|
@@ -87,6 +91,9 @@ module Cuprum::Currying
|
|
87
91
|
# @return [Array] the arguments to pass to the curried command.
|
88
92
|
attr_reader :arguments
|
89
93
|
|
94
|
+
# @return [Proc, nil] a block to pass to the curried command.
|
95
|
+
attr_reader :block
|
96
|
+
|
90
97
|
# @return [Cuprum::Command] the original command to curry.
|
91
98
|
attr_reader :command
|
92
99
|
|
@@ -95,14 +102,14 @@ module Cuprum::Currying
|
|
95
102
|
|
96
103
|
private
|
97
104
|
|
98
|
-
def process(*args, **kwargs, &
|
105
|
+
def process(*args, **kwargs, &override)
|
99
106
|
args = [*arguments, *args]
|
100
107
|
kwargs = keywords.merge(kwargs)
|
101
108
|
|
102
109
|
if kwargs.empty?
|
103
|
-
command.call(*args, &block)
|
110
|
+
command.call(*args, &(override || block))
|
104
111
|
else
|
105
|
-
command.call(*args, **kwargs, &block)
|
112
|
+
command.call(*args, **kwargs, &(override || block))
|
106
113
|
end
|
107
114
|
end
|
108
115
|
end
|
data/lib/cuprum/currying.rb
CHANGED
@@ -65,11 +65,12 @@ module Cuprum
|
|
65
65
|
# @return [Cuprum::Currying::CurriedCommand] the curried command.
|
66
66
|
#
|
67
67
|
# @see Cuprum::Currying::CurriedCommand#call
|
68
|
-
def curry(*arguments, **keywords)
|
69
|
-
return self if arguments.empty? && keywords.empty?
|
68
|
+
def curry(*arguments, **keywords, &block)
|
69
|
+
return self if arguments.empty? && keywords.empty? && block.nil?
|
70
70
|
|
71
71
|
Cuprum::Currying::CurriedCommand.new(
|
72
72
|
arguments: arguments,
|
73
|
+
block: block,
|
73
74
|
command: self,
|
74
75
|
keywords: keywords
|
75
76
|
)
|
data/lib/cuprum/error.rb
CHANGED
@@ -4,34 +4,68 @@ require 'cuprum'
|
|
4
4
|
|
5
5
|
module Cuprum
|
6
6
|
# Wrapper class for encapsulating an error state for a failed Cuprum result.
|
7
|
+
#
|
7
8
|
# Additional details can be passed by setting the #message or by using a
|
8
9
|
# subclass of Cuprum::Error.
|
9
10
|
class Error
|
10
|
-
|
11
|
-
|
11
|
+
# Short string used to identify the type of error.
|
12
|
+
#
|
13
|
+
# Primarily used for serialization. This value can be overriden by passing
|
14
|
+
# in the :type parameter to the constructor.
|
15
|
+
#
|
16
|
+
# Subclasses of Cuprum::Error should define their own default TYPE constant.
|
17
|
+
TYPE = 'cuprum.error'
|
12
18
|
|
13
19
|
# @param message [String] Optional message describing the nature of the
|
14
20
|
# error.
|
15
|
-
|
16
|
-
|
21
|
+
# @param properties [Hash] Additional properties used to compare errors.
|
22
|
+
# @param type [String] Short string used to identify the type of error.
|
23
|
+
def initialize(message: nil, type: nil, **properties)
|
24
|
+
@message = message
|
25
|
+
@type = type || self.class::TYPE
|
26
|
+
@comparable_properties = properties.merge(message: message, type: type)
|
17
27
|
end
|
18
28
|
|
19
|
-
# @return [String]
|
29
|
+
# @return [String] optional message describing the nature of the error.
|
20
30
|
attr_reader :message
|
21
31
|
|
32
|
+
# @return [String] short string used to identify the type of error.
|
33
|
+
attr_reader :type
|
34
|
+
|
22
35
|
# @param other [Cuprum::Error] The other object to compare.
|
23
36
|
#
|
24
|
-
# @return [Boolean] true if the other object has the same class and
|
25
|
-
# otherwise false.
|
37
|
+
# @return [Boolean] true if the other object has the same class and
|
38
|
+
# properties; otherwise false.
|
26
39
|
def ==(other)
|
27
40
|
other.instance_of?(self.class) &&
|
28
|
-
comparable_properties
|
41
|
+
other.comparable_properties == comparable_properties
|
42
|
+
end
|
43
|
+
|
44
|
+
# Generates a serializable representation of the error object.
|
45
|
+
#
|
46
|
+
# By default, contains the #type and #message properties and an empty :data
|
47
|
+
# Hash. This can be overriden in subclasses by overriding the private method
|
48
|
+
# #as_json_data; this should always return a Hash with String keys and whose
|
49
|
+
# values are basic objects or data structures of the same.
|
50
|
+
#
|
51
|
+
# @return [Hash<String, Object>] a serializable hash representation of the
|
52
|
+
# error.
|
53
|
+
def as_json
|
54
|
+
{
|
55
|
+
'data' => as_json_data,
|
56
|
+
'message' => message,
|
57
|
+
'type' => type
|
58
|
+
}
|
29
59
|
end
|
30
60
|
|
61
|
+
protected
|
62
|
+
|
63
|
+
attr_reader :comparable_properties
|
64
|
+
|
31
65
|
private
|
32
66
|
|
33
|
-
def
|
34
|
-
|
67
|
+
def as_json_data
|
68
|
+
{}
|
35
69
|
end
|
36
70
|
end
|
37
71
|
end
|
@@ -13,6 +13,9 @@ module Cuprum::Errors
|
|
13
13
|
# Format for generating error message.
|
14
14
|
MESSAGE_FORMAT = 'no implementation defined for %s'
|
15
15
|
|
16
|
+
# Short string used to identify the type of error.
|
17
|
+
TYPE = 'cuprum.errors.command_not_implemented'
|
18
|
+
|
16
19
|
# @param command [Cuprum::Command] The command called without a definition.
|
17
20
|
def initialize(command:)
|
18
21
|
@command = command
|
@@ -20,7 +23,7 @@ module Cuprum::Errors
|
|
20
23
|
class_name = command&.class&.name || 'command'
|
21
24
|
message = MESSAGE_FORMAT % class_name
|
22
25
|
|
23
|
-
super(message: message)
|
26
|
+
super(command: command, message: message)
|
24
27
|
end
|
25
28
|
|
26
29
|
# @return [Cuprum::Command] The command called without a definition.
|
@@ -28,8 +31,8 @@ module Cuprum::Errors
|
|
28
31
|
|
29
32
|
private
|
30
33
|
|
31
|
-
def
|
32
|
-
|
34
|
+
def as_json_data
|
35
|
+
command ? { 'class_name' => command.class.name } : {}
|
33
36
|
end
|
34
37
|
end
|
35
38
|
end
|
@@ -7,12 +7,12 @@ module Cuprum::Errors
|
|
7
7
|
# Error class to be used when trying to access the result of an uncalled
|
8
8
|
# Operation.
|
9
9
|
class OperationNotCalled < Cuprum::Error
|
10
|
-
COMPARABLE_PROPERTIES = %i[operation].freeze
|
11
|
-
private_constant :COMPARABLE_PROPERTIES
|
12
|
-
|
13
10
|
# Format for generating error message.
|
14
11
|
MESSAGE_FORMAT = '%s was not called and does not have a result'
|
15
12
|
|
13
|
+
# Short string used to identify the type of error.
|
14
|
+
TYPE = 'cuprum.errors.operation_not_called'
|
15
|
+
|
16
16
|
# @param operation [Cuprum::Operation] The uncalled operation.
|
17
17
|
def initialize(operation:)
|
18
18
|
@operation = operation
|
@@ -20,7 +20,7 @@ module Cuprum::Errors
|
|
20
20
|
class_name = operation&.class&.name || 'operation'
|
21
21
|
message = MESSAGE_FORMAT % class_name
|
22
22
|
|
23
|
-
super(message: message)
|
23
|
+
super(message: message, operation: operation)
|
24
24
|
end
|
25
25
|
|
26
26
|
# @return [Cuprum::Operation] The uncalled operation.
|
@@ -28,8 +28,8 @@ module Cuprum::Errors
|
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
|
-
def
|
32
|
-
|
31
|
+
def as_json_data
|
32
|
+
operation ? { 'class_name' => operation.class.name } : {}
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/error'
|
4
|
+
require 'cuprum/errors'
|
5
|
+
|
6
|
+
module Cuprum::Errors
|
7
|
+
# An error returned when a command encounters an unhandled exception.
|
8
|
+
class UncaughtException < Cuprum::Error
|
9
|
+
# Short string used to identify the type of error.
|
10
|
+
TYPE = 'cuprum.collections.errors.uncaught_exception'
|
11
|
+
|
12
|
+
# @param exception [StandardError] The exception that was raised.
|
13
|
+
# @param message [String] A message to display. Will be annotated with
|
14
|
+
# details on the exception and the exception's cause (if any).
|
15
|
+
def initialize(exception:, message: 'uncaught exception')
|
16
|
+
@exception = exception
|
17
|
+
@cause = exception.cause
|
18
|
+
|
19
|
+
super(message: generate_message(message))
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [StandardError] the exception that was raised.
|
23
|
+
attr_reader :exception
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :cause
|
28
|
+
|
29
|
+
def as_json_data # rubocop:disable Metrics/MethodLength
|
30
|
+
data = {
|
31
|
+
'exception_backtrace' => exception.backtrace,
|
32
|
+
'exception_class' => exception.class,
|
33
|
+
'exception_message' => exception.message
|
34
|
+
}
|
35
|
+
|
36
|
+
return data unless cause
|
37
|
+
|
38
|
+
data.update(
|
39
|
+
{
|
40
|
+
'cause_backtrace' => cause.backtrace,
|
41
|
+
'cause_class' => cause.class,
|
42
|
+
'cause_message' => cause.message
|
43
|
+
}
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_message(message)
|
48
|
+
message = "#{message} #{exception.class}: #{exception.message}"
|
49
|
+
|
50
|
+
return message unless cause
|
51
|
+
|
52
|
+
message + " caused by #{cause.class}: #{cause.message}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/cuprum/errors.rb
CHANGED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum/errors/uncaught_exception'
|
4
|
+
|
5
|
+
module Cuprum
|
6
|
+
# Utility class for handling uncaught exceptions in commands.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class UnsafeCommand < Cuprum::Command
|
10
|
+
# private
|
11
|
+
#
|
12
|
+
# def process
|
13
|
+
# raise 'Something went wrong.'
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# class SafeCommand < UnsafeCommand
|
18
|
+
# include Cuprum::ExceptionHandling
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# UnsafeCommand.new.call
|
22
|
+
# #=> raises a StandardError
|
23
|
+
#
|
24
|
+
# result = SafeCommand.new.call
|
25
|
+
# #=> a Cuprum::Result
|
26
|
+
# result.error
|
27
|
+
# #=> a Cuprum::Errors::UncaughtException error.
|
28
|
+
# result.error.message
|
29
|
+
# #=> 'uncaught exception in SafeCommand -' \
|
30
|
+
# ' StandardError: Something went wrong.'
|
31
|
+
module ExceptionHandling
|
32
|
+
# Wraps the #call method with a rescue clause matching any StandardError.
|
33
|
+
#
|
34
|
+
# If a StandardError or subclass thereof is raised and not caught by #call,
|
35
|
+
# then ExceptionHandling will rescue the exception and return a failing
|
36
|
+
# Cuprum::Result with a Cuprum::Errors::UncaughtException error.
|
37
|
+
#
|
38
|
+
# @return [Cuprum::Result] the result of calling the superclass method, or
|
39
|
+
# a failing result if a StandardError is raised.
|
40
|
+
def call(*args, **kwargs, &block)
|
41
|
+
super
|
42
|
+
rescue StandardError => exception
|
43
|
+
error = Cuprum::Errors::UncaughtException.new(
|
44
|
+
exception: exception,
|
45
|
+
message: "uncaught exception in #{self.class.name} - "
|
46
|
+
)
|
47
|
+
failure(error)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum'
|
4
|
+
require 'cuprum/matching'
|
5
|
+
|
6
|
+
module Cuprum
|
7
|
+
# Provides result matching based on result status, error, and value.
|
8
|
+
#
|
9
|
+
# First, define match clauses using the .match DSL. Each match clause has a
|
10
|
+
# status and optionally a value class and/or error class. A result will only
|
11
|
+
# match the clause if the result status is the same as the clause's status.
|
12
|
+
# If the clause sets a value class, then the result value must be an instance
|
13
|
+
# of that class (or an instance of a subclass). If the clause sets an error
|
14
|
+
# class, then the result error must be an instance of that class (or an
|
15
|
+
# instance of a subclass).
|
16
|
+
#
|
17
|
+
# Once the matcher defines one or more match clauses, call #call with a result
|
18
|
+
# to match the result. The matcher will determine the best match with the same
|
19
|
+
# status (value and error match the result, only value or error match, or just
|
20
|
+
# status matches) and then call the match clause with the result. If no match
|
21
|
+
# clauses match the result, the matcher will instead raise a
|
22
|
+
# Cuprum::Matching::NoMatchError.
|
23
|
+
#
|
24
|
+
# @example Matching A Status
|
25
|
+
# matcher = Cuprum::Matcher.new do
|
26
|
+
# match(:failure) { 'Something went wrong' }
|
27
|
+
#
|
28
|
+
# match(:success) { 'Ok' }
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# matcher.call(Cuprum::Result.new(status: :failure))
|
32
|
+
# #=> 'Something went wrong'
|
33
|
+
#
|
34
|
+
# matcher.call(Cuprum::Result.new(status: :success))
|
35
|
+
# #=> 'Ok'
|
36
|
+
#
|
37
|
+
# @example Matching An Error
|
38
|
+
# matcher = Cuprum::Matcher.new do
|
39
|
+
# match(:failure) { 'Something went wrong' }
|
40
|
+
#
|
41
|
+
# match(:failure, error: CustomError) { |result| result.error.message }
|
42
|
+
#
|
43
|
+
# match(:success) { 'Ok' }
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# matcher.call(Cuprum::Result.new(status: :failure))
|
47
|
+
# #=> 'Something went wrong'
|
48
|
+
#
|
49
|
+
# error = CustomError.new(message: 'The magic smoke is escaping.')
|
50
|
+
# matcher.call(Cuprum::Result.new(error: error))
|
51
|
+
# #=> 'The magic smoke is escaping.'
|
52
|
+
#
|
53
|
+
# @example Using A Match Context
|
54
|
+
# context = Struct.new(:name).new('programs')
|
55
|
+
# matcher = Cuprum::Matcher.new(context) do
|
56
|
+
# match(:failure) { 'Something went wrong' }
|
57
|
+
#
|
58
|
+
# match(:success) { "Greetings, #{name}!" }
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# matcher.call(Cuprum::Result.new(status: :success)
|
62
|
+
# #=> 'Greetings, programs!'
|
63
|
+
class Matcher
|
64
|
+
include Cuprum::Matching
|
65
|
+
|
66
|
+
# @param match_context [Object] the execution context for a matching clause.
|
67
|
+
#
|
68
|
+
# @yield Executes the block in the context of the singleton class. This is
|
69
|
+
# used to define match clauses when instantiating a Matcher instance.
|
70
|
+
def initialize(match_context = nil, &block)
|
71
|
+
@match_context = match_context
|
72
|
+
|
73
|
+
singleton_class.instance_exec(&block) if block_given?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns a copy of the matcher with the given execution context.
|
77
|
+
#
|
78
|
+
# @param match_context [Object] the execution context for a matching clause.
|
79
|
+
#
|
80
|
+
# @return [Cuprum::Matcher] the copied matcher.
|
81
|
+
def with_context(match_context)
|
82
|
+
clone.tap { |copy| copy.match_context = match_context }
|
83
|
+
end
|
84
|
+
alias_method :using_context, :with_context
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
attr_writer :match_context
|
89
|
+
end
|
90
|
+
end
|