cuprum 0.9.1 → 0.11.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/DEVELOPMENT.md +42 -53
  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 +17 -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 +10 -1
  31. data/lib/cuprum/rspec/be_a_result_matcher.rb +5 -7
  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 +13 -10
  37. metadata +34 -19
  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,7 +101,7 @@ module Cuprum
99
101
  # an error.
100
102
  def reset!
101
103
  @result = nil
102
- end # method reset
104
+ end
103
105
 
104
106
  # @return [Symbol, nil] the status of the most recent result, or nil if
105
107
  # the operation has not been called.
@@ -112,7 +114,7 @@ module Cuprum
112
114
  # been called.
113
115
  def success?
114
116
  called? ? result.success? : false
115
- end # method success?
117
+ end
116
118
 
117
119
  # Returns the most result if the operation was previously called.
118
120
  # Otherwise, returns a failing result.
@@ -130,8 +132,8 @@ module Cuprum
130
132
  # operation has not been called.
131
133
  def value
132
134
  called? ? result.value : nil
133
- end # method value
134
- end # module
135
+ end
136
+ end
135
137
  include Mixin
136
138
 
137
139
  # @!method call
@@ -156,9 +158,9 @@ module Cuprum
156
158
  # (see Cuprum::Operation::Mixin#success?)
157
159
 
158
160
  # @!method to_cuprum_result
159
- # (see Cuprum::Operation::Mixin#to_cuprum_result?)
161
+ # (see Cuprum::Operation::Mixin#to_cuprum_result)
160
162
 
161
163
  # @!method value
162
164
  # (see Cuprum::Operation::Mixin#value)
163
- end # class
164
- 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
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
@@ -46,9 +46,9 @@ module Cuprum::RSpec
46
46
  message = "expected #{actual.inspect} to #{description}"
47
47
 
48
48
  if !actual_is_result?
49
- message + ', but the object is not a result'
49
+ "#{message}, but the object is not a result"
50
50
  elsif actual_is_uncalled_operation?
51
- message + ', but the object is an uncalled operation'
51
+ "#{message}, but the object is an uncalled operation"
52
52
  elsif !properties_match?
53
53
  message + properties_failure_message
54
54
  else
@@ -182,7 +182,6 @@ module Cuprum::RSpec
182
182
  ' positives, since any other result will match.'
183
183
  end
184
184
 
185
- # rubocop:disable Metrics/CyclomaticComplexity
186
185
  # rubocop:disable Metrics/AbcSize
187
186
  def properties_description
188
187
  msg = ''
@@ -191,7 +190,7 @@ module Cuprum::RSpec
191
190
  ary << 'error' if expected_error? && !expected_error.nil?
192
191
 
193
192
  unless ary.empty?
194
- msg = "with the expected #{tools.array.humanize_list(ary)}"
193
+ msg = "with the expected #{tools.array_tools.humanize_list(ary)}"
195
194
  end
196
195
 
197
196
  return msg unless expected_status?
@@ -200,7 +199,6 @@ module Cuprum::RSpec
200
199
 
201
200
  msg + " and status: #{expected_status.inspect}"
202
201
  end
203
- # rubocop:enable Metrics/CyclomaticComplexity
204
202
  # rubocop:enable Metrics/AbcSize
205
203
 
206
204
  def properties_failure_message
@@ -220,8 +218,8 @@ module Cuprum::RSpec
220
218
  ary << 'value' unless value_matches?
221
219
  ary << 'error' unless error_matches?
222
220
 
223
- ", but the #{tools.array.humanize_list(ary)}" \
224
- " #{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:"
225
223
  end
226
224
 
227
225
  def properties_warning
@@ -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