cuprum 0.7.0 → 0.10.0.rc.0

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