cuprum 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 +10,7 @@ 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.
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
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-03-01 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
169
  version: '0'
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: []