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
@@ -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
|
data/lib/cuprum/operation.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'cuprum/command'
|
2
4
|
require 'cuprum/errors/operation_not_called'
|
3
5
|
|
@@ -26,8 +28,8 @@ module Cuprum
|
|
26
28
|
# @book = operation.value
|
27
29
|
#
|
28
30
|
# render :new
|
29
|
-
# end
|
30
|
-
# end
|
31
|
+
# end
|
32
|
+
# end
|
31
33
|
#
|
32
34
|
# Like a Command, an Operation can be defined directly by passing an
|
33
35
|
# implementation block to the constructor or by creating a subclass that
|
@@ -41,7 +43,7 @@ module Cuprum
|
|
41
43
|
# @example
|
42
44
|
# class CustomOperation < CustomCommand
|
43
45
|
# include Cuprum::Operation::Mixin
|
44
|
-
# end
|
46
|
+
# end
|
45
47
|
module Mixin
|
46
48
|
# @return [Cuprum::Result] The result from the most recent call of the
|
47
49
|
# operation.
|
@@ -62,32 +64,32 @@ module Cuprum
|
|
62
64
|
# implementation.
|
63
65
|
#
|
64
66
|
# @see Cuprum::Command#call
|
65
|
-
def call
|
67
|
+
def call(*args, **kwargs, &block)
|
66
68
|
reset! if called? # Clear reference to most recent result.
|
67
69
|
|
68
70
|
@result = super
|
69
71
|
|
70
72
|
self
|
71
|
-
end
|
73
|
+
end
|
72
74
|
|
73
75
|
# @return [Boolean] true if the operation has been called and has a
|
74
76
|
# reference to the most recent result; otherwise false.
|
75
77
|
def called?
|
76
78
|
!result.nil?
|
77
|
-
end
|
79
|
+
end
|
78
80
|
|
79
81
|
# @return [Object] the error (if any) from the most recent result, or nil
|
80
82
|
# if the operation has not been called.
|
81
83
|
def error
|
82
84
|
called? ? result.error : nil
|
83
|
-
end
|
85
|
+
end
|
84
86
|
|
85
87
|
# @return [Boolean] true if the most recent result had an error, or false
|
86
88
|
# if the most recent result had no error or if the operation has not
|
87
89
|
# been called.
|
88
90
|
def failure?
|
89
91
|
called? ? result.failure? : false
|
90
|
-
end
|
92
|
+
end
|
91
93
|
|
92
94
|
# Clears the reference to the most recent call of the operation, if any.
|
93
95
|
# This allows the result and any referenced data to be garbage collected.
|
@@ -99,7 +101,7 @@ module Cuprum
|
|
99
101
|
# an error.
|
100
102
|
def reset!
|
101
103
|
@result = nil
|
102
|
-
end
|
104
|
+
end
|
103
105
|
|
104
106
|
# @return [Symbol, nil] the status of the most recent result, or nil if
|
105
107
|
# the operation has not been called.
|
@@ -112,7 +114,7 @@ module Cuprum
|
|
112
114
|
# been called.
|
113
115
|
def success?
|
114
116
|
called? ? result.success? : false
|
115
|
-
end
|
117
|
+
end
|
116
118
|
|
117
119
|
# Returns the most result if the operation was previously called.
|
118
120
|
# Otherwise, returns a failing result.
|
@@ -130,8 +132,8 @@ module Cuprum
|
|
130
132
|
# operation has not been called.
|
131
133
|
def value
|
132
134
|
called? ? result.value : nil
|
133
|
-
end
|
134
|
-
end
|
135
|
+
end
|
136
|
+
end
|
135
137
|
include Mixin
|
136
138
|
|
137
139
|
# @!method call
|
@@ -156,9 +158,9 @@ module Cuprum
|
|
156
158
|
# (see Cuprum::Operation::Mixin#success?)
|
157
159
|
|
158
160
|
# @!method to_cuprum_result
|
159
|
-
# (see Cuprum::Operation::Mixin#to_cuprum_result
|
161
|
+
# (see Cuprum::Operation::Mixin#to_cuprum_result)
|
160
162
|
|
161
163
|
# @!method value
|
162
164
|
# (see Cuprum::Operation::Mixin#value)
|
163
|
-
end
|
164
|
-
end
|
165
|
+
end
|
166
|
+
end
|
data/lib/cuprum/result.rb
CHANGED
@@ -5,6 +5,7 @@ require 'cuprum'
|
|
5
5
|
module Cuprum
|
6
6
|
# Data object that encapsulates the result of calling a Cuprum command.
|
7
7
|
class Result
|
8
|
+
# Enumerates the default permitted values for a Result#status.
|
8
9
|
STATUSES = %i[success failure].freeze
|
9
10
|
|
10
11
|
# @param value [Object] The value returned by calling the command.
|
@@ -28,8 +29,6 @@ module Cuprum
|
|
28
29
|
# @return [Symbol] the status of the result, either :success or :failure.
|
29
30
|
attr_reader :status
|
30
31
|
|
31
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
32
|
-
|
33
32
|
# Compares the other object to the result.
|
34
33
|
#
|
35
34
|
# @param other [#value, #success?] An object responding to, at minimum,
|
@@ -45,7 +44,6 @@ module Cuprum
|
|
45
44
|
|
46
45
|
true
|
47
46
|
end
|
48
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
49
47
|
|
50
48
|
# @return [Boolean] true if the result status is :failure, otherwise false.
|
51
49
|
def failure?
|
@@ -2,16 +2,25 @@
|
|
2
2
|
|
3
3
|
require 'cuprum/rspec/be_a_result_matcher'
|
4
4
|
|
5
|
-
module RSpec
|
5
|
+
module Cuprum::RSpec
|
6
6
|
module Matchers # rubocop:disable Style/Documentation
|
7
|
+
# Asserts that the object is a Cuprum::Result with status: :failure.
|
8
|
+
#
|
9
|
+
# @return [Cuprum::RSpec::BeAResultMatcher] the generated matcher.
|
7
10
|
def be_a_failing_result
|
8
11
|
be_a_result.with_status(:failure)
|
9
12
|
end
|
10
13
|
|
14
|
+
# Asserts that the object is a Cuprum::Result with status: :success.
|
15
|
+
#
|
16
|
+
# @return [Cuprum::RSpec::BeAResultMatcher] the generated matcher.
|
11
17
|
def be_a_passing_result
|
12
18
|
be_a_result.with_status(:success).and_error(nil)
|
13
19
|
end
|
14
20
|
|
21
|
+
# Asserts that the object is a Cuprum::Result.
|
22
|
+
#
|
23
|
+
# @return [Cuprum::RSpec::BeAResultMatcher] the generated matcher.
|
15
24
|
def be_a_result
|
16
25
|
Cuprum::RSpec::BeAResultMatcher.new
|
17
26
|
end
|
@@ -46,9 +46,9 @@ module Cuprum::RSpec
|
|
46
46
|
message = "expected #{actual.inspect} to #{description}"
|
47
47
|
|
48
48
|
if !actual_is_result?
|
49
|
-
message
|
49
|
+
"#{message}, but the object is not a result"
|
50
50
|
elsif actual_is_uncalled_operation?
|
51
|
-
message
|
51
|
+
"#{message}, but the object is an uncalled operation"
|
52
52
|
elsif !properties_match?
|
53
53
|
message + properties_failure_message
|
54
54
|
else
|
@@ -182,7 +182,6 @@ module Cuprum::RSpec
|
|
182
182
|
' positives, since any other result will match.'
|
183
183
|
end
|
184
184
|
|
185
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
186
185
|
# rubocop:disable Metrics/AbcSize
|
187
186
|
def properties_description
|
188
187
|
msg = ''
|
@@ -200,7 +199,6 @@ module Cuprum::RSpec
|
|
200
199
|
|
201
200
|
msg + " and status: #{expected_status.inspect}"
|
202
201
|
end
|
203
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
204
202
|
# rubocop:enable Metrics/AbcSize
|
205
203
|
|
206
204
|
def properties_failure_message
|