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.
@@ -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
@@ -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 # if-else
30
- # end # create
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 # class
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 *args, **kwargs, &block
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 # method call
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 # method called?
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 # method error
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 # method failure?
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 # method reset
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 # method success?
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 # method value
134
- end # module
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 # class
164
- end # module
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 + ', but the object is not a result'
49
+ "#{message}, but the object is not a result"
50
50
  elsif actual_is_uncalled_operation?
51
- message + ', but the object is an uncalled operation'
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