cuprum 0.9.0 → 0.11.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -9
  3. data/DEVELOPMENT.md +45 -50
  4. data/README.md +728 -536
  5. data/lib/cuprum.rb +12 -6
  6. data/lib/cuprum/built_in.rb +3 -1
  7. data/lib/cuprum/built_in/identity_command.rb +6 -4
  8. data/lib/cuprum/built_in/identity_operation.rb +4 -2
  9. data/lib/cuprum/built_in/null_command.rb +5 -3
  10. data/lib/cuprum/built_in/null_operation.rb +4 -2
  11. data/lib/cuprum/command.rb +37 -59
  12. data/lib/cuprum/command_factory.rb +50 -24
  13. data/lib/cuprum/currying.rb +79 -0
  14. data/lib/cuprum/currying/curried_command.rb +116 -0
  15. data/lib/cuprum/error.rb +44 -10
  16. data/lib/cuprum/errors.rb +2 -0
  17. data/lib/cuprum/errors/command_not_implemented.rb +6 -3
  18. data/lib/cuprum/errors/operation_not_called.rb +6 -6
  19. data/lib/cuprum/errors/uncaught_exception.rb +55 -0
  20. data/lib/cuprum/exception_handling.rb +50 -0
  21. data/lib/cuprum/matcher.rb +90 -0
  22. data/lib/cuprum/matcher_list.rb +150 -0
  23. data/lib/cuprum/matching.rb +232 -0
  24. data/lib/cuprum/matching/match_clause.rb +65 -0
  25. data/lib/cuprum/middleware.rb +210 -0
  26. data/lib/cuprum/operation.rb +23 -15
  27. data/lib/cuprum/processing.rb +10 -14
  28. data/lib/cuprum/result.rb +2 -4
  29. data/lib/cuprum/result_helpers.rb +22 -0
  30. data/lib/cuprum/rspec/be_a_result.rb +11 -2
  31. data/lib/cuprum/rspec/be_a_result_matcher.rb +22 -9
  32. data/lib/cuprum/rspec/be_callable.rb +14 -0
  33. data/lib/cuprum/steps.rb +233 -0
  34. data/lib/cuprum/utils.rb +3 -1
  35. data/lib/cuprum/utils/instance_spy.rb +37 -30
  36. data/lib/cuprum/version.rb +14 -11
  37. metadata +36 -21
  38. data/lib/cuprum/chaining.rb +0 -420
@@ -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, &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,14 +101,20 @@ module Cuprum
99
101
  # an error.
100
102
  def reset!
101
103
  @result = nil
102
- end # method reset
104
+ end
105
+
106
+ # @return [Symbol, nil] the status of the most recent result, or nil if
107
+ # the operation has not been called.
108
+ def status
109
+ called? ? result.status : nil
110
+ end
103
111
 
104
112
  # @return [Boolean] true if the most recent result had no error, or false
105
113
  # if the most recent result had an error or if the operation has not
106
114
  # been called.
107
115
  def success?
108
116
  called? ? result.success? : false
109
- end # method success?
117
+ end
110
118
 
111
119
  # Returns the most result if the operation was previously called.
112
120
  # Otherwise, returns a failing result.
@@ -124,8 +132,8 @@ module Cuprum
124
132
  # operation has not been called.
125
133
  def value
126
134
  called? ? result.value : nil
127
- end # method value
128
- end # module
135
+ end
136
+ end
129
137
  include Mixin
130
138
 
131
139
  # @!method call
@@ -150,9 +158,9 @@ module Cuprum
150
158
  # (see Cuprum::Operation::Mixin#success?)
151
159
 
152
160
  # @!method to_cuprum_result
153
- # (see Cuprum::Operation::Mixin#to_cuprum_result?)
161
+ # (see Cuprum::Operation::Mixin#to_cuprum_result)
154
162
 
155
163
  # @!method value
156
164
  # (see Cuprum::Operation::Mixin#value)
157
- end # class
158
- end # module
165
+ end
166
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cuprum/errors/command_not_implemented'
4
+ require 'cuprum/result_helpers'
4
5
 
5
6
  module Cuprum
6
7
  # Functional implementation for creating a command object. Cuprum::Processing
@@ -57,6 +58,8 @@ module Cuprum
57
58
  #
58
59
  # @see Cuprum::Command
59
60
  module Processing
61
+ include Cuprum::ResultHelpers
62
+
60
63
  # Returns a nonnegative integer for commands that take a fixed number of
61
64
  # arguments. For commands that take a variable number of arguments, returns
62
65
  # -n-1, where n is the number of required arguments.
@@ -87,8 +90,13 @@ module Cuprum
87
90
  #
88
91
  # @yield If a block argument is given, it will be passed to the
89
92
  # implementation.
90
- def call(*args, &block)
91
- value = process(*args, &block)
93
+ def call(*args, **kwargs, &block)
94
+ value =
95
+ if kwargs.empty?
96
+ process(*args, &block)
97
+ else
98
+ process(*args, **kwargs, &block)
99
+ end
92
100
 
93
101
  return value.to_cuprum_result if value_is_result?(value)
94
102
 
@@ -97,14 +105,6 @@ module Cuprum
97
105
 
98
106
  private
99
107
 
100
- def build_result(error: nil, status: nil, value: nil)
101
- Cuprum::Result.new(error: error, status: status, value: value)
102
- end
103
-
104
- def failure(error)
105
- build_result(error: error)
106
- end
107
-
108
108
  # @!visibility public
109
109
  # @overload process(*arguments, **keywords, &block)
110
110
  # The implementation of the command, to be executed when the #call method
@@ -128,10 +128,6 @@ module Cuprum
128
128
  build_result(error: error)
129
129
  end
130
130
 
131
- def success(value)
132
- build_result(value: value)
133
- end
134
-
135
131
  def value_is_result?(value)
136
132
  value.respond_to?(:to_cuprum_result)
137
133
  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?
@@ -71,7 +69,7 @@ module Cuprum
71
69
  def normalize_status(status)
72
70
  return status unless status.is_a?(String) || status.is_a?(Symbol)
73
71
 
74
- tools.string.underscore(status).intern
72
+ tools.string_tools.underscore(status).intern
75
73
  end
76
74
 
77
75
  def resolve_status(status)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+
5
+ module Cuprum
6
+ # Helper methods for generating Cuprum result objects.
7
+ module ResultHelpers
8
+ private
9
+
10
+ def build_result(error: nil, status: nil, value: nil)
11
+ Cuprum::Result.new(error: error, status: status, value: value)
12
+ end
13
+
14
+ def failure(error)
15
+ build_result(error: error)
16
+ end
17
+
18
+ def success(value)
19
+ build_result(value: value)
20
+ end
21
+ end
22
+ end
@@ -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
- be_a_result.with_status(:success)
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
@@ -10,6 +10,9 @@ module Cuprum::RSpec
10
10
  DEFAULT_VALUE = Object.new.freeze
11
11
  private_constant :DEFAULT_VALUE
12
12
 
13
+ RSPEC_MATCHER_METHODS = %i[description failure_message matches?].freeze
14
+ private_constant :RSPEC_MATCHER_METHODS
15
+
13
16
  def initialize
14
17
  @expected_error = DEFAULT_VALUE
15
18
  @expected_value = DEFAULT_VALUE
@@ -43,9 +46,9 @@ module Cuprum::RSpec
43
46
  message = "expected #{actual.inspect} to #{description}"
44
47
 
45
48
  if !actual_is_result?
46
- message + ', but the object is not a result'
49
+ "#{message}, but the object is not a result"
47
50
  elsif actual_is_uncalled_operation?
48
- message + ', but the object is an uncalled operation'
51
+ "#{message}, but the object is an uncalled operation"
49
52
  elsif !properties_match?
50
53
  message + properties_failure_message
51
54
  else
@@ -151,7 +154,9 @@ module Cuprum::RSpec
151
154
  end
152
155
 
153
156
  def expected_properties?
154
- expected_error? || expected_status? || expected_value?
157
+ (expected_error? && !expected_error.nil?) ||
158
+ expected_status? ||
159
+ expected_value?
155
160
  end
156
161
 
157
162
  def expected_error?
@@ -167,7 +172,7 @@ module Cuprum::RSpec
167
172
  end
168
173
 
169
174
  def inspect_expected(expected)
170
- return expected.description if expected.respond_to?(:description)
175
+ return expected.description if rspec_matcher?(expected)
171
176
 
172
177
  expected.inspect
173
178
  end
@@ -177,14 +182,15 @@ module Cuprum::RSpec
177
182
  ' positives, since any other result will match.'
178
183
  end
179
184
 
180
- def properties_description # rubocop:disable Metrics/AbcSize
185
+ # rubocop:disable Metrics/AbcSize
186
+ def properties_description
181
187
  msg = ''
182
188
  ary = []
183
189
  ary << 'value' if expected_value?
184
- ary << 'error' if expected_error?
190
+ ary << 'error' if expected_error? && !expected_error.nil?
185
191
 
186
192
  unless ary.empty?
187
- msg = "with the expected #{tools.array.humanize_list(ary)}"
193
+ msg = "with the expected #{tools.array_tools.humanize_list(ary)}"
188
194
  end
189
195
 
190
196
  return msg unless expected_status?
@@ -193,6 +199,7 @@ module Cuprum::RSpec
193
199
 
194
200
  msg + " and status: #{expected_status.inspect}"
195
201
  end
202
+ # rubocop:enable Metrics/AbcSize
196
203
 
197
204
  def properties_failure_message
198
205
  properties_short_message +
@@ -211,8 +218,8 @@ module Cuprum::RSpec
211
218
  ary << 'value' unless value_matches?
212
219
  ary << 'error' unless error_matches?
213
220
 
214
- ", but the #{tools.array.humanize_list(ary)}" \
215
- " #{tools.integer.pluralize(ary.size, 'does', 'do')} not match:"
221
+ ", but the #{tools.array_tools.humanize_list(ary)}" \
222
+ " #{tools.integer_tools.pluralize(ary.size, 'does', 'do')} not match:"
216
223
  end
217
224
 
218
225
  def properties_warning
@@ -234,6 +241,12 @@ module Cuprum::RSpec
234
241
  @result ||= actual.to_cuprum_result
235
242
  end
236
243
 
244
+ def rspec_matcher?(value)
245
+ RSPEC_MATCHER_METHODS.all? do |method_name|
246
+ value.respond_to?(method_name)
247
+ end
248
+ end
249
+
237
250
  def status_failure_message
238
251
  return '' if status_matches?
239
252
 
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rspec'
4
+
5
+ module Cuprum::RSpec
6
+ module Matchers # rubocop:disable Style/Documentation
7
+ # Asserts that the command defines a :process method.
8
+ #
9
+ # @return [RSpec::Matchers::BuiltIn::RespondTo] the generated matcher.
10
+ def be_callable
11
+ respond_to(:process, true)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/result_helpers'
4
+
5
+ module Cuprum
6
+ # The Steps supports step by step processes that halt on a failed step.
7
+ #
8
+ # After including Cuprum::Steps, use the #steps instance method to wrap a
9
+ # series of instructions. Each instruction is then defined using the #step
10
+ # method. Steps can be defined either as a block or as a method invocation.
11
+ #
12
+ # When the steps block is evaluated, each step is called in sequence. If the
13
+ # step resolves to a passing result, the result value is returned and
14
+ # execution continues to the next step. If all of the steps pass, then the
15
+ # result of the final step is returned from the #steps block.
16
+ #
17
+ # Conversely, if any step resolves to a failing result, that failing result is
18
+ # immediately returned from the #steps block. No further steps will be called.
19
+ #
20
+ # For example, consider updating a database record using a primary key and an
21
+ # attributes hash. Broken down into its basics, this requires the following
22
+ # instructions:
23
+ #
24
+ # - Using the primary key, find the existing record in the database.
25
+ # - Update the record object with the given attributes.
26
+ # - Save the updated record back to the database.
27
+ #
28
+ # Note that each of these steps can fail for different reasons. For example,
29
+ # if a record with the given primary key does not exist in the database, then
30
+ # the first instruction will fail, and the follow up steps should not be
31
+ # executed. Further, whatever context is executing these steps probably wants
32
+ # to know which step failed, and why.
33
+ #
34
+ # @example Defining Methods As Steps
35
+ # def assign_attributes(record, attributes); end
36
+ #
37
+ # def find_record(primary_key); end
38
+ #
39
+ # def save_record(record); end
40
+ #
41
+ # def update_record(primary_key, attributes)
42
+ # steps do
43
+ # record = step :find_record, primary_key
44
+ # record = step :assign_attributes, record, attributes
45
+ # step :save_record, record
46
+ # end
47
+ # end
48
+ #
49
+ # @example Defining Blocks As Steps
50
+ # class AssignAttributes < Cuprum::Command; end
51
+ #
52
+ # class FindRecord < Cuprum::Command; end
53
+ #
54
+ # class SaveRecord < Cuprum::Command; end
55
+ #
56
+ # def update_record(primary_key, attributes)
57
+ # steps do
58
+ # record = step { FindRecord.new.call(primary_key) }
59
+ # record = step { AssignAttributes.new.call(record, attributes) }
60
+ # step { SaveRecord.new.call(record) }
61
+ # end
62
+ # end
63
+ module Steps
64
+ include Cuprum::ResultHelpers
65
+
66
+ UNDEFINED = Object.new.freeze
67
+ private_constant :UNDEFINED
68
+
69
+ class << self
70
+ # @!visibility private
71
+ def execute_method(receiver, method_name, *args, **kwargs, &block)
72
+ if block_given? && kwargs.empty?
73
+ receiver.send(method_name, *args, &block)
74
+ elsif block_given?
75
+ receiver.send(method_name, *args, **kwargs, &block)
76
+ elsif kwargs.empty?
77
+ receiver.send(method_name, *args)
78
+ else
79
+ receiver.send(method_name, *args, **kwargs)
80
+ end
81
+ end
82
+
83
+ # @!visibility private
84
+ def extract_result_value(result)
85
+ return result unless result.respond_to?(:to_cuprum_result)
86
+
87
+ result = result.to_cuprum_result
88
+
89
+ return result.value if result.success?
90
+
91
+ throw :cuprum_failed_step, result
92
+ end
93
+
94
+ # rubocop:disable Metrics/MethodLength
95
+ # @!visibility private
96
+ def validate_method_name(method_name)
97
+ if method_name.nil?
98
+ raise ArgumentError,
99
+ 'expected a block or a method name',
100
+ caller(1..-1)
101
+ end
102
+
103
+ unless method_name.is_a?(String) || method_name.is_a?(Symbol)
104
+ raise ArgumentError,
105
+ 'expected method name to be a String or Symbol',
106
+ caller(1..-1)
107
+ end
108
+
109
+ return unless method_name.empty?
110
+
111
+ raise ArgumentError, "method name can't be blank", caller(1..-1)
112
+ end
113
+ # rubocop:enable Metrics/MethodLength
114
+ end
115
+
116
+ # Executes the block and returns the value, or halts on a failure.
117
+ #
118
+ # @yield Called with no parameters.
119
+ #
120
+ # @return [Object] the #value of the result, or the returned object.
121
+ #
122
+ # The #step method is used to evaluate a sequence of processes, and to
123
+ # fail fast and halt processing if any of the steps returns a failing
124
+ # result. Each invocation of #step should be wrapped in a #steps block,
125
+ # or used inside the #process method of a Command.
126
+ #
127
+ # If the object returned by the block is a Cuprum result or compatible
128
+ # object (such as a called operation), the value is converted to a Cuprum
129
+ # result via the #to_cuprum_result method. Otherwise, the object is
130
+ # returned directly from #step.
131
+ #
132
+ # If the returned object is a passing result, the #value of the result is
133
+ # returned by #step.
134
+ #
135
+ # If the returned object is a failing result, then #step will throw
136
+ # :cuprum_failed_result and the failing result. This is caught by the
137
+ # #steps block, and halts execution of any subsequent steps.
138
+ #
139
+ # @example Calling a Step
140
+ # # The #do_something method returns the string 'some value'.
141
+ # step { do_something() } #=> 'some value'
142
+ #
143
+ # value = step { do_something() }
144
+ # value #=> 'some value'
145
+ #
146
+ # @example Calling a Step with a Passing Result
147
+ # # The #do_something_else method returns a Cuprum result with a value
148
+ # # of 'another value'.
149
+ # step { do_something_else() } #=> 'another value'
150
+ #
151
+ # # The result is passing, so the value is extracted and returned.
152
+ # value = step { do_something_else() }
153
+ # value #=> 'another value'
154
+ #
155
+ # @example Calling a Step with a Failing Result
156
+ # # The #do_something_wrong method returns a failing Cuprum result.
157
+ # step { do_something_wrong() } # Throws the :cuprum_failed_step symbol.
158
+ def step(method_name = UNDEFINED, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength
159
+ result =
160
+ if method_name != UNDEFINED || !args.empty? || !kwargs.empty?
161
+ SleepingKingStudios::Tools::CoreTools.deprecate(
162
+ "#{self.class}#step(method_name)",
163
+ message: 'Use the block form: step { method_name(*args, **kwargs) }'
164
+ )
165
+
166
+ Cuprum::Steps.validate_method_name(method_name)
167
+
168
+ Cuprum::Steps
169
+ .execute_method(self, method_name, *args, **kwargs, &block)
170
+ elsif !block_given?
171
+ raise ArgumentError, 'expected a block'
172
+ else
173
+ block.call
174
+ end
175
+
176
+ Cuprum::Steps.extract_result_value(result)
177
+ end
178
+
179
+ # Returns the first failing #step result, or the final result if none fail.
180
+ #
181
+ # The #steps method is used to wrap a series of #step calls. Each step is
182
+ # executed in sequence. If any of the steps returns a failing result, that
183
+ # result is immediately returned from #steps. Otherwise, #steps wraps the
184
+ # value returned by a block in a Cuprum result.
185
+ #
186
+ # @yield Called with no parameters.
187
+ #
188
+ # @yieldreturn A Cuprum result, or an object to be wrapped in a result.
189
+ #
190
+ # @return [Cuprum::Result] the result or object returned by the block,
191
+ # wrapped in a Cuprum result.
192
+ #
193
+ # @example With A Passing Step
194
+ # result = steps do
195
+ # step { success('some value') }
196
+ # end
197
+ # result.class #=> Cuprum::Result
198
+ # result.success? #=> true
199
+ # result.value #=> 'some value'
200
+ #
201
+ # @example With A Failing Step
202
+ # result = steps do
203
+ # step { failure('something went wrong') }
204
+ # end
205
+ # result.class #=> Cuprum::Result
206
+ # result.success? #=> false
207
+ # result.error #=> 'something went wrong'
208
+ #
209
+ # @example With Multiple Steps
210
+ # result = steps do
211
+ # # This step is passing, so execution continues on to the next step.
212
+ # step { success('first step') }
213
+ #
214
+ # # This step is failing, so execution halts and returns this result.
215
+ # step { failure('second step') }
216
+ #
217
+ # # This step will never be called.
218
+ # step { success('third step') }
219
+ # end
220
+ # result.class #=> Cuprum::Result
221
+ # result.success? #=> false
222
+ # result.error #=> 'second step'
223
+ def steps(&block)
224
+ raise ArgumentError, 'no block given' unless block_given?
225
+
226
+ result = catch(:cuprum_failed_step) { block.call }
227
+
228
+ return result if result.respond_to?(:to_cuprum_result)
229
+
230
+ success(result)
231
+ end
232
+ end
233
+ end