cuprum 0.10.0 → 0.11.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 +52 -2
- data/DEVELOPMENT.md +4 -21
- data/README.md +487 -95
- data/lib/cuprum.rb +12 -7
- 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 +29 -58
- data/lib/cuprum/command_factory.rb +7 -5
- data/lib/cuprum/currying.rb +3 -2
- data/lib/cuprum/currying/curried_command.rb +11 -4
- 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/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 +47 -89
- data/lib/cuprum/utils.rb +3 -1
- data/lib/cuprum/utils/instance_spy.rb +28 -28
- data/lib/cuprum/version.rb +14 -11
- metadata +32 -21
- data/lib/cuprum/chaining.rb +0 -441
@@ -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
|
@@ -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
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum'
|
4
|
+
|
5
|
+
module Cuprum
|
6
|
+
# Handles matching a result against an ordered list of matchers.
|
7
|
+
#
|
8
|
+
# A MatcherList should be used when you have a series of matchers with a
|
9
|
+
# defined priority ordering. Within that ordering, the list will check for the
|
10
|
+
# most specific matching clause in each of the matchers. A clause matching
|
11
|
+
# both the value and error will match first, followed by a clause matching
|
12
|
+
# only the result value or error, and finally a clause matching only the
|
13
|
+
# result status will match. If none of the matchers have a clause that matches
|
14
|
+
# the result, a Cuprum::Matching::NoMatchError will be raised.
|
15
|
+
#
|
16
|
+
# @example Using A MatcherList
|
17
|
+
# generic_matcher = Cuprum::Matcher.new do
|
18
|
+
# match(:failure) { 'generic failure' }
|
19
|
+
#
|
20
|
+
# match(:failure, error: CustomError) { 'custom failure' }
|
21
|
+
# end
|
22
|
+
# specific_matcher = Cuprum::Matcher.new do
|
23
|
+
# match(:failure, error: Cuprum::Error) { 'specific failure' }
|
24
|
+
# end
|
25
|
+
# matcher_list = Cuprum::MatcherList.new(
|
26
|
+
# [
|
27
|
+
# specific_matcher,
|
28
|
+
# generic_matcher
|
29
|
+
# ]
|
30
|
+
# )
|
31
|
+
#
|
32
|
+
# # A failure without an error does not match the first matcher, so the
|
33
|
+
# # matcher list continues on to the next matcher in the list.
|
34
|
+
# result = Cuprum::Result.new(status: :failure)
|
35
|
+
# matcher_list.call(result)
|
36
|
+
# #=> 'generic failure'
|
37
|
+
#
|
38
|
+
# # A failure with an error matches the first matcher.
|
39
|
+
# error = Cuprum::Error.new(message: 'Something went wrong.')
|
40
|
+
# result = Cuprum::Result.new(error: error)
|
41
|
+
# matcher_list.call(result)
|
42
|
+
# #=> 'specific failure'
|
43
|
+
#
|
44
|
+
# # A failure with an error subclass still matches the first matcher, even
|
45
|
+
# # though the second matcher has a more exact match.
|
46
|
+
# error = CustomError.new(message: 'The magic smoke is escaping.')
|
47
|
+
# result = Cuprum::Result.new(error: error)
|
48
|
+
# matcher_list.call(result)
|
49
|
+
# #=> 'specific failure'
|
50
|
+
class MatcherList
|
51
|
+
# @param matchers [Array<Cuprum::Matching>] The matchers to match against a
|
52
|
+
# result, in order of descending priority.
|
53
|
+
def initialize(matchers)
|
54
|
+
@matchers = matchers
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<Cuprum::Matching>] the matchers to match against a result.
|
58
|
+
attr_reader :matchers
|
59
|
+
|
60
|
+
# Finds and executes the best matching clause from the ordered matchers.
|
61
|
+
#
|
62
|
+
# When given a result, the matcher list will check through each of the
|
63
|
+
# matchers in the order they were given for match clauses that match the
|
64
|
+
# result. Each matcher is checked for a clause that matches the status,
|
65
|
+
# error, and value of the result. If no matching clause is found, the
|
66
|
+
# matchers are then checked for a clause matching the status and either the
|
67
|
+
# error or value of the result. Finally, if there are still no matching
|
68
|
+
# clauses, the matchers are checked for a clause that matches the result
|
69
|
+
# status.
|
70
|
+
#
|
71
|
+
# Once a matching clause is found, that clause is then called with the
|
72
|
+
# given result.
|
73
|
+
#
|
74
|
+
# If none of the matchers have a clause that matches the result, a
|
75
|
+
# Cuprum::Matching::NoMatchError will be raised.
|
76
|
+
#
|
77
|
+
# @param result [Cuprum::Result] The result to match.
|
78
|
+
#
|
79
|
+
# @raise Cuprum::Matching::NoMatchError if none of the matchers match the
|
80
|
+
# given result.
|
81
|
+
#
|
82
|
+
# @see Cuprum::Matcher#call.
|
83
|
+
def call(result)
|
84
|
+
unless result.respond_to?(:to_cuprum_result)
|
85
|
+
raise ArgumentError, 'result must be a Cuprum::Result'
|
86
|
+
end
|
87
|
+
|
88
|
+
result = result.to_cuprum_result
|
89
|
+
matcher = matcher_for(result)
|
90
|
+
|
91
|
+
return matcher.call(result) if matcher
|
92
|
+
|
93
|
+
raise Cuprum::Matching::NoMatchError,
|
94
|
+
"no match found for #{result.inspect}"
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def error_match?(matcher:, result:)
|
100
|
+
matcher.matches?(
|
101
|
+
result.status,
|
102
|
+
error: result.error&.class
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def exact_match?(matcher:, result:)
|
107
|
+
matcher.matches?(
|
108
|
+
result.status,
|
109
|
+
error: result.error&.class,
|
110
|
+
value: result.value&.class
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def find_exact_match(result)
|
115
|
+
matchers.find do |matcher|
|
116
|
+
exact_match?(matcher: matcher, result: result)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def find_generic_match(result)
|
121
|
+
matchers.find do |matcher|
|
122
|
+
generic_match?(matcher: matcher, result: result)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_partial_match(result)
|
127
|
+
matchers.find do |matcher|
|
128
|
+
error_match?(matcher: matcher, result: result) ||
|
129
|
+
value_match?(matcher: matcher, result: result)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def generic_match?(matcher:, result:)
|
134
|
+
matcher.matches?(result.status)
|
135
|
+
end
|
136
|
+
|
137
|
+
def matcher_for(result)
|
138
|
+
find_exact_match(result) ||
|
139
|
+
find_partial_match(result) ||
|
140
|
+
find_generic_match(result)
|
141
|
+
end
|
142
|
+
|
143
|
+
def value_match?(matcher:, result:)
|
144
|
+
matcher.matches?(
|
145
|
+
result.status,
|
146
|
+
value: result.value&.class
|
147
|
+
)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuprum'
|
4
|
+
|
5
|
+
module Cuprum
|
6
|
+
# Implements result matching based on result status, error, and value.
|
7
|
+
#
|
8
|
+
# @see Cuprum::Matcher.
|
9
|
+
module Matching
|
10
|
+
autoload :MatchClause, 'cuprum/matching/match_clause'
|
11
|
+
|
12
|
+
# Class methods extend-ed into a class when the module is included.
|
13
|
+
module ClassMethods
|
14
|
+
# Defines a match clause for the matcher.
|
15
|
+
#
|
16
|
+
# @param status [Symbol] The status to match. The clause will match a
|
17
|
+
# result only if the result has the same status as the match clause.
|
18
|
+
# @param error [Class] The type of error to match. If given, the clause
|
19
|
+
# will match a result only if the result error is an instance of the
|
20
|
+
# given class, or an instance of a subclass.
|
21
|
+
# @param value [Class] The type of value to match. If given, the clause
|
22
|
+
# will match a result only if the result value is an instance of the
|
23
|
+
# given class, or an instance of a subclass.
|
24
|
+
#
|
25
|
+
# @yield The code to execute on a successful match.
|
26
|
+
# @yieldparam result [Cuprum::Result] The matched result.
|
27
|
+
def match(status, error: nil, value: nil, &block)
|
28
|
+
validate_status!(status)
|
29
|
+
validate_error!(error)
|
30
|
+
validate_value!(value)
|
31
|
+
|
32
|
+
clause = MatchClause.new(block, error, status, value)
|
33
|
+
clauses = match_clauses[status]
|
34
|
+
index = clauses.bsearch_index { |item| clause <= item } || -1
|
35
|
+
|
36
|
+
# Clauses are sorted from most specific to least specific.
|
37
|
+
clauses.insert(index, clause)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @private
|
41
|
+
def match_result(result:)
|
42
|
+
status_clauses(result.status).find do |clause|
|
43
|
+
clause.matches_result?(result: result)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @private
|
48
|
+
def matches_result?(result:)
|
49
|
+
status_clauses(result.status).reverse_each.any? do |clause|
|
50
|
+
clause.matches_result?(result: result)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @private
|
55
|
+
def matches_status?(error:, status:, value:)
|
56
|
+
status_clauses(status).reverse_each.any? do |clause|
|
57
|
+
clause.matches_details?(error: error, value: value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def match_clauses
|
64
|
+
@match_clauses ||= Hash.new { |hsh, key| hsh[key] = [] }
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def status_clauses(status)
|
70
|
+
ancestors
|
71
|
+
.select { |ancestor| ancestor < Cuprum::Matching }
|
72
|
+
.map { |ancestor| ancestor.match_clauses[status] }
|
73
|
+
.reduce([], &:concat)
|
74
|
+
.sort
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_error!(error)
|
78
|
+
return if error.nil? || error.is_a?(Module)
|
79
|
+
|
80
|
+
raise ArgumentError,
|
81
|
+
'error must be a Class or Module',
|
82
|
+
caller(1..-1)
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_status!(status)
|
86
|
+
if status.nil? || status.to_s.empty?
|
87
|
+
raise ArgumentError, "status can't be blank", caller(1..-1)
|
88
|
+
end
|
89
|
+
|
90
|
+
return if status.is_a?(Symbol)
|
91
|
+
|
92
|
+
raise ArgumentError, 'status must be a Symbol', caller(1..-1)
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_value!(value)
|
96
|
+
return if value.nil? || value.is_a?(Module)
|
97
|
+
|
98
|
+
raise ArgumentError,
|
99
|
+
'value must be a Class or Module',
|
100
|
+
caller(1..-1)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Exception raised when the matcher does not match a result.
|
105
|
+
class NoMatchError < StandardError; end
|
106
|
+
|
107
|
+
class << self
|
108
|
+
private
|
109
|
+
|
110
|
+
def included(other)
|
111
|
+
super
|
112
|
+
|
113
|
+
other.extend(ClassMethods)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Object, nil] the execution context for a matching clause.
|
118
|
+
attr_reader :match_context
|
119
|
+
|
120
|
+
# Finds the match clause matching the result and calls the stored block.
|
121
|
+
#
|
122
|
+
# Match clauses are defined using the .match DSL. When a result is matched,
|
123
|
+
# the defined clauses matching the result status are checked in descending
|
124
|
+
# order of specificity:
|
125
|
+
#
|
126
|
+
# - Clauses that expect both a value and an error.
|
127
|
+
# - Clauses that expect a value.
|
128
|
+
# - Clauses that expect an error.
|
129
|
+
# - Clauses that do not expect a value or an error.
|
130
|
+
#
|
131
|
+
# If there are multiple clauses that expect a value or an error, they are
|
132
|
+
# sorted by inheritance - a clause with a subclass value or error is checked
|
133
|
+
# before the clause with the parent class.
|
134
|
+
#
|
135
|
+
# Using that ordering, each potential clause is checked for a match with the
|
136
|
+
# result. If the clause defines a value, then the result will match the
|
137
|
+
# clause only if the result value is an instance of the expected value (or
|
138
|
+
# an instance of a subclass). Likewise, if the clause defines an error, then
|
139
|
+
# the result will match the clause only if the result error is an instance
|
140
|
+
# of the expected error class (or an instance of a subclass). Clauses that
|
141
|
+
# do not define either a value nor an error will match with any result with
|
142
|
+
# the same status, but as the least specific are always matched last.
|
143
|
+
#
|
144
|
+
# Matchers can also inherit clauses from a parent class or from an included
|
145
|
+
# module. Inherited or included clauses are checked after clauses defined on
|
146
|
+
# the matcher itself, so the matcher can override generic matches with more
|
147
|
+
# specific functionality.
|
148
|
+
#
|
149
|
+
# Finally, once the most specific matching clause is found, #call will
|
150
|
+
# call the block used to define the clause. If the block takes at least one
|
151
|
+
# argument, the result will be passed to the block; otherwise, it will be
|
152
|
+
# called with no parameters. If there is no clause matching the result,
|
153
|
+
# #call will instead raise a Cuprum::Matching::NoMatchError.
|
154
|
+
#
|
155
|
+
# The match clause is executed in the context of the matcher object. This
|
156
|
+
# allows instance methods defined for the matcher to be called as part of
|
157
|
+
# the match clause block. If the matcher defines a non-nil
|
158
|
+
# #matching_context, the block is instead executed in the context of the
|
159
|
+
# matching_context using #instance_exec.
|
160
|
+
#
|
161
|
+
# @param result [Cuprum::Result] The result to match.
|
162
|
+
#
|
163
|
+
# @return [Object] the value returned by the stored block.
|
164
|
+
#
|
165
|
+
# @raise [NoMatchError] if there is no clause matching the result.
|
166
|
+
#
|
167
|
+
# @see ClassMethods::match
|
168
|
+
# @see #match_context
|
169
|
+
def call(result)
|
170
|
+
unless result.respond_to?(:to_cuprum_result)
|
171
|
+
raise ArgumentError, 'result must be a Cuprum::Result'
|
172
|
+
end
|
173
|
+
|
174
|
+
result = result.to_cuprum_result
|
175
|
+
clause = singleton_class.match_result(result: result)
|
176
|
+
|
177
|
+
raise NoMatchError, "no match found for #{result.inspect}" if clause.nil?
|
178
|
+
|
179
|
+
call_match(block: clause.block, result: result)
|
180
|
+
end
|
181
|
+
|
182
|
+
# @return [Boolean] true if an execution context is defined for a matching
|
183
|
+
# clause; otherwise false.
|
184
|
+
def match_context?
|
185
|
+
!match_context.nil?
|
186
|
+
end
|
187
|
+
|
188
|
+
# @overload matches?(result)
|
189
|
+
# Checks if the matcher has any match clauses that match the given result.
|
190
|
+
#
|
191
|
+
# @param result [Cuprum::Result] The result to match.
|
192
|
+
#
|
193
|
+
# @return [Boolean] true if the matcher has at least one match clause that
|
194
|
+
# matches the result; otherwise false.
|
195
|
+
#
|
196
|
+
# @overload matches?(status, error: nil, value: nil)
|
197
|
+
# Checks if the matcher has any clauses matching the status and details.
|
198
|
+
#
|
199
|
+
# @param status [Symbol] The status to match.
|
200
|
+
# @param error [Class, nil] The class of error to match, if any.
|
201
|
+
# @param value [Class, nil] The class of value to match, if any.
|
202
|
+
#
|
203
|
+
# @return [Boolean] true if the matcher has at least one match clause that
|
204
|
+
# matches the status and details; otherwise false.
|
205
|
+
def matches?(result_or_status, error: nil, value: nil) # rubocop:disable Metrics/MethodLength
|
206
|
+
if result_or_status.respond_to?(:to_cuprum_result)
|
207
|
+
raise ArgumentError, 'error defined by result' unless error.nil?
|
208
|
+
raise ArgumentError, 'value defined by result' unless value.nil?
|
209
|
+
|
210
|
+
return singleton_class.matches_result?(
|
211
|
+
result: result_or_status.to_cuprum_result
|
212
|
+
)
|
213
|
+
elsif result_or_status.is_a?(Symbol)
|
214
|
+
return singleton_class.matches_status?(
|
215
|
+
error: error,
|
216
|
+
status: result_or_status,
|
217
|
+
value: value
|
218
|
+
)
|
219
|
+
end
|
220
|
+
|
221
|
+
raise ArgumentError, 'argument must be a result or a status'
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def call_match(block:, result:)
|
227
|
+
args = block.arity.zero? ? [] : [result]
|
228
|
+
|
229
|
+
(match_context || self).instance_exec(*args, &block)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|