cuprum 0.10.0 → 1.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|