cuprum 1.0.0 → 1.1.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.
@@ -37,8 +37,9 @@ module Cuprum
37
37
  #
38
38
  # @see Cuprum::Command
39
39
  class Operation < Cuprum::Command
40
- # Module-based implementation of the Operation methods. Use this to convert
41
- # an already-defined command into an operation.
40
+ # Module-based implementation of the Operation methods.
41
+ #
42
+ # Use this to convert an already-defined command into an operation.
42
43
  #
43
44
  # @example
44
45
  # class CustomOperation < CustomCommand
@@ -50,9 +51,7 @@ module Cuprum
50
51
  attr_reader :result
51
52
 
52
53
  # @overload call(*arguments, **keywords, &block)
53
- # Executes the logic encoded in the constructor block, or the #process
54
- # method if no block was passed to the constructor, and returns the
55
- # operation object.
54
+ # Calls the command implementation and stores the result.
56
55
  #
57
56
  # @param arguments [Array] Arguments to be passed to the implementation.
58
57
  #
@@ -63,7 +62,7 @@ module Cuprum
63
62
  # @yield If a block argument is given, it will be passed to the
64
63
  # implementation.
65
64
  #
66
- # @see Cuprum::Command#call
65
+ # @see Cuprum::Command#call
67
66
  def call(*args, **kwargs, &block)
68
67
  reset! if called? # Clear reference to most recent result.
69
68
 
@@ -116,10 +115,9 @@ module Cuprum
116
115
  called? ? result.success? : false
117
116
  end
118
117
 
119
- # Returns the most result if the operation was previously called.
120
- # Otherwise, returns a failing result.
121
- #
122
- # @return [Cuprum::Result] the most recent result or failing result.
118
+ # @return [Cuprum::Result] the most recent result if the operation was
119
+ # previously called; otherwise, returns a failing result with a
120
+ # Cuprum::Errors::OperationNotCalled error.
123
121
  def to_cuprum_result
124
122
  return result if result
125
123
 
@@ -60,18 +60,23 @@ module Cuprum
60
60
  module Processing
61
61
  include Cuprum::ResultHelpers
62
62
 
63
- # Returns a nonnegative integer for commands that take a fixed number of
64
- # arguments. For commands that take a variable number of arguments, returns
65
- # -n-1, where n is the number of required arguments.
63
+ # Returns an indication of the number of arguments accepted by #call.
64
+ #
65
+ # If the method takes a fixed number N of arguments, returns N. If the
66
+ # method takes a variable number of arguments, returns -N-1, where N is the
67
+ # number of required arguments. Keyword arguments will be considered as a
68
+ # single additional argument, that argument being mandatory if any keyword
69
+ # argument is mandatory.
66
70
  #
67
71
  # @return [Integer] The number of arguments.
72
+ #
73
+ # @see Method#arity.
68
74
  def arity
69
75
  method(:process).arity
70
76
  end
71
77
 
72
78
  # @overload call(*arguments, **keywords, &block)
73
- # Executes the command implementation and returns a Cuprum::Result or
74
- # compatible object.
79
+ # Executes the command and returns a Cuprum::Result or compatible object.
75
80
  #
76
81
  # Each time #call is invoked, the object performs the following steps:
77
82
  #
@@ -107,11 +112,16 @@ module Cuprum
107
112
 
108
113
  # @!visibility public
109
114
  # @overload process(*arguments, **keywords, &block)
110
- # The implementation of the command, to be executed when the #call method
111
- # is called. If #process returns a result, that result will be returned by
115
+ # The implementation of the command.
116
+ #
117
+ # Whereas the #call method provides the public interface for calling a
118
+ # command, the #process method defines the actual implementation. This
119
+ # method should not be called directly.
120
+ #
121
+ # When the command is called via #call, the parameters are passed to
122
+ # #process. If #process returns a result, that result will be returned by
112
123
  # #call; otherwise, the value returned by #process will be wrapped in a
113
- # successful Cuprum::Result object. This method should not be called
114
- # directly.
124
+ # successful Cuprum::Result object.
115
125
  #
116
126
  # @param arguments [Array] The arguments, if any, passed from #call.
117
127
  #
@@ -119,9 +129,10 @@ module Cuprum
119
129
  #
120
130
  # @yield The block, if any, passed from #call.
121
131
  #
122
- # @return [Object] the value of the result object to be returned by #call.
132
+ # @return [Cuprum::Result, Object] a result object, or the value of the
133
+ # result object to be returned by #call.
123
134
  #
124
- # @note This is a private method.
135
+ # @note This is a private method.
125
136
  def process(*_args)
126
137
  error = Cuprum::Errors::CommandNotImplemented.new(command: self)
127
138
 
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'cuprum'
6
+ require 'cuprum/errors/multiple_errors'
7
+
8
+ module Cuprum
9
+ # Collection object that encapsulates a set of Cuprum results.
10
+ #
11
+ # Each Cuprum::ResultList wraps an Array of Cuprum::Result objects, and itself
12
+ # implements the same methods as a Result: #status, #value, #error, and the
13
+ # #success? and #failure? predicates. As such, a Command's #process method can
14
+ # return a ResultList instead of a Result. This is useful for commands that
15
+ # operate on a collection of items, such as a MapCommand or a controller
16
+ # endpoint that performs a bulk operation.
17
+ #
18
+ # @see Cuprum::Result.
19
+ class ResultList
20
+ extend Forwardable
21
+ include Enumerable
22
+
23
+ UNDEFINED = Object.new.freeze
24
+ private_constant :UNDEFINED
25
+
26
+ # @!method each
27
+ # Iterates over the results.
28
+ #
29
+ # @overload each()
30
+ # @return [Enumerator] an enumerator over the results.
31
+ #
32
+ # @overload each(&block)
33
+ # Yields each result to the block.
34
+ #
35
+ # @yieldparam result [Cuprum::Result] the yielded result.
36
+
37
+ # @param allow_partial [true, false] If true, allows for some failing
38
+ # results as long as there is at least one passing result. Defaults to
39
+ # false.
40
+ # @param error [Cuprum::Error] If given, sets the error for the result list
41
+ # to the specified error object.
42
+ # @param results [Array<Cuprum::Result>] The wrapped results.
43
+ # @param status [:success, :failure] If given, sets the status of the result
44
+ # list to the specified value.
45
+ # @param value [Object] The value of the result. Defaults to the mapped
46
+ # values of the results.
47
+ def initialize(
48
+ *results,
49
+ allow_partial: false,
50
+ error: UNDEFINED,
51
+ status: UNDEFINED,
52
+ value: UNDEFINED
53
+ )
54
+ @allow_partial = allow_partial
55
+ @results = normalize_results(results)
56
+ @error = error == UNDEFINED ? build_error : error
57
+ @status = status == UNDEFINED ? build_status : status
58
+ @value = value == UNDEFINED ? values : value
59
+ end
60
+
61
+ # @return [Array<Cuprum::Result>] the wrapped results.
62
+ attr_reader :results
63
+ alias_method :to_a, :results
64
+
65
+ # Returns the error for the result list.
66
+ #
67
+ # If the result list was initialized with an error, returns that error.
68
+ #
69
+ # If any of the results have errors, aggregates the result errors into a
70
+ # Cuprum::MultipleErrors object.
71
+ #
72
+ # If none of the results have errors, returns nil.
73
+ #
74
+ # @return [Cuprum::Errors::MultipleErrors, Cuprum::Error, nil] the error for
75
+ # the result list.
76
+ attr_reader :error
77
+
78
+ # Determines the status of the combined results.
79
+ #
80
+ # If the result list was initialize with a status, returns that status.
81
+ #
82
+ # If there are no failing results, i.e. the results array is empty or all of
83
+ # the results are passing, returns :success.
84
+ #
85
+ # If there is at least one failing result, it instead returns :failure.
86
+ #
87
+ # If the :allow_partial flag is set to true, returns :success if the results
88
+ # array is empty or there is at least one passing result. If there is at
89
+ # least one failing result and no passing results, it instead returns
90
+ # :failure.
91
+ #
92
+ # @return [:success, :failure] the status of the combined results.
93
+ attr_reader :status
94
+
95
+ # @return [Object] The value of the result. Defaults to the mapped values of
96
+ # the results.
97
+ attr_reader :value
98
+
99
+ def_delegators :@results, :each
100
+
101
+ # @return [true, false] true if the other object is a ResultList with
102
+ # matching results and options; otherwise false.
103
+ def ==(other)
104
+ other.is_a?(ResultList) &&
105
+ results == other.results &&
106
+ allow_partial? == other.allow_partial?
107
+ end
108
+
109
+ # @return [true, false] if true, allows for some failing results as long as
110
+ # there is at least one passing result. Defaults to false.
111
+ #
112
+ # @see #status
113
+ # @see #to_cuprum_result
114
+ def allow_partial?
115
+ @allow_partial
116
+ end
117
+
118
+ # @return [Array<Cuprum::Error, nil>] the error, if any, for each result.
119
+ def errors
120
+ @errors ||= results.map(&:error)
121
+ end
122
+
123
+ # @return [Boolean] true if the result status is :failure, otherwise false.
124
+ def failure?
125
+ status == :failure
126
+ end
127
+
128
+ # @return [Array<Symbol>] the status for each result.
129
+ def statuses
130
+ @statuses ||= results.map(&:status)
131
+ end
132
+
133
+ # @return [Boolean] true if the result status is :success, otherwise false.
134
+ def success?
135
+ status == :success
136
+ end
137
+
138
+ # Converts the result list to a Cuprum::Result.
139
+ #
140
+ # @return [Cuprum::Result] the converted result.
141
+ #
142
+ # @see #error
143
+ # @see #status
144
+ # @see #value
145
+ def to_cuprum_result
146
+ Cuprum::Result.new(error: error, status: status, value: value)
147
+ end
148
+
149
+ # @return [Array<Object, nil>] the value, if any, for each result.
150
+ def values
151
+ @values ||= results.map(&:value)
152
+ end
153
+
154
+ private
155
+
156
+ def build_error
157
+ return if errors.compact.empty?
158
+
159
+ Cuprum::Errors::MultipleErrors.new(errors: errors)
160
+ end
161
+
162
+ def build_status
163
+ passing_result? ? :success : :failure
164
+ end
165
+
166
+ def normalize_results(results)
167
+ results.map do |obj|
168
+ next obj if obj.is_a?(Cuprum::ResultList)
169
+
170
+ next obj.to_cuprum_result if obj.respond_to?(:to_cuprum_result)
171
+
172
+ raise ArgumentError,
173
+ "invalid result: #{obj.inspect} does not respond to #to_cuprum_result"
174
+ end
175
+ end
176
+
177
+ def passing_result?
178
+ return true if results.empty?
179
+
180
+ if allow_partial?
181
+ results.any?(&:success?)
182
+ else
183
+ results.all?(&:success?)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -142,7 +142,7 @@ module Cuprum::RSpec
142
142
  return '' if error_matches?
143
143
 
144
144
  "\n expected error: #{inspect_expected(expected_error)}" \
145
- "\n actual error: #{result.error.inspect}"
145
+ "\n actual error: #{result.error.inspect}"
146
146
  end
147
147
 
148
148
  def error_matches?
@@ -179,7 +179,7 @@ module Cuprum::RSpec
179
179
 
180
180
  def negated_matcher_warning
181
181
  "Using `expect().not_to be_a_result#{properties_warning}` risks false" \
182
- ' positives, since any other result will match.'
182
+ ' positives, since any other result will match.'
183
183
  end
184
184
 
185
185
  # rubocop:disable Metrics/AbcSize
@@ -219,7 +219,7 @@ module Cuprum::RSpec
219
219
  ary << 'error' unless error_matches?
220
220
 
221
221
  ", but the #{tools.array_tools.humanize_list(ary)}" \
222
- " #{tools.integer_tools.pluralize(ary.size, 'does', 'do')} not match:"
222
+ " #{tools.integer_tools.pluralize(ary.size, 'does', 'do')} not match:"
223
223
  end
224
224
 
225
225
  def properties_warning
@@ -234,7 +234,7 @@ module Cuprum::RSpec
234
234
 
235
235
  return message if ary.size == 1
236
236
 
237
- message + ary[1..-1].map { |str| ".and_#{str}()" }.join
237
+ message + ary[1..].map { |str| ".and_#{str}()" }.join
238
238
  end
239
239
 
240
240
  def result
@@ -251,7 +251,7 @@ module Cuprum::RSpec
251
251
  return '' if status_matches?
252
252
 
253
253
  "\n expected status: #{expected_status.inspect}" \
254
- "\n actual status: #{result.status.inspect}"
254
+ "\n actual status: #{result.status.inspect}"
255
255
  end
256
256
 
257
257
  def status_matches?
@@ -270,7 +270,7 @@ module Cuprum::RSpec
270
270
  return '' if value_matches?
271
271
 
272
272
  "\n expected value: #{inspect_expected(expected_value)}" \
273
- "\n actual value: #{result.value.inspect}"
273
+ "\n actual value: #{result.value.inspect}"
274
274
  end
275
275
 
276
276
  def value_matches?
data/lib/cuprum/steps.rb CHANGED
@@ -128,7 +128,8 @@ module Cuprum
128
128
  #
129
129
  # @yield Called with no parameters.
130
130
  #
131
- # @yieldreturn A Cuprum result, or an object to be wrapped in a result.
131
+ # @yieldreturn [Cuprum::Result, Object] a Cuprum result, or an object to be
132
+ # wrapped in a result.
132
133
  #
133
134
  # @return [Cuprum::Result] the result or object returned by the block,
134
135
  # wrapped in a Cuprum result.
@@ -163,6 +164,8 @@ module Cuprum
163
164
  # result.class #=> Cuprum::Result
164
165
  # result.success? #=> false
165
166
  # result.error #=> 'second step'
167
+ #
168
+ # @raise [ArgumentError] if a block is not given.
166
169
  def steps(&block)
167
170
  raise ArgumentError, 'no block given' unless block_given?
168
171
 
@@ -3,54 +3,52 @@
3
3
  require 'cuprum/utils'
4
4
 
5
5
  module Cuprum::Utils
6
- # Utility module for instrumenting calls to the #call method of any instance
7
- # of a command class. This can be used to unobtrusively test the
8
- # functionality of code that calls a command without providing a reference to
9
- # the command instance, such as chained commands or methods that create and
10
- # call a command instance.
6
+ # Instruments calls to the #call method of any instance of a command class.
7
+ #
8
+ # This can be used to unobtrusively test the functionality of code that calls
9
+ # a command without providing a reference to the command instance, such
10
+ # methods that create and call a command instance.
11
11
  #
12
12
  # @example Observing calls to instances of a command.
13
13
  # spy = Cuprum::Utils::InstanceSpy.spy_on(CustomCommand)
14
14
  #
15
- # expect(spy).to receive(:call).with(1, 2, 3, :four => '4')
15
+ # allow(spy).to receive(:call)
16
16
  #
17
17
  # CustomCommand.new.call(1, 2, 3, :four => '4')
18
18
  #
19
- # @example Observing calls to a chained command.
20
- # spy = Cuprum::Utils::InstanceSpy.spy_on(ChainedCommand)
21
- #
22
- # expect(spy).to receive(:call)
23
- #
24
- # Cuprum::Command.new {}.
25
- # chain { |result| ChainedCommand.new.call(result) }.
26
- # call
19
+ # expect(spy).to have_received(:call).with(1, 2, 3, :four => '4')
27
20
  #
28
21
  # @example Block syntax
29
22
  # Cuprum::Utils::InstanceSpy.spy_on(CustomCommand) do |spy|
30
- # expect(spy).to receive(:call)
23
+ # allow(spy).to receive(:call)
31
24
  #
32
25
  # CustomCommand.new.call
26
+ #
27
+ # expect(spy).to have_received(:call)
33
28
  # end
34
29
  module InstanceSpy
35
- # Minimal class that implements a #call method to mirror method calls to
36
- # instances of an instrumented command class.
30
+ # Minimal class double implementing the #call method.
37
31
  class Spy
38
- # Empty method that accepts any arguments and an optional block.
39
- def call(*_args, &block); end
32
+ # Empty method that accepts any parameters and an optional block.
33
+ def call(*_args, **_kwargs, &block); end
40
34
  end
41
35
 
42
36
  class << self
43
- # Retires all spies. Subsequent calls to the #call method on command
44
- # instances will not be mirrored to existing spy objects.
37
+ # Retires all spies.
38
+ #
39
+ # Subsequent calls to the #call method on command instances will not be
40
+ # mirrored to existing spy objects.
45
41
  def clear_spies
46
42
  Thread.current[:cuprum_instance_spies] = nil
47
43
 
48
44
  nil
49
45
  end
50
46
 
51
- # Finds or creates a spy object for the given module or class. Each time
52
- # that the #call method is called for an object of the given type, the
53
- # spy's #call method will be invoked with the same arguments and block.
47
+ # Finds or creates a spy object for the given module or class.
48
+ #
49
+ # Each time that the #call method is called for an object of the given
50
+ # type, the spy's #call method will be invoked with the same arguments and
51
+ # block.
54
52
  #
55
53
  # @param command_class [Class, Module] The type of command to spy on.
56
54
  # Must be either a Module, or a Class that extends Cuprum::Command.
@@ -10,13 +10,13 @@ module Cuprum
10
10
  # Major version.
11
11
  MAJOR = 1
12
12
  # Minor version.
13
- MINOR = 0
13
+ MINOR = 1
14
14
  # Patch version.
15
15
  PATCH = 0
16
16
  # Prerelease version.
17
- PRERELEASE = nil
17
+ PRERELEASE = :rc
18
18
  # Build metadata.
19
- BUILD = nil
19
+ BUILD = 0
20
20
 
21
21
  class << self
22
22
  # Generates the gem version string from the Version constants.
data/lib/cuprum.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A lightweight, functional-lite toolkit for making business logic a first-class
4
- # citizen of your application.
3
+ # Toolkit for implementing business logic as function objects.
5
4
  module Cuprum
6
5
  autoload :Command, 'cuprum/command'
7
6
  autoload :Error, 'cuprum/error'
7
+ autoload :MapCommand, 'cuprum/map_command'
8
8
  autoload :Matcher, 'cuprum/matcher'
9
9
  autoload :Middleware, 'cuprum/middleware'
10
10
  autoload :Operation, 'cuprum/operation'
11
11
  autoload :Result, 'cuprum/result'
12
+ autoload :ResultList, 'cuprum/result_list'
12
13
  autoload :Steps, 'cuprum/steps'
13
14
 
14
15
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuprum
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0.rc.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob "Merlin" Smith
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-16 00:00:00.000000000 Z
11
+ date: 2023-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sleeping_king_studios-tools
@@ -44,56 +44,56 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.5'
47
+ version: '2.7'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.5'
54
+ version: '2.7'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 1.10.0
61
+ version: '1.31'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 1.10.0
68
+ version: '1.31'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop-rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '2.1'
75
+ version: '2.11'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '2.1'
82
+ version: '2.11'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: simplecov
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.15'
89
+ version: '0.21'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.15'
96
+ version: '0.21'
97
97
  description: |-
98
98
  An opinionated implementation of the Command pattern for Ruby applications.
99
99
  Cuprum wraps your business logic in a consistent, object-oriented interface
@@ -123,9 +123,11 @@ files:
123
123
  - lib/cuprum/error.rb
124
124
  - lib/cuprum/errors.rb
125
125
  - lib/cuprum/errors/command_not_implemented.rb
126
+ - lib/cuprum/errors/multiple_errors.rb
126
127
  - lib/cuprum/errors/operation_not_called.rb
127
128
  - lib/cuprum/errors/uncaught_exception.rb
128
129
  - lib/cuprum/exception_handling.rb
130
+ - lib/cuprum/map_command.rb
129
131
  - lib/cuprum/matcher.rb
130
132
  - lib/cuprum/matcher_list.rb
131
133
  - lib/cuprum/matching.rb
@@ -135,6 +137,7 @@ files:
135
137
  - lib/cuprum/processing.rb
136
138
  - lib/cuprum/result.rb
137
139
  - lib/cuprum/result_helpers.rb
140
+ - lib/cuprum/result_list.rb
138
141
  - lib/cuprum/rspec.rb
139
142
  - lib/cuprum/rspec/be_a_result.rb
140
143
  - lib/cuprum/rspec/be_a_result_matcher.rb
@@ -150,7 +153,7 @@ metadata:
150
153
  bug_tracker_uri: https://github.com/sleepingkingstudios/cuprum/issues
151
154
  source_code_uri: https://github.com/sleepingkingstudios/cuprum
152
155
  rubygems_mfa_required: 'true'
153
- post_install_message:
156
+ post_install_message:
154
157
  rdoc_options: []
155
158
  require_paths:
156
159
  - lib
@@ -158,15 +161,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
158
161
  requirements:
159
162
  - - ">="
160
163
  - !ruby/object:Gem::Version
161
- version: 2.5.0
164
+ version: 2.6.0
162
165
  required_rubygems_version: !ruby/object:Gem::Requirement
163
166
  requirements:
164
- - - ">="
167
+ - - ">"
165
168
  - !ruby/object:Gem::Version
166
- version: '0'
169
+ version: 1.3.1
167
170
  requirements: []
168
- rubygems_version: 3.1.4
169
- signing_key:
171
+ rubygems_version: 3.4.1
172
+ signing_key:
170
173
  specification_version: 4
171
174
  summary: An opinionated implementation of the Command pattern.
172
175
  test_files: []