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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/DEVELOPMENT.md +42 -53
  4. data/README.md +728 -536
  5. data/lib/cuprum.rb +12 -6
  6. data/lib/cuprum/built_in.rb +3 -1
  7. data/lib/cuprum/built_in/identity_command.rb +6 -4
  8. data/lib/cuprum/built_in/identity_operation.rb +4 -2
  9. data/lib/cuprum/built_in/null_command.rb +5 -3
  10. data/lib/cuprum/built_in/null_operation.rb +4 -2
  11. data/lib/cuprum/command.rb +37 -59
  12. data/lib/cuprum/command_factory.rb +50 -24
  13. data/lib/cuprum/currying.rb +79 -0
  14. data/lib/cuprum/currying/curried_command.rb +116 -0
  15. data/lib/cuprum/error.rb +44 -10
  16. data/lib/cuprum/errors.rb +2 -0
  17. data/lib/cuprum/errors/command_not_implemented.rb +6 -3
  18. data/lib/cuprum/errors/operation_not_called.rb +6 -6
  19. data/lib/cuprum/errors/uncaught_exception.rb +55 -0
  20. data/lib/cuprum/exception_handling.rb +50 -0
  21. data/lib/cuprum/matcher.rb +90 -0
  22. data/lib/cuprum/matcher_list.rb +150 -0
  23. data/lib/cuprum/matching.rb +232 -0
  24. data/lib/cuprum/matching/match_clause.rb +65 -0
  25. data/lib/cuprum/middleware.rb +210 -0
  26. data/lib/cuprum/operation.rb +17 -15
  27. data/lib/cuprum/processing.rb +10 -14
  28. data/lib/cuprum/result.rb +2 -4
  29. data/lib/cuprum/result_helpers.rb +22 -0
  30. data/lib/cuprum/rspec/be_a_result.rb +10 -1
  31. data/lib/cuprum/rspec/be_a_result_matcher.rb +5 -7
  32. data/lib/cuprum/rspec/be_callable.rb +14 -0
  33. data/lib/cuprum/steps.rb +233 -0
  34. data/lib/cuprum/utils.rb +3 -1
  35. data/lib/cuprum/utils/instance_spy.rb +37 -30
  36. data/lib/cuprum/version.rb +13 -10
  37. metadata +34 -19
  38. data/lib/cuprum/chaining.rb +0 -420
@@ -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
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/matching'
4
+
5
+ module Cuprum::Matching
6
+ # @private
7
+ #
8
+ # Value object that represents a potential result match for a Matcher.
9
+ #
10
+ # Should not be instantiated directly; instead, instantiate a Cuprum::Matcher
11
+ # or include Cuprum::Matching in a custom class.
12
+ MatchClause = Struct.new(:block, :error, :status, :value) do
13
+ include Comparable
14
+
15
+ # @param other [Cuprum::Matching::MatchClause] The other result to compare.
16
+ #
17
+ # @return [Integer] the comparison result.
18
+ def <=>(other)
19
+ return nil unless other.is_a?(Cuprum::Matching::MatchClause)
20
+
21
+ cmp = compare(value, other.value)
22
+
23
+ return cmp unless cmp.zero?
24
+
25
+ compare(error, other.error)
26
+ end
27
+
28
+ # Checks if the match clause matches the specified error and value.
29
+ #
30
+ # @return [Boolean] true if the error and value match, otherwise false.
31
+ def matches_details?(error:, value:)
32
+ return false unless matches_detail?(error, self.error)
33
+ return false unless matches_detail?(value, self.value)
34
+
35
+ true
36
+ end
37
+
38
+ # Checks if the match clause matches the given result.
39
+ #
40
+ # @return [Boolean] true if the result matches, otherwise false.
41
+ def matches_result?(result:)
42
+ return false unless error.nil? || result.error.is_a?(error)
43
+ return false unless value.nil? || result.value.is_a?(value)
44
+
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ def compare(left, right)
51
+ return 0 if left.nil? && right.nil?
52
+ return 1 if left.nil?
53
+ return -1 if right.nil?
54
+
55
+ left <=> right || 0
56
+ end
57
+
58
+ def matches_detail?(actual, expected)
59
+ return true if actual.nil? && expected.nil?
60
+ return false if actual.nil? || expected.nil?
61
+
62
+ actual <= expected
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+
5
+ module Cuprum
6
+ # Implements a wrapper around another command.
7
+ #
8
+ # A middleware command wraps the execution of another command, allowing the
9
+ # developer to compose functionality without an explicit wrapper command.
10
+ # Because the middleware is responsible for calling the wrapped command, it
11
+ # has control over when that command is called, with what parameters, and how
12
+ # the command result is handled.
13
+ #
14
+ # To use middleware, start by defining a middleware command. This can either
15
+ # be a class that includes Cuprum::Middleware, or a command instance that
16
+ # extends Cuprum::Middleware. Each middleware command's #process method takes
17
+ # as its first argument the wrapped command. By convention, any additional
18
+ # arguments and any keywords or a block are passed to the wrapped command, but
19
+ # some middleware will override ths behavior.
20
+ #
21
+ # When defining #process, make sure to either call super or call the wrapped
22
+ # command directly, unless the middleware is specifically intended not to call
23
+ # the wrapped command under those circumstances.
24
+ #
25
+ # Middleware is powerful because it allows the developer to manipulate the
26
+ # parameters passed to a command, add handling to a result, or even intercept
27
+ # or override the command execution. These are some of the possible use cases
28
+ # for middleware:
29
+ #
30
+ # - Injecting code before or after a command.
31
+ # - Changing the parameters passed to a command.
32
+ # - Adding behavior based on the command result.
33
+ # - Overriding the command behavior based on the parameters.
34
+ #
35
+ # Middleware is loosely coupled, meaning that one middleware command can wrap
36
+ # any number of other commands. One example would be logging middleware, which
37
+ # could record when a command is called and with what parameters. For a more
38
+ # involved example, consider authorization in a web application. If individual
39
+ # actions are defined as commands, then a single authorization middleware
40
+ # class could wrap each individual action, reducing both the testing burden
41
+ # and the amount of code that must be maintained.
42
+ #
43
+ # @example Basic Middleware
44
+ # class ExampleCommand < Cuprum::Command
45
+ # private def process(**options)
46
+ # return failure(options[:error]) if options[:error]
47
+ #
48
+ # "Options: #{options.inspect}"
49
+ # end
50
+ # end
51
+ #
52
+ # class LoggingMiddleware < Cuprum::Command
53
+ # include Cuprum::Middleware
54
+ #
55
+ # # The middleware injects a logging step before the wrapped command is
56
+ # # called. Notice that this middleware is generic, and can be used with
57
+ # # virtually any other command.
58
+ # private def process(next_command, *args, **kwargs)
59
+ # Logger.info("Calling command #{next_command.class}")
60
+ #
61
+ # super
62
+ # end
63
+ # end
64
+ #
65
+ # command = Command.new { |**opts| "Called with #{opts.inspect}" }
66
+ # middleware = LoggingMiddleware.new
67
+ # result = middleware.call(command, { id: 0 })
68
+ # #=> logs "Calling command ExampleCommand"
69
+ # result.value
70
+ # #=> "Options: { id: 0 }"
71
+ #
72
+ # @example Injecting Parameters
73
+ # class ApiMiddleware < Cuprum::Command
74
+ # include Cuprum::Middleware
75
+ #
76
+ # # The middleware adds the :api_key to the parameters passed to the
77
+ # # command. If an :api_key keyword is passed, then the passed value will
78
+ # # take precedence.
79
+ # private def process(next_command, *args, **kwargs)
80
+ # super(next_command, *args, api_key: '12345', **kwargs)
81
+ # end
82
+ # end
83
+ #
84
+ # command = Command.new { |**opts| "Called with #{opts.inspect}" }
85
+ # middleware = LoggingMiddleware.new
86
+ # result = middleware.call(command, { id: 0 })
87
+ # result.value
88
+ # #=> "Options: { id: 0, api_key: '12345' }"
89
+ #
90
+ # @example Handling Results
91
+ # class IgnoreFailure < Cuprum::Command
92
+ # include Cuprum::Middleware
93
+ #
94
+ # # The middleware runs the command once. On a failing result, the
95
+ # # middleware discards the failing result and returns a result with a
96
+ # # value of nil.
97
+ # private def process(next_command, *args, **kwargs)
98
+ # result = super
99
+ #
100
+ # return result if result.success?
101
+ #
102
+ # success(nil)
103
+ # end
104
+ # end
105
+ #
106
+ # command = Command.new { |**opts| "Called with #{opts.inspect}" }
107
+ # middleware = LoggingMiddleware.new
108
+ # result = middleware.call(command, { id: 0 })
109
+ # result.success?
110
+ # #=> true
111
+ # result.value
112
+ # #=> "Options: { id: 0, api_key: '12345' }"
113
+ #
114
+ # error = Cuprum::Error.new(message: 'Something went wrong.')
115
+ # result = middleware.call(command, error: error)
116
+ # result.success?
117
+ # #=> true
118
+ # result.value
119
+ # #=> nil
120
+ #
121
+ # @example Flow Control
122
+ # class AuthenticationMiddleware < Cuprum::Command
123
+ # include Cuprum::Middleware
124
+ #
125
+ # # The middleware finds the current user based on the given keywords. If
126
+ # # a valid user is found, the user is then passed on to the command.
127
+ # # If a user is not found, then the middleware will immediately halt (due
128
+ # # to #step) and return the failing result from the authentication
129
+ # # command.
130
+ # private def process(next_command, *args, **kwargs)
131
+ # current_user = step { AuthenticateUser.new.call(**kwargs) }
132
+ #
133
+ # super(next_command, *args, current_user: current_user, **kwargs)
134
+ # end
135
+ # end
136
+ #
137
+ # @example Advanced Command Wrapping
138
+ # class RetryMiddleware < Cuprum::Command
139
+ # include Cuprum::Middleware
140
+ #
141
+ # # The middleware runs the command up to three times. If a result is
142
+ # # passing, that result is returned immediately; otherwise, the last
143
+ # # failing result will be returned by the middleware.
144
+ # private def process(next_command, *args, **kwargs)
145
+ # result = nil
146
+ #
147
+ # 3.times do
148
+ # result = super
149
+ #
150
+ # return result if result.success?
151
+ # end
152
+ #
153
+ # result
154
+ # end
155
+ # end
156
+ module Middleware
157
+ # @!method call(next_command, *arguments, **keywords, &block)
158
+ # Calls the next command with the given arguments, keywords, and block.
159
+ #
160
+ # Subclasses can call super to easily call the next command with the given
161
+ # parameters, or pass explicit parameters into super to call the next
162
+ # command with those parameters.
163
+ #
164
+ # @param next_command [Cuprum::Command] The command to call.
165
+ # @param arguments [Array] The arguments to pass to the command.
166
+ # @param keywords [Hash] The keywords to pass to the command.
167
+ #
168
+ # @yield A block to pass to the command.
169
+ #
170
+ # @return [Cuprum::Result] the result of calling the command.
171
+
172
+ # Helper method for wrapping a command with middleware.
173
+ #
174
+ # This method takes the given command and middleware and returns a command
175
+ # that will call the middleware in order, followed by the given command.
176
+ # This is done via partial application: the last item in the middleware is
177
+ # partially applied with the given command as the middleware's next command
178
+ # parameter. The next to last middleware is then partially applied with the
179
+ # last middleware as the next command and so on. This ensures that the
180
+ # middleware commands will be called in the given order, and that each
181
+ # middleware command wraps the next, down to the given command at the root.
182
+ #
183
+ # @param command [Cuprum::Command] The command to wrap with middleware.
184
+ # @param middleware [Cuprum::Middleware, Array<Cuprum::Middleware>] The
185
+ # middleware to wrap around the command. Will be called in the order they
186
+ # are given.
187
+ #
188
+ # @return [Cuprum::Command] the outermost middleware command, with the next
189
+ # command parameter partially applied.
190
+ def self.apply(command:, middleware:)
191
+ middleware = Array(middleware)
192
+
193
+ return command if middleware.empty?
194
+
195
+ middleware.reverse_each.reduce(command) do |next_command, cmd|
196
+ cmd.curry(next_command)
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ def process(next_command, *args, **kwargs, &block)
203
+ if kwargs.empty?
204
+ step { next_command.call(*args, &block) }
205
+ else
206
+ step { next_command.call(*args, **kwargs, &block) }
207
+ end
208
+ end
209
+ end
210
+ end