cuprum 1.2.0 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dab108b9d6630d5ebb692b2a918654cc9a6a2cb6256a5aad7331b40bd2ea7f1d
4
- data.tar.gz: 55e8f74553df29e8b244e69257c9178b3b8ea0b931c4f8b7a58ae4f587d17e27
3
+ metadata.gz: 878254bafd0929c73d6762f9fe732bbf647ae7d595c52733a53b1cd4115a308d
4
+ data.tar.gz: 4612a002c3523ea886274e4463e15d0d7139185cbebb5752e7ab4a0289d2d776
5
5
  SHA512:
6
- metadata.gz: 3f520bbc963d86937b02b8f7849bb064afafa32e43d47e8e5b394f5ff598e05665ba366bf2bffc44f7a3964982c28dba90d7621ab11b6b5a981217a092b62e15
7
- data.tar.gz: b3b1b63fa90af7d8f2edfd1d7db6ce5b14e2f10397907fd9dcd07138d4c0d5f70a0d1b538c5c7bd2cb1c9b0a10dfd94997c673ba293dbedfcb0855dda413c43d
6
+ metadata.gz: 537237e0cb5d7013af24907c0f2a7daf4d2967b5ad22bbfe078a76d7fc975df623f0119428ed0a6a962033e7d50ab8b9544b5bc590814ea65cbcc05d1fc5ea0d
7
+ data.tar.gz: 12fbd72264936ffaba10313fdc5a20ddd0c3003532c83ef47bb4accecf5652864d7dc99975ae96c86caaf00bfe7f8b1a2d206d46ea2626579ff9dcaf661ab019
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0
4
+
5
+ The "A Dream Given Form" Update
6
+
7
+ As of version 1.3.0, Cuprum will no longer support Ruby 3.0.
8
+
9
+ ### Commands
10
+
11
+ Updated `Cuprum::ExceptionHandling` to re-raise the exception if the `ENV['CUPRUM_RERAISE_EXCEPTIONS']` flag is set.
12
+
13
+ Implemented `Command.subclass`, allowing partial application of constructor parameters (including the implementation block).
14
+
15
+ Refactored `Command.new(&block)`, which no longer overwrites the `#process` method directly. Commands can now use `super` in the `#process` method to wrap a block implementation (or the parent class implementation if the parent class overrides `#process`).
16
+
17
+ **Breaking Change:** The private method `Command#process_block` is now reserved and used to handle commands with block implementations.
18
+
19
+ #### Parameter Validation
20
+
21
+ Implemented `Cuprum::ParameterValidation`, which provides a DSL for validating a command's parameters prior to evaluating `#process`.
22
+
23
+ ### Errors
24
+
25
+ **Breaking Change:** Corrected the namespace for `Cuprum::Errors::UncaughtException::TYPE` to remove a reference to `cuprum-collections`. If this causes an issue, update your application to reference the constant, rather than a hard-coded value.
26
+
27
+ ### RSpec
28
+
29
+ Implemented `Cuprum::RSpec::Deferred::ParameterValidationExamples`, which provide a shortcut for testing commands that use parameter validation.
30
+
3
31
  ## 1.2.0
4
32
 
5
33
  The "Straight On Till Morning" Update
data/README.md CHANGED
@@ -30,7 +30,7 @@ On the opposite end of the scale, frameworks such as [Dry::Monads](https://dry-r
30
30
 
31
31
  ## Compatibility
32
32
 
33
- Cuprum is tested against Ruby (MRI) 3.0 through 3.2.
33
+ Cuprum is tested against Ruby (MRI) 3.1 through 3.4.
34
34
 
35
35
  ## Documentation
36
36
 
@@ -38,9 +38,11 @@ Code documentation is generated using [YARD](https://yardoc.org/), and can be ge
38
38
 
39
39
  The full documentation is available via [GitHub Pages](http://sleepingkingstudios.github.io/cuprum), and includes the code documentation as well as a deeper explanation of Cuprum's features and design philosophy. It also includes documentation for prior versions of the gem.
40
40
 
41
+ To generate documentation locally, see the [SleepingKingStudios::Docs](https://github.com/sleepingkingstudios/sleeping_king_studios-docs) gem.
42
+
41
43
  ## License
42
44
 
43
- Copyright (c) 2017-2022 Rob Smith
45
+ Copyright (c) 2017-2025 Rob Smith
44
46
 
45
47
  Cuprum is released under the [MIT License](https://opensource.org/licenses/MIT).
46
48
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'sleeping_king_studios/tools/toolbox/subclass'
4
+
3
5
  require 'cuprum/currying'
4
6
  require 'cuprum/processing'
5
7
  require 'cuprum/steps'
@@ -69,10 +71,30 @@ module Cuprum
69
71
  #
70
72
  # @see Cuprum::Processing
71
73
  class Command
74
+ extend SleepingKingStudios::Tools::Toolbox::Subclass
72
75
  include Cuprum::Processing
73
76
  include Cuprum::Currying
74
77
  include Cuprum::Steps
75
78
 
79
+ # @!scope class
80
+
81
+ # @!method subclass(*class_arguments, **class_keywords, &block)
82
+ # Creates a subclass with partially applied constructor parameters.
83
+ #
84
+ # @param class_arguments [Array] the arguments, if any, to apply to the
85
+ # constructor. These arguments will be added before any args passed
86
+ # directly to the constructor.
87
+ # @param class_keywords [Hash] the keywords, if any, to apply to the
88
+ # constructor. These keywords will be added before any kwargs passed
89
+ # directly to the constructor.
90
+ #
91
+ # @yield the block, if any, to pass to the constructor. This will be
92
+ # overriden by a block passed directly to the constructor.
93
+ #
94
+ # @return [Class] the generated subclass.
95
+
96
+ # @!scope instance
97
+
76
98
  # Returns a new instance of Cuprum::Command.
77
99
  #
78
100
  # @yield If a block is given, the block is used to define a private #process
@@ -88,13 +110,15 @@ module Cuprum
88
110
  def initialize(&implementation)
89
111
  return unless implementation
90
112
 
91
- define_singleton_method :process, &implementation
113
+ define_singleton_method :process_block, &implementation
92
114
 
93
- singleton_class.send(:private, :process)
115
+ singleton_class.send(:private, :process_block)
116
+
117
+ @process_block = true
94
118
  end
95
119
 
96
120
  # (see Cuprum::Processing#call)
97
- def call(*args, **kwargs, &block)
121
+ def call(*args, **kwargs, &)
98
122
  steps { super }
99
123
  end
100
124
 
@@ -115,5 +139,18 @@ module Cuprum
115
139
  end
116
140
  end
117
141
  end
142
+
143
+ private
144
+
145
+ # (see Cuprum::Processing#process)
146
+ #
147
+ # @!visibility public
148
+ def process(...)
149
+ process_block? ? process_block(...) : super
150
+ end
151
+
152
+ def process_block?
153
+ @process_block
154
+ end
118
155
  end
119
156
  end
@@ -98,9 +98,9 @@ module Cuprum
98
98
  guard_abstract_factory!
99
99
 
100
100
  if klass
101
- define_command_from_class(klass, name: name, metadata: metadata)
101
+ define_command_from_class(klass, name:, metadata:)
102
102
  elsif block_given?
103
- define_command_from_block(defn, name: name, metadata: metadata)
103
+ define_command_from_block(defn, name:, metadata:)
104
104
  else
105
105
  require_definition!
106
106
  end
@@ -287,11 +287,11 @@ module Cuprum
287
287
 
288
288
  private
289
289
 
290
- def build_command(command_class, *args, **kwargs, &block)
290
+ def build_command(command_class, *args, **kwargs, &)
291
291
  if kwargs.empty?
292
- command_class.new(*args, &block)
292
+ command_class.new(*args, &)
293
293
  else
294
- command_class.new(*args, **kwargs, &block)
294
+ command_class.new(*args, **kwargs, &)
295
295
  end
296
296
  end
297
297
 
@@ -106,11 +106,13 @@ module Cuprum::Currying
106
106
  args = [*arguments, *args]
107
107
  kwargs = keywords.merge(kwargs)
108
108
 
109
+ # rubocop:disable Style/RedundantParentheses
109
110
  if kwargs.empty?
110
111
  command.call(*args, &(override || block))
111
112
  else
112
113
  command.call(*args, **kwargs, &(override || block))
113
114
  end
115
+ # rubocop:enable Style/RedundantParentheses
114
116
  end
115
117
  end
116
118
  end
@@ -69,10 +69,10 @@ module Cuprum
69
69
  return self if arguments.empty? && keywords.empty? && block.nil?
70
70
 
71
71
  Cuprum::Currying::CurriedCommand.new(
72
- arguments: arguments,
73
- block: block,
72
+ arguments:,
73
+ block:,
74
74
  command: self,
75
- keywords: keywords
75
+ keywords:
76
76
  )
77
77
  end
78
78
  end
data/lib/cuprum/error.rb CHANGED
@@ -65,7 +65,7 @@ module Cuprum
65
65
  def initialize(message: nil, type: nil, **properties)
66
66
  @message = message
67
67
  @type = type || self.class::TYPE
68
- @comparable_properties = properties.merge(message: message, type: type)
68
+ @comparable_properties = properties.merge(message:, type:)
69
69
  end
70
70
 
71
71
  # @return [String] optional message describing the nature of the error.
@@ -22,7 +22,7 @@ module Cuprum::Errors
22
22
  class_name = command&.class&.name || 'command'
23
23
  message = MESSAGE_FORMAT % class_name
24
24
 
25
- super(command: command, message: message)
25
+ super(command:, message:)
26
26
  end
27
27
 
28
28
  # @return [Cuprum::Command] The command called without a definition.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/error'
4
+ require 'cuprum/errors'
5
+
6
+ module Cuprum::Errors
7
+ # Error returned when a command's parameters fail validation.
8
+ class InvalidParameters < Cuprum::Error
9
+ # Short string used to identify the type of error.
10
+ TYPE = 'cuprum.errors.invalid_parameters'
11
+
12
+ # @param command_class [Class] the class of the failed command.
13
+ # @param failures [Array<String>] the messages for the failed validations.
14
+ def initialize(command_class:, failures:)
15
+ @command_class = command_class
16
+ @failures = failures
17
+
18
+ super(
19
+ command_class:,
20
+ failures:,
21
+ message: generate_message
22
+ )
23
+ end
24
+
25
+ # @return [Class] the class of the failed command.
26
+ attr_reader :command_class
27
+
28
+ # @return [Array<String>] the messages for the failed validations.
29
+ attr_reader :failures
30
+
31
+ private
32
+
33
+ def as_json_data
34
+ {
35
+ 'command_class' => command_class.name,
36
+ 'failures' => failures.map(&:to_s)
37
+ }
38
+ end
39
+
40
+ def generate_message
41
+ "invalid parameters for #{command_class.name} - #{failures.join(', ')}"
42
+ end
43
+ end
44
+ end
@@ -16,7 +16,7 @@ module Cuprum::Errors
16
16
  @errors = errors
17
17
 
18
18
  super(
19
- errors: errors,
19
+ errors:,
20
20
  message: message || default_message
21
21
  )
22
22
  end
@@ -19,7 +19,7 @@ module Cuprum::Errors
19
19
  class_name = operation&.class&.name || 'operation'
20
20
  message = MESSAGE_FORMAT % class_name
21
21
 
22
- super(message: message, operation: operation)
22
+ super(message:, operation:)
23
23
  end
24
24
 
25
25
  # @return [Cuprum::Operation] The uncalled operation.
@@ -7,7 +7,7 @@ module Cuprum::Errors
7
7
  # Error returned when a command encounters an unhandled exception.
8
8
  class UncaughtException < Cuprum::Error
9
9
  # Short string used to identify the type of error.
10
- TYPE = 'cuprum.collections.errors.uncaught_exception'
10
+ TYPE = 'cuprum.errors.uncaught_exception'
11
11
 
12
12
  # @param exception [StandardError] The exception that was raised.
13
13
  # @param message [String] A message to display. Will be annotated with
data/lib/cuprum/errors.rb CHANGED
@@ -6,6 +6,7 @@ module Cuprum
6
6
  # Namespace for custom Cuprum error classes.
7
7
  module Errors
8
8
  autoload :CommandNotImplemented, 'cuprum/errors/command_not_implemented'
9
+ autoload :InvalidParameters, 'cuprum/errors/invalid_parameters'
9
10
  autoload :MultipleErrors, 'cuprum/errors/multiple_errors'
10
11
  autoload :OperationNotCalled, 'cuprum/errors/operation_not_called'
11
12
  autoload :UncaughtException, 'cuprum/errors/uncaught_exception'
@@ -5,6 +5,10 @@ require 'cuprum/errors/uncaught_exception'
5
5
  module Cuprum
6
6
  # Utility module for handling uncaught exceptions in commands.
7
7
  #
8
+ # This functionality can be temporarily disabled by setting the
9
+ # ENV['CUPRUM_RERAISE_EXCEPTIONS'] flag; this can be used to debug issues when
10
+ # testing commands.
11
+ #
8
12
  # @example
9
13
  # class UnsafeCommand < Cuprum::Command
10
14
  # private
@@ -39,11 +43,16 @@ module Cuprum
39
43
  # a failing result if a StandardError is raised.
40
44
  #
41
45
  # @see Cuprum::Processing#call
42
- def call(*args, **kwargs, &block)
46
+ #
47
+ # @raise StandardError if an exception is raised and the
48
+ # ENV['CUPRUM_RERAISE_EXCEPTIONS'] flag is set.
49
+ def call(*args, **kwargs, &)
43
50
  super
44
51
  rescue StandardError => exception
52
+ raise exception if ENV['CUPRUM_RERAISE_EXCEPTIONS']
53
+
45
54
  error = Cuprum::Errors::UncaughtException.new(
46
- exception: exception,
55
+ exception:,
47
56
  message: "uncaught exception in #{self.class.name} - "
48
57
  )
49
58
  failure(error)
@@ -221,6 +221,10 @@ module Cuprum
221
221
  def splat_items?
222
222
  return @splat_items unless @splat_items.nil?
223
223
 
224
+ if respond_to?(:process_block, true)
225
+ return @splat_items = method(:process_block).arity > 1
226
+ end
227
+
224
228
  @splat_items = method(:process).arity > 1
225
229
  end
226
230
  end
@@ -69,10 +69,10 @@ module Cuprum
69
69
  #
70
70
  # @yield Executes the block in the context of the singleton class. This is
71
71
  # used to define match clauses when instantiating a Matcher instance.
72
- def initialize(match_context = nil, &block)
72
+ def initialize(match_context = nil, &)
73
73
  @match_context = match_context
74
74
 
75
- singleton_class.instance_exec(&block) if block_given?
75
+ singleton_class.instance_exec(&) if block_given?
76
76
  end
77
77
 
78
78
  # Returns a copy of the matcher with the given execution context.
@@ -113,20 +113,20 @@ module Cuprum
113
113
 
114
114
  def find_exact_match(result)
115
115
  matchers.find do |matcher|
116
- exact_match?(matcher: matcher, result: result)
116
+ exact_match?(matcher:, result:)
117
117
  end
118
118
  end
119
119
 
120
120
  def find_generic_match(result)
121
121
  matchers.find do |matcher|
122
- generic_match?(matcher: matcher, result: result)
122
+ generic_match?(matcher:, result:)
123
123
  end
124
124
  end
125
125
 
126
126
  def find_partial_match(result)
127
127
  matchers.find do |matcher|
128
- error_match?(matcher: matcher, result: result) ||
129
- value_match?(matcher: matcher, result: result)
128
+ error_match?(matcher:, result:) ||
129
+ value_match?(matcher:, result:)
130
130
  end
131
131
  end
132
132
 
@@ -40,21 +40,21 @@ module Cuprum
40
40
  # @private
41
41
  def match_result(result:)
42
42
  status_clauses(result.status).find do |clause|
43
- clause.matches_result?(result: result)
43
+ clause.matches_result?(result:)
44
44
  end
45
45
  end
46
46
 
47
47
  # @private
48
48
  def matches_result?(result:)
49
49
  status_clauses(result.status).reverse_each.any? do |clause|
50
- clause.matches_result?(result: result)
50
+ clause.matches_result?(result:)
51
51
  end
52
52
  end
53
53
 
54
54
  # @private
55
55
  def matches_status?(error:, status:, value:)
56
56
  status_clauses(status).reverse_each.any? do |clause|
57
- clause.matches_details?(error: error, value: value)
57
+ clause.matches_details?(error:, value:)
58
58
  end
59
59
  end
60
60
 
@@ -174,11 +174,11 @@ module Cuprum
174
174
  end
175
175
 
176
176
  result = result.to_cuprum_result
177
- clause = singleton_class.match_result(result: result)
177
+ clause = singleton_class.match_result(result:)
178
178
 
179
179
  raise NoMatchError, "no match found for #{result.inspect}" if clause.nil?
180
180
 
181
- call_match(block: clause.block, result: result)
181
+ call_match(block: clause.block, result:)
182
182
  end
183
183
 
184
184
  # @return [Boolean] true if an execution context is defined for a matching
@@ -214,9 +214,9 @@ module Cuprum
214
214
  )
215
215
  elsif result_or_status.is_a?(Symbol)
216
216
  return singleton_class.matches_status?(
217
- error: error,
217
+ error:,
218
218
  status: result_or_status,
219
- value: value
219
+ value:
220
220
  )
221
221
  end
222
222
 
@@ -63,7 +63,7 @@ module Cuprum
63
63
  # implementation.
64
64
  #
65
65
  # @see Cuprum::Command#call
66
- def call(*args, **kwargs, &block)
66
+ def call(*args, **kwargs, &)
67
67
  reset! if called? # Clear reference to most recent result.
68
68
 
69
69
  @result = super
@@ -123,7 +123,7 @@ module Cuprum
123
123
 
124
124
  error = Cuprum::Errors::OperationNotCalled.new(operation: self)
125
125
 
126
- Cuprum::Result.new(error: error)
126
+ Cuprum::Result.new(error:)
127
127
  end
128
128
 
129
129
  # @return [Object] the value of the most recent result, or nil if the
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/parameter_validation'
4
+
5
+ module Cuprum::ParameterValidation
6
+ # @api private
7
+ #
8
+ # Value class representing a single validation for a parameter.
9
+ class ValidationRule < Struct.new( # rubocop:disable Style/StructInheritance
10
+ :name,
11
+ :type,
12
+ :method_name,
13
+ :options,
14
+ :block,
15
+ keyword_init: true
16
+ )
17
+ # Custom validation type for a block validation.
18
+ BLOCK_VALIDATION_TYPE = :_block_validation
19
+
20
+ # Custom validation type for a named method validation.
21
+ NAMED_VALIDATION_TYPE = :_named_method_validation
22
+
23
+ # @param name [Symbol] the name of the parameter to validate.
24
+ # @param type [Symbol] the type of validation to perform.
25
+ # @param method_name [Symbol] the name for the validation method.
26
+ # @param options [Hash] additional options to pass to the validator.
27
+ # @param block [Proc] a block to pass to the validator, if any.
28
+ def initialize(name:, type:, method_name: nil, **options, &block)
29
+ super(
30
+ block:,
31
+ method_name: method_name&.to_sym || method_name_for(name:, type:),
32
+ name: name.to_sym,
33
+ options:,
34
+ type: type.to_sym
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def method_name_for(name:, type:)
41
+ case type
42
+ when BLOCK_VALIDATION_TYPE
43
+ :validate
44
+ when NAMED_VALIDATION_TYPE
45
+ :"validate_#{name}"
46
+ else
47
+ :"validate_#{type}"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/assertions'
4
+
5
+ require 'cuprum/errors/invalid_parameters'
6
+ require 'cuprum/parameter_validation'
7
+
8
+ module Cuprum::ParameterValidation
9
+ # Utility class for validating mapped parameters.
10
+ class Validator
11
+ # Exception raised when performing an unknown validation type.
12
+ class UnknownValidationError < StandardError; end
13
+
14
+ def initialize
15
+ @failures = []
16
+ end
17
+
18
+ # Validates the given parameters.
19
+ #
20
+ # @param command [Object] the command whose parameters are validated.
21
+ # @param parameters [Hash{Symbol=>Object}] the parameters to validate.
22
+ # @param rules [Array<ValidationRule>] the rules used to validate the
23
+ # parameters.
24
+ #
25
+ # @return [Cuprum::Result] a result with the validation errors, if any.
26
+ def call(command:, parameters:, rules:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
27
+ validator.clear
28
+
29
+ rules.each do |rule|
30
+ value = parameters[rule.name]
31
+
32
+ if rule.type == ValidationRule::BLOCK_VALIDATION_TYPE
33
+ evaluate_block_validation(rule:, value:)
34
+ elsif command.respond_to?(rule.method_name, true)
35
+ evaluate_command_validation(command:, rule:, value:)
36
+ elsif validator.respond_to?(rule.method_name)
37
+ evaluate_validation(rule:, value:)
38
+ else
39
+ raise UnknownValidationError, error_message_for(command:, rule:)
40
+ end
41
+ end
42
+
43
+ handle_failures_for(command)
44
+ end
45
+
46
+ private
47
+
48
+ def error_message_for(command:, rule:)
49
+ unless rule.type == ValidationRule::NAMED_VALIDATION_TYPE
50
+ return "unknown validation type #{rule.type.inspect}"
51
+ end
52
+
53
+ "undefined method '#{rule.method_name}' for an instance of " \
54
+ "#{command.class.name}"
55
+ end
56
+
57
+ def evaluate_block_validation(rule:, value:)
58
+ return if rule.block.call(value)
59
+
60
+ message = rule.options.fetch(
61
+ :message,
62
+ "#{rule.options.fetch(:as, rule.name)} is invalid"
63
+ )
64
+ validator << message
65
+ end
66
+
67
+ def evaluate_command_validation(command:, rule:, value:) # rubocop:disable Metrics/MethodLength
68
+ message = command.send(
69
+ rule.method_name,
70
+ value,
71
+ as: rule.name,
72
+ **rule.options
73
+ )
74
+
75
+ if message.is_a?(Array)
76
+ message.each { |item| validator << item }
77
+ elsif message
78
+ validator << message
79
+ end
80
+ end
81
+
82
+ def evaluate_validation(rule:, value:)
83
+ validator.send(
84
+ rule.method_name,
85
+ value,
86
+ as: rule.name,
87
+ **rule.options
88
+ )
89
+ end
90
+
91
+ def handle_failures_for(command)
92
+ return Cuprum::Result.new if validator.empty?
93
+
94
+ error = Cuprum::Errors::InvalidParameters.new(
95
+ command_class: command.class,
96
+ failures: validator.each.to_a
97
+ )
98
+ Cuprum::Result.new(error:)
99
+ end
100
+
101
+ def tools
102
+ SleepingKingStudios::Tools::Toolbelt.instance
103
+ end
104
+
105
+ def validator
106
+ @validator ||= tools.assertions.aggregator_class.new
107
+ end
108
+ end
109
+ end