cuprum 0.8.0 → 0.9.0.beta.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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