cuprum 0.7.0 → 0.10.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.
@@ -1,154 +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: []
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
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
49
 
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
-
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?
83
-
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!
52
+ @status == :failure
53
+ end
109
54
 
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
63
+ end
120
64
 
121
- # @api private
122
- def update other_result
123
- return self if other_result.nil?
124
-
125
- self.value = other_result.value
126
-
127
- update_status(other_result)
128
-
129
- update_errors(other_result)
130
-
131
- halt! if other_result.halted?
65
+ private
132
66
 
133
- self
134
- end # method update
67
+ def defined_statuses
68
+ self.class::STATUSES
69
+ end
135
70
 
136
- protected
71
+ def normalize_status(status)
72
+ return status unless status.is_a?(String) || status.is_a?(Symbol)
137
73
 
138
- attr_reader :status
74
+ tools.string_tools.underscore(status).intern
75
+ end
139
76
 
140
- private
77
+ def resolve_status(status)
78
+ return error.nil? ? :success : :failure if status.nil?
141
79
 
142
- def update_errors other_result
143
- return if other_result.errors.empty?
80
+ normalized = normalize_status(status)
144
81
 
145
- @errors += other_result.errors
146
- end # method update_errors
82
+ return normalized if defined_statuses.include?(normalized)
147
83
 
148
- def update_status other_result
149
- return if status || !errors.empty?
84
+ raise ArgumentError, "invalid status #{status.inspect}"
85
+ end
150
86
 
151
- @status = other_result.status
152
- end # method update_status
153
- end # class
154
- end # module
87
+ def tools
88
+ SleepingKingStudios::Tools::Toolbelt.instance
89
+ end
90
+ end
91
+ end
@@ -1,113 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cuprum'
2
4
 
3
5
  module Cuprum
4
- # Helper methods that delegate result methods to the currently processed
5
- # result.
6
- #
7
- # @example
8
- # class LogCommand
9
- # include Cuprum::Processing
10
- # include Cuprum::ResultHelpers
11
- #
12
- # private
13
- #
14
- # def process log
15
- # case log[:level]
16
- # when 'fatal'
17
- # halt!
18
- #
19
- # 'error'
20
- # when 'error' && log[:message]
21
- # errors << message
22
- #
23
- # 'error'
24
- # when 'error'
25
- # failure!
26
- #
27
- # 'error'
28
- # else
29
- # 'ok'
30
- # end # case
31
- # end # method process
32
- # end # class
33
- #
34
- # result = LogCommand.new.call(:level => 'info')
35
- # result.success? #=> true
36
- #
37
- # string = 'something went wrong'
38
- # result = LogCommand.new.call(:level => 'error', :message => string)
39
- # result.success? #=> false
40
- # result.errors #=> ['something went wrong']
41
- #
42
- # result = LogCommand.new.call(:level => 'error')
43
- # result.success? #=> false
44
- # result.errors #=> []
45
- #
46
- # result = LogCommand.new.call(:level => 'fatal')
47
- # result.halted? #=> true
48
- #
49
- # @see Cuprum::Command
6
+ # Helper methods for generating Cuprum result objects.
50
7
  module ResultHelpers
51
8
  private
52
9
 
53
- # @!visibility public
54
- #
55
- # Provides a reference to the current result's errors object. Messages or
56
- # error objects added to this will be included in the #errors method of the
57
- # returned result object.
58
- #
59
- # @return [Array, Object] The errors object.
60
- #
61
- # @see Cuprum::Result#errors.
62
- #
63
- # @note This is a private method, and only available when executing the
64
- # command implementation as defined in the constructor block or the
65
- # #process method.
66
- def errors
67
- result&.errors
68
- end # method errors
69
-
70
- # @!visibility public
71
- #
72
- # Marks the current result as failed. Calling #failure? on the returned
73
- # result object will evaluate to true, whether or not the result has any
74
- # errors.
75
- #
76
- # @see Cuprum::Result#failure!.
77
- #
78
- # @note This is a private method, and only available when executing the
79
- # command implementation as defined in the constructor block or the
80
- # #process method.
81
- def failure!
82
- result&.failure!
83
- end # method failure!
10
+ def build_result(error: nil, status: nil, value: nil)
11
+ Cuprum::Result.new(error: error, status: status, value: value)
12
+ end
84
13
 
85
- # @!visibility public
86
- #
87
- # Marks the current result as halted.
88
- #
89
- # @see Cuprum::Result#halt!.
90
- #
91
- # @note This is a private method, and only available when executing the
92
- # command implementation as defined in the constructor block or the
93
- # #process method.
94
- def halt!
95
- result&.halt!
96
- end # method halt!
14
+ def failure(error)
15
+ build_result(error: error)
16
+ end
97
17
 
98
- # @!visibility public
99
- #
100
- # Marks the current result as passing. Calling #success? on the returned
101
- # result object will evaluate to true, whether or not the result has any
102
- # errors.
103
- #
104
- # @see Cuprum::Result#success!.
105
- #
106
- # @note This is a private method, and only available when executing the
107
- # command implementation as defined in the constructor block or the
108
- # #process method.
109
- def success!
110
- result&.success!
111
- end # method success!
112
- end # module
113
- end # module
18
+ def success(value)
19
+ build_result(value: value)
20
+ end
21
+ end
22
+ 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).and_error(nil)
13
+ end
14
+
15
+ def be_a_result
16
+ Cuprum::RSpec::BeAResultMatcher.new
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,286 @@
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
+ RSPEC_MATCHER_METHODS = %i[description failure_message matches?].freeze
14
+ private_constant :RSPEC_MATCHER_METHODS
15
+
16
+ def initialize
17
+ @expected_error = DEFAULT_VALUE
18
+ @expected_value = DEFAULT_VALUE
19
+ end
20
+
21
+ # @return [String] a short description of the matcher and expected
22
+ # properties.
23
+ def description
24
+ message = 'be a Cuprum result'
25
+
26
+ return message unless expected_properties?
27
+
28
+ "#{message} #{properties_description}"
29
+ end
30
+
31
+ # Checks that the given actual object is not a Cuprum result.
32
+ #
33
+ # @param actual [Object] The actual object to match.
34
+ #
35
+ # @return [Boolean] false if the actual object is a result; otherwise true.
36
+ def does_not_match?(actual)
37
+ @actual = actual
38
+
39
+ raise ArgumentError, negated_matcher_warning if expected_properties?
40
+
41
+ !actual_is_result?
42
+ end
43
+
44
+ # @return [String] a summary message describing a failed expectation.
45
+ def failure_message
46
+ message = "expected #{actual.inspect} to #{description}"
47
+
48
+ if !actual_is_result?
49
+ message + ', but the object is not a result'
50
+ elsif actual_is_uncalled_operation?
51
+ message + ', but the object is an uncalled operation'
52
+ elsif !properties_match?
53
+ message + properties_failure_message
54
+ else
55
+ # :nocov:
56
+ message
57
+ # :nocov:
58
+ end
59
+ end
60
+
61
+ # @return [String] a summary message describing a failed negated
62
+ # expectation.
63
+ def failure_message_when_negated
64
+ "expected #{actual.inspect} not to #{description}"
65
+ end
66
+
67
+ # Checks that the given actual object is a Cuprum result or compatible
68
+ # object and has the specified properties.
69
+ #
70
+ # @param actual [Object] The actual object to match.
71
+ #
72
+ # @return [Boolean] true if the actual object is a result with the expected
73
+ # properties; otherwise false.
74
+ def matches?(actual)
75
+ @actual = actual
76
+
77
+ actual_is_result? && !actual_is_uncalled_operation? && properties_match?
78
+ end
79
+
80
+ # Sets an error expectation on the matcher. Calls to #matches? will fail
81
+ # unless the actual object has the specified error.
82
+ #
83
+ # @param error [Cuprum::Error, Object] The expected error.
84
+ #
85
+ # @return [BeAResultMatcher] the updated matcher.
86
+ def with_error(error)
87
+ @expected_error = error
88
+
89
+ self
90
+ end
91
+ alias_method :and_error, :with_error
92
+
93
+ # Sets a status expectation on the matcher. Calls to #matches? will fail
94
+ # unless the actual object has the specified status.
95
+ #
96
+ # @param status [Symbol] The expected status.
97
+ #
98
+ # @return [BeAResultMatcher] the updated matcher.
99
+ def with_status(status)
100
+ @expected_status = status
101
+
102
+ self
103
+ end
104
+ alias_method :and_status, :with_status
105
+
106
+ # Sets a value expectation on the matcher. Calls to #matches? will fail
107
+ # unless the actual object has the specified value.
108
+ #
109
+ # @param value [Object] The expected value.
110
+ #
111
+ # @return [BeAResultMatcher] the updated matcher.
112
+ def with_value(value)
113
+ @expected_value = value
114
+
115
+ self
116
+ end
117
+ alias_method :and_value, :with_value
118
+
119
+ private
120
+
121
+ attr_reader \
122
+ :actual,
123
+ :expected_error,
124
+ :expected_status,
125
+ :expected_value
126
+
127
+ def actual_is_result?
128
+ actual.respond_to?(:to_cuprum_result)
129
+ end
130
+
131
+ def actual_is_uncalled_operation?
132
+ result.error.is_a?(Cuprum::Errors::OperationNotCalled)
133
+ end
134
+
135
+ def compare_items(expected, actual)
136
+ return expected.matches?(actual) if expected.respond_to?(:matches?)
137
+
138
+ expected == actual
139
+ end
140
+
141
+ def error_failure_message
142
+ return '' if error_matches?
143
+
144
+ "\n expected error: #{inspect_expected(expected_error)}" \
145
+ "\n actual error: #{result.error.inspect}"
146
+ end
147
+
148
+ def error_matches?
149
+ return @error_matches unless @error_matches.nil?
150
+
151
+ return @error_matches = true unless expected_error?
152
+
153
+ @error_matches = compare_items(expected_error, result.error)
154
+ end
155
+
156
+ def expected_properties?
157
+ (expected_error? && !expected_error.nil?) ||
158
+ expected_status? ||
159
+ expected_value?
160
+ end
161
+
162
+ def expected_error?
163
+ expected_error != DEFAULT_VALUE
164
+ end
165
+
166
+ def expected_status?
167
+ !!expected_status
168
+ end
169
+
170
+ def expected_value?
171
+ expected_value != DEFAULT_VALUE
172
+ end
173
+
174
+ def inspect_expected(expected)
175
+ return expected.description if rspec_matcher?(expected)
176
+
177
+ expected.inspect
178
+ end
179
+
180
+ def negated_matcher_warning
181
+ "Using `expect().not_to be_a_result#{properties_warning}` risks false" \
182
+ ' positives, since any other result will match.'
183
+ end
184
+
185
+ # rubocop:disable Metrics/CyclomaticComplexity
186
+ # rubocop:disable Metrics/AbcSize
187
+ def properties_description
188
+ msg = ''
189
+ ary = []
190
+ ary << 'value' if expected_value?
191
+ ary << 'error' if expected_error? && !expected_error.nil?
192
+
193
+ unless ary.empty?
194
+ msg = "with the expected #{tools.array_tools.humanize_list(ary)}"
195
+ end
196
+
197
+ return msg unless expected_status?
198
+
199
+ return "with status: #{expected_status.inspect}" if msg.empty?
200
+
201
+ msg + " and status: #{expected_status.inspect}"
202
+ end
203
+ # rubocop:enable Metrics/CyclomaticComplexity
204
+ # rubocop:enable Metrics/AbcSize
205
+
206
+ def properties_failure_message
207
+ properties_short_message +
208
+ status_failure_message +
209
+ value_failure_message +
210
+ error_failure_message
211
+ end
212
+
213
+ def properties_match?
214
+ error_matches? && status_matches? && value_matches?
215
+ end
216
+
217
+ def properties_short_message
218
+ ary = []
219
+ ary << 'status' unless status_matches?
220
+ ary << 'value' unless value_matches?
221
+ ary << 'error' unless error_matches?
222
+
223
+ ", but the #{tools.array_tools.humanize_list(ary)}" \
224
+ " #{tools.integer_tools.pluralize(ary.size, 'does', 'do')} not match:"
225
+ end
226
+
227
+ def properties_warning
228
+ ary = []
229
+ ary << 'value' if expected_value?
230
+ ary << 'status' if expected_status?
231
+ ary << 'error' if expected_error?
232
+
233
+ return '' if ary.empty?
234
+
235
+ message = ".with_#{ary.first}()"
236
+
237
+ return message if ary.size == 1
238
+
239
+ message + ary[1..-1].map { |str| ".and_#{str}()" }.join
240
+ end
241
+
242
+ def result
243
+ @result ||= actual.to_cuprum_result
244
+ end
245
+
246
+ def rspec_matcher?(value)
247
+ RSPEC_MATCHER_METHODS.all? do |method_name|
248
+ value.respond_to?(method_name)
249
+ end
250
+ end
251
+
252
+ def status_failure_message
253
+ return '' if status_matches?
254
+
255
+ "\n expected status: #{expected_status.inspect}" \
256
+ "\n actual status: #{result.status.inspect}"
257
+ end
258
+
259
+ def status_matches?
260
+ return @status_matches unless @status_matches.nil?
261
+
262
+ return @status_matches = true unless expected_status?
263
+
264
+ @status_matches = result.status == expected_status
265
+ end
266
+
267
+ def tools
268
+ SleepingKingStudios::Tools::Toolbelt.instance
269
+ end
270
+
271
+ def value_failure_message
272
+ return '' if value_matches?
273
+
274
+ "\n expected value: #{inspect_expected(expected_value)}" \
275
+ "\n actual value: #{result.value.inspect}"
276
+ end
277
+
278
+ def value_matches?
279
+ return @value_matches unless @value_matches.nil?
280
+
281
+ return @value_matches = true unless expected_value?
282
+
283
+ @value_matches = compare_items(expected_value, result.value)
284
+ end
285
+ end
286
+ end