cuprum 0.8.0 → 0.9.0.beta.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.
@@ -1,166 +1,91 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum'
2
4
 
3
5
  module Cuprum
4
6
  # Data object that encapsulates the result of calling a Cuprum command.
5
7
  class Result
8
+ STATUSES = %i[success failure].freeze
9
+
6
10
  # @param value [Object] The value returned by calling the command.
7
- # @param errors [Array] The errors (if any) generated when the command was
8
- # called.
9
- def initialize value = nil, errors: nil
11
+ # @param error [Object] The error (if any) generated when the command was
12
+ # called. Can be a Cuprum::Error, a model errors object, etc.
13
+ # @param status [String, Symbol] The status of the result. Must be :success,
14
+ # :failure, or nil.
15
+ def initialize(value: nil, error: nil, status: nil)
10
16
  @value = value
11
- @errors = errors.nil? ? build_errors : errors
12
- @status = nil
13
- @halted = false
14
- end # constructor
17
+ @error = error
18
+ @status = resolve_status(status)
19
+ end
15
20
 
16
21
  # @return [Object] the value returned by calling the command.
17
- attr_accessor :value
22
+ attr_reader :value
18
23
 
19
- # @return [Array] the errors (if any) generated when the command was
24
+ # @return [Object] the error (if any) generated when the command was
20
25
  # called.
21
- attr_accessor :errors
26
+ attr_reader :error
27
+
28
+ # @return [Symbol] the status of the result, either :success or :failure.
29
+ attr_reader :status
22
30
 
23
- # rubocop:disable Metrics/AbcSize
24
31
  # rubocop:disable Metrics/CyclomaticComplexity
25
- # rubocop:disable Metrics/MethodLength
26
- # rubocop:disable Metrics/PerceivedComplexity
27
32
 
28
33
  # Compares the other object to the result.
29
34
  #
30
35
  # @param other [#value, #success?] An object responding to, at minimum,
31
- # #value and #success?. If present, the #failure?, #errors and #halted?
32
- # values will also be compared.
36
+ # #value and #success?. If present, the #failure? and #error values
37
+ # will also be compared.
33
38
  #
34
39
  # @return [Boolean] True if all present values match the result, otherwise
35
40
  # false.
36
- def == other
37
- return false unless other.respond_to?(:value) && other.value == value
38
-
39
- unless other.respond_to?(:success?) && other.success? == success?
40
- return false
41
- end # unless
42
-
43
- if other.respond_to?(:failure?) && other.failure? != failure?
44
- return false
45
- end # if
46
-
47
- if other.respond_to?(:errors) && other.errors != errors
48
- return false
49
- end # if
50
-
51
- if other.respond_to?(:halted?) && other.halted? != halted?
52
- return false
53
- end # if
41
+ def ==(other)
42
+ return false unless other.respond_to?(:value) && other.value == value
43
+ return false unless other.respond_to?(:status) && other.status == status
44
+ return false unless other.respond_to?(:error) && other.error == error
54
45
 
55
46
  true
56
- end # method ==
57
- # rubocop:enable Metrics/AbcSize
47
+ end
58
48
  # rubocop:enable Metrics/CyclomaticComplexity
59
- # rubocop:enable Metrics/MethodLength
60
- # rubocop:enable Metrics/PerceivedComplexity
61
-
62
- # @return [Boolean] true if the result is empty, i.e. has no value or errors
63
- # and does not have its status set or is halted.
64
- def empty?
65
- value.nil? && errors.empty? && @status.nil? && !halted?
66
- end # method empty?
67
49
 
68
- # Marks the result as a failure, whether or not the command generated any
69
- # errors.
70
- #
71
- # @return [Cuprum::Result] The result.
72
- def failure!
73
- @status = :failure
74
-
75
- self
76
- end # method failure!
77
-
78
- # @return [Boolean] false if the command did not generate any errors,
79
- # otherwise true.
50
+ # @return [Boolean] true if the result status is :failure, otherwise false.
80
51
  def failure?
81
- @status == :failure || (@status.nil? && !errors.empty?)
82
- end # method failure?
52
+ @status == :failure
53
+ end
83
54
 
84
- # Marks the result as halted. Any subsequent chained commands will not be
85
- # run.
86
- #
87
- # @return [Cuprum::Result] The result.
88
- def halt!
89
- @halted = true
90
-
91
- self
92
- end # method halt!
93
-
94
- # @return [Boolean] true if the command has been halted, and will not run
95
- # any subsequent chained commands.
96
- def halted?
97
- @halted
98
- end # method halted?
99
-
100
- # Marks the result as a success, whether or not the command generated any
101
- # errors.
102
- #
103
- # @return [Cuprum::Result] The result.
104
- def success!
105
- @status = :success
106
-
107
- self
108
- end # method success!
109
-
110
- # @return [Boolean] true if the command did not generate any errors,
111
- # otherwise false.
55
+ # @return [Boolean] true if the result status is :success, otherwise false.
112
56
  def success?
113
- @status == :success || (@status.nil? && errors.empty?)
114
- end # method success?
57
+ @status == :success
58
+ end
115
59
 
116
60
  # @return [Cuprum::Result] The result.
117
- def to_result
61
+ def to_cuprum_result
118
62
  self
119
- end # method to_result
120
-
121
- # @api private
122
- def update other_result
123
- return self if other_result.nil?
124
-
125
- self.value = other_result.value
63
+ end
126
64
 
127
- update_status(other_result)
128
-
129
- update_errors(other_result)
65
+ private
130
66
 
131
- halt! if other_result.halted?
67
+ def defined_statuses
68
+ self.class::STATUSES
69
+ end
132
70
 
133
- self
134
- end # method update
71
+ def normalize_status(status)
72
+ return status unless status.is_a?(String) || status.is_a?(Symbol)
135
73
 
136
- protected
137
-
138
- attr_reader :status
74
+ tools.string.underscore(status).intern
75
+ end
139
76
 
140
- private
141
-
142
- # @!visibility public
143
- #
144
- # Generates an empty errors object. When the command is called, the result
145
- # will have its #errors property initialized to the value returned by
146
- # #build_errors. By default, this is an array. If you want to use a custom
147
- # errors object type, override this method in a subclass.
148
- #
149
- # @return [Array] An empty errors object.
150
- def build_errors
151
- []
152
- end # method build_errors
77
+ def resolve_status(status)
78
+ return error.nil? ? :success : :failure if status.nil?
153
79
 
154
- def update_errors other_result
155
- return if other_result.errors.empty?
80
+ normalized = normalize_status(status)
156
81
 
157
- @errors += other_result.errors
158
- end # method update_errors
82
+ return normalized if defined_statuses.include?(normalized)
159
83
 
160
- def update_status other_result
161
- return if status || !errors.empty?
84
+ raise ArgumentError, "invalid status #{status.inspect}"
85
+ end
162
86
 
163
- @status = other_result.status
164
- end # method update_status
165
- end # class
166
- end # module
87
+ def tools
88
+ SleepingKingStudios::Tools::Toolbelt.instance
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+
5
+ module Cuprum
6
+ # Namespace for RSpec extensions for testing Cuprum applications.
7
+ module RSpec; end
8
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rspec/be_a_result_matcher'
4
+
5
+ module RSpec
6
+ module Matchers # rubocop:disable Style/Documentation
7
+ def be_a_failing_result
8
+ be_a_result.with_status(:failure)
9
+ end
10
+
11
+ def be_a_passing_result
12
+ be_a_result.with_status(:success)
13
+ end
14
+
15
+ def be_a_result
16
+ Cuprum::RSpec::BeAResultMatcher.new
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/errors/operation_not_called'
4
+ require 'cuprum/rspec'
5
+
6
+ module Cuprum::RSpec
7
+ # Custom matcher that asserts the actual object is a Cuprum result object with
8
+ # the specified properties.
9
+ class BeAResultMatcher # rubocop:disable Metrics/ClassLength
10
+ DEFAULT_VALUE = Object.new.freeze
11
+ private_constant :DEFAULT_VALUE
12
+
13
+ def initialize
14
+ @expected_error = DEFAULT_VALUE
15
+ @expected_value = DEFAULT_VALUE
16
+ end
17
+
18
+ # @return [String] a short description of the matcher and expected
19
+ # properties.
20
+ def description
21
+ message = 'be a Cuprum result'
22
+
23
+ return message unless expected_properties?
24
+
25
+ "#{message} #{properties_description}"
26
+ end
27
+
28
+ # Checks that the given actual object is not a Cuprum result.
29
+ #
30
+ # @param actual [Object] The actual object to match.
31
+ #
32
+ # @return [Boolean] false if the actual object is a result; otherwise true.
33
+ def does_not_match?(actual)
34
+ @actual = actual
35
+
36
+ raise ArgumentError, negated_matcher_warning if expected_properties?
37
+
38
+ !actual_is_result?
39
+ end
40
+
41
+ # @return [String] a summary message describing a failed expectation.
42
+ def failure_message
43
+ message = "expected #{actual.inspect} to #{description}"
44
+
45
+ if !actual_is_result?
46
+ message + ', but the object is not a result'
47
+ elsif actual_is_uncalled_operation?
48
+ message + ', but the object is an uncalled operation'
49
+ elsif !properties_match?
50
+ message + properties_failure_message
51
+ else
52
+ # :nocov:
53
+ message
54
+ # :nocov:
55
+ end
56
+ end
57
+
58
+ # @return [String] a summary message describing a failed negated
59
+ # expectation.
60
+ def failure_message_when_negated
61
+ "expected #{actual.inspect} not to #{description}"
62
+ end
63
+
64
+ # Checks that the given actual object is a Cuprum result or compatible
65
+ # object and has the specified properties.
66
+ #
67
+ # @param actual [Object] The actual object to match.
68
+ #
69
+ # @return [Boolean] true if the actual object is a result with the expected
70
+ # properties; otherwise false.
71
+ def matches?(actual)
72
+ @actual = actual
73
+
74
+ actual_is_result? && !actual_is_uncalled_operation? && properties_match?
75
+ end
76
+
77
+ # Sets an error expectation on the matcher. Calls to #matches? will fail
78
+ # unless the actual object has the specified error.
79
+ #
80
+ # @param error [Cuprum::Error, Object] The expected error.
81
+ #
82
+ # @return [BeAResultMatcher] the updated matcher.
83
+ def with_error(error)
84
+ @expected_error = error
85
+
86
+ self
87
+ end
88
+ alias_method :and_error, :with_error
89
+
90
+ # Sets a status expectation on the matcher. Calls to #matches? will fail
91
+ # unless the actual object has the specified status.
92
+ #
93
+ # @param status [Symbol] The expected status.
94
+ #
95
+ # @return [BeAResultMatcher] the updated matcher.
96
+ def with_status(status)
97
+ @expected_status = status
98
+
99
+ self
100
+ end
101
+ alias_method :and_status, :with_status
102
+
103
+ # Sets a value expectation on the matcher. Calls to #matches? will fail
104
+ # unless the actual object has the specified value.
105
+ #
106
+ # @param value [Object] The expected value.
107
+ #
108
+ # @return [BeAResultMatcher] the updated matcher.
109
+ def with_value(value)
110
+ @expected_value = value
111
+
112
+ self
113
+ end
114
+ alias_method :and_value, :with_value
115
+
116
+ private
117
+
118
+ attr_reader \
119
+ :actual,
120
+ :expected_error,
121
+ :expected_status,
122
+ :expected_value
123
+
124
+ def actual_is_result?
125
+ actual.respond_to?(:to_cuprum_result)
126
+ end
127
+
128
+ def actual_is_uncalled_operation?
129
+ result.error.is_a?(Cuprum::Errors::OperationNotCalled)
130
+ end
131
+
132
+ def compare_items(expected, actual)
133
+ return expected.matches?(actual) if expected.respond_to?(:matches?)
134
+
135
+ expected == actual
136
+ end
137
+
138
+ def error_failure_message
139
+ return '' if error_matches?
140
+
141
+ "\n expected error: #{inspect_expected(expected_error)}" \
142
+ "\n actual error: #{result.error.inspect}"
143
+ end
144
+
145
+ def error_matches?
146
+ return @error_matches unless @error_matches.nil?
147
+
148
+ return @error_matches = true unless expected_error?
149
+
150
+ @error_matches = compare_items(expected_error, result.error)
151
+ end
152
+
153
+ def expected_properties?
154
+ expected_error? || expected_status? || expected_value?
155
+ end
156
+
157
+ def expected_error?
158
+ expected_error != DEFAULT_VALUE
159
+ end
160
+
161
+ def expected_status?
162
+ !!expected_status
163
+ end
164
+
165
+ def expected_value?
166
+ expected_value != DEFAULT_VALUE
167
+ end
168
+
169
+ def inspect_expected(expected)
170
+ return expected.description if expected.respond_to?(:description)
171
+
172
+ expected.inspect
173
+ end
174
+
175
+ def negated_matcher_warning
176
+ "Using `expect().not_to be_a_result#{properties_warning}` risks false" \
177
+ ' positives, since any other result will match.'
178
+ end
179
+
180
+ def properties_description # rubocop:disable Metrics/AbcSize
181
+ msg = ''
182
+ ary = []
183
+ ary << 'value' if expected_value?
184
+ ary << 'error' if expected_error?
185
+
186
+ unless ary.empty?
187
+ msg = "with the expected #{tools.array.humanize_list(ary)}"
188
+ end
189
+
190
+ return msg unless expected_status?
191
+
192
+ return "with status: #{expected_status.inspect}" if msg.empty?
193
+
194
+ msg + " and status: #{expected_status.inspect}"
195
+ end
196
+
197
+ def properties_failure_message
198
+ properties_short_message +
199
+ status_failure_message +
200
+ value_failure_message +
201
+ error_failure_message
202
+ end
203
+
204
+ def properties_match?
205
+ error_matches? && status_matches? && value_matches?
206
+ end
207
+
208
+ def properties_short_message
209
+ ary = []
210
+ ary << 'status' unless status_matches?
211
+ ary << 'value' unless value_matches?
212
+ ary << 'error' unless error_matches?
213
+
214
+ ", but the #{tools.array.humanize_list(ary)}" \
215
+ " #{tools.integer.pluralize(ary.size, 'does', 'do')} not match:"
216
+ end
217
+
218
+ def properties_warning
219
+ ary = []
220
+ ary << 'value' if expected_value?
221
+ ary << 'status' if expected_status?
222
+ ary << 'error' if expected_error?
223
+
224
+ return '' if ary.empty?
225
+
226
+ message = ".with_#{ary.first}()"
227
+
228
+ return message if ary.size == 1
229
+
230
+ message + ary[1..-1].map { |str| ".and_#{str}()" }.join
231
+ end
232
+
233
+ def result
234
+ @result ||= actual.to_cuprum_result
235
+ end
236
+
237
+ def status_failure_message
238
+ return '' if status_matches?
239
+
240
+ "\n expected status: #{expected_status.inspect}" \
241
+ "\n actual status: #{result.status.inspect}"
242
+ end
243
+
244
+ def status_matches?
245
+ return @status_matches unless @status_matches.nil?
246
+
247
+ return @status_matches = true unless expected_status?
248
+
249
+ @status_matches = result.status == expected_status
250
+ end
251
+
252
+ def tools
253
+ SleepingKingStudios::Tools::Toolbelt.instance
254
+ end
255
+
256
+ def value_failure_message
257
+ return '' if value_matches?
258
+
259
+ "\n expected value: #{inspect_expected(expected_value)}" \
260
+ "\n actual value: #{result.value.inspect}"
261
+ end
262
+
263
+ def value_matches?
264
+ return @value_matches unless @value_matches.nil?
265
+
266
+ return @value_matches = true unless expected_value?
267
+
268
+ @value_matches = compare_items(expected_value, result.value)
269
+ end
270
+ end
271
+ end