cuprum 1.2.0.rc.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.
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools'
4
+
5
+ require 'cuprum'
6
+ require 'cuprum/utils/parameters_mapping'
7
+
8
+ module Cuprum
9
+ # Mixin for declaring validations for command parameters.
10
+ module ParameterValidation
11
+ extend SleepingKingStudios::Tools::Toolbox::Mixin
12
+
13
+ autoload :ValidationRule, 'cuprum/parameter_validation/validation_rule'
14
+ autoload :Validator, 'cuprum/parameter_validation/validator'
15
+
16
+ # Class methods for parameter validation.
17
+ module ClassMethods
18
+ # @private
19
+ def each_validation(&)
20
+ return enum_for(:each_validation) unless block_given?
21
+
22
+ ancestors.reverse_each do |ancestor|
23
+ next unless ancestor.respond_to?(:validation_rules, true)
24
+
25
+ ancestor.validation_rules.each(&)
26
+ end
27
+ end
28
+
29
+ # @overload validate(name, **options)
30
+ # Defines a validation for the specified parameter.
31
+ #
32
+ # This validation will call the #validate_$name method on the command
33
+ # with the value of the named parameter. If the method returns a failure
34
+ # message, that message is added to the failed validations.
35
+ #
36
+ # @param name [String, Symbol] the parameter to validate.
37
+ # @param options [Hash] additional options to pass to the validation
38
+ # method.
39
+ #
40
+ # @option options as [String, Symbol] the name of the parameter as
41
+ # displayed in the failure message, if any. Defaults to the value of
42
+ # the name parameter.
43
+ #
44
+ # @return void
45
+ #
46
+ # @overload validate(name, using:, **options)
47
+ # Defines a validation for the specified parameter.
48
+ #
49
+ # This validation will call the named method on the command with the
50
+ # value of the named parameter. If the method returns a failure message,
51
+ # that message is added to the failed validations.
52
+ #
53
+ # @param name [String, Symbol] the parameter to validate.
54
+ # @param using [String, Symbol] the name of the method used to validate
55
+ # the parameter.
56
+ # @param options [Hash] additional options to pass to the validation
57
+ # method.
58
+ #
59
+ # @option options as [String, Symbol] the name of the parameter as
60
+ # displayed in the failure message, if any. Defaults to the value of
61
+ # the name parameter.
62
+ #
63
+ # @return void
64
+ #
65
+ # @overload validate(name, **options, &block)
66
+ # Defines a validation for the specified parameter.
67
+ #
68
+ # This validation will call the given block with the value of the named
69
+ # parameter. If the block returns nil or false, a failure message is
70
+ # added to the failed validations
71
+ #
72
+ # @param name [String, Symbol] the parameter to validate.
73
+ # @param options [Hash] additional options for the validation.
74
+ #
75
+ # @option options as [String, Symbol] the name of the parameter as
76
+ # displayed in the failure message, if any. Defaults to the value of
77
+ # the name parameter.
78
+ # @option options message [String] the failure message to display.
79
+ # Defaults to "$name is invalid".
80
+ #
81
+ # @yield the block to validate the parameter.
82
+ # @yieldparam value [Object] the value of the named parameter.
83
+ # @yieldreturn [true, false] true if the given value is valid for the
84
+ # parameter; otherwise false.
85
+ #
86
+ # @return void
87
+ #
88
+ # @overload validate(name, type, **options)
89
+ # Defines a validation for the specified parameter.
90
+ #
91
+ # This validation will call the #validate_$type method on the command
92
+ # with the value of the named parameter. If the method returns a failure
93
+ # message, that message is added to the failed validations.
94
+ #
95
+ # If the command does not define the method, it will call the
96
+ # SleepingKingStudios::Tools::Assertions instance method with the same
97
+ # name. If the validation fails, the failure message is added to the
98
+ # failed validations.
99
+ #
100
+ # @param name [String, Symbol] the parameter to validate.
101
+ # @param type [String, Symbol] the validation method to run.
102
+ # @param options [Hash] additional options to pass to the validation
103
+ # method.
104
+ #
105
+ # @option options as [String, Symbol] the name of the parameter as
106
+ # displayed in the failure message, if any. Defaults to the value of
107
+ # the name parameter.
108
+ # @option options message [String] the message to display on a failed
109
+ # validation.
110
+ #
111
+ # @raise [Cuprum::ParameterValidation::Validator::UnknownValidationError]
112
+ # if neither the command nor the standard tools defines the method.
113
+ def validate(name, type = nil, using: nil, **options, &)
114
+ tools.assertions.validate_name(name, as: 'name')
115
+
116
+ if type && !type.is_a?(Module)
117
+ tools.assertions.validate_name(type, as: 'type')
118
+ end
119
+
120
+ tools.assertions.validate_name(using, as: 'using') if using
121
+
122
+ validation_rules <<
123
+ build_validation_rule(name:, options:, type:, using:, &)
124
+ end
125
+
126
+ # @private
127
+ def validate_parameters(command, ...)
128
+ parameters = parameters_mapping.call(...)
129
+ rules = each_validation
130
+
131
+ Validator.new.call(command:, parameters:, rules:)
132
+ rescue NameError => exception
133
+ raise unless exception.name == :process
134
+
135
+ error = Cuprum::Errors::CommandNotImplemented.new(command:)
136
+
137
+ Cuprum::Result.new(error:)
138
+ end
139
+
140
+ protected
141
+
142
+ def validation_rules
143
+ @validation_rules ||= []
144
+ end
145
+
146
+ private
147
+
148
+ def build_block_validation(name, **options, &)
149
+ type = ValidationRule::BLOCK_VALIDATION_TYPE
150
+
151
+ ValidationRule.new(name:, type:, as: name.to_s, **options, &)
152
+ end
153
+
154
+ def build_method_validation(name, method_name, **options)
155
+ type = ValidationRule::NAMED_VALIDATION_TYPE
156
+
157
+ ValidationRule.new(name:, type:, as: name.to_s, method_name:, **options)
158
+ end
159
+
160
+ def build_named_validation(name, **options)
161
+ type = ValidationRule::NAMED_VALIDATION_TYPE
162
+
163
+ ValidationRule.new(name:, type:, as: name.to_s, **options)
164
+ end
165
+
166
+ def build_type_validation(name, type, **options)
167
+ unless type.is_a?(Module)
168
+ return ValidationRule.new(name:, type:, as: name.to_s, **options)
169
+ end
170
+
171
+ ValidationRule.new(
172
+ name:,
173
+ type: :instance_of,
174
+ as: name.to_s,
175
+ expected: type,
176
+ **options
177
+ )
178
+ end
179
+
180
+ def build_validation_rule(name:, options:, type:, using:, &)
181
+ return build_type_validation(name, type, **options) if type
182
+
183
+ return build_method_validation(name, using, **options) if using
184
+
185
+ return build_block_validation(name, **options, &) if block_given?
186
+
187
+ build_named_validation(name, **options)
188
+ end
189
+
190
+ def parameters_mapping
191
+ @parameters_mapping ||=
192
+ Cuprum::Utils::ParametersMapping.build(instance_method(:process))
193
+ end
194
+
195
+ def tools
196
+ SleepingKingStudios::Tools::Toolbelt.instance
197
+ end
198
+ end
199
+
200
+ # @overload call(*arguments, **keywords, &block)
201
+ # Validates the parameters and passes them to super.
202
+ #
203
+ # @param arguments [Array] the arguments to validate.
204
+ # @param keywords [Hash] the keywords to validate.
205
+ # @param block [Proc] the block to validate, if any,
206
+ #
207
+ # @return [Cuprum::Result] a failing result with a
208
+ # Cuprum::Errors::InvalidParameters error if the validation fails;
209
+ # otherwise the normal result of calling the command.
210
+ #
211
+ # @see Cuprum::Processing#call.
212
+ def call(...)
213
+ result = self.class.validate_parameters(self, ...)
214
+
215
+ return result if result.failure?
216
+
217
+ super
218
+ end
219
+ end
220
+ end
@@ -95,17 +95,17 @@ module Cuprum
95
95
  #
96
96
  # @yield If a block argument is given, it will be passed to the
97
97
  # implementation.
98
- def call(*args, **kwargs, &block)
98
+ def call(*args, **kwargs, &)
99
99
  value =
100
100
  if kwargs.empty?
101
- process(*args, &block)
101
+ process(*args, &)
102
102
  else
103
- process(*args, **kwargs, &block)
103
+ process(*args, **kwargs, &)
104
104
  end
105
105
 
106
106
  return value.to_cuprum_result if value_is_result?(value)
107
107
 
108
- build_result(value: value)
108
+ build_result(value:)
109
109
  end
110
110
 
111
111
  private
@@ -136,7 +136,7 @@ module Cuprum
136
136
  def process(*_args)
137
137
  error = Cuprum::Errors::CommandNotImplemented.new(command: self)
138
138
 
139
- build_result(error: error)
139
+ build_result(error:)
140
140
  end
141
141
 
142
142
  def value_is_result?(value)
data/lib/cuprum/result.rb CHANGED
@@ -54,9 +54,9 @@ module Cuprum
54
54
  # @return [Hash{Symbol => Object}] a Hash representation of the result.
55
55
  def properties
56
56
  {
57
- error: error,
58
- status: status,
59
- value: value
57
+ error:,
58
+ status:,
59
+ value:
60
60
  }
61
61
  end
62
62
  alias_method :to_h, :properties
@@ -8,15 +8,15 @@ module Cuprum
8
8
  private
9
9
 
10
10
  def build_result(error: nil, status: nil, value: nil)
11
- Cuprum::Result.new(error: error, status: status, value: value)
11
+ Cuprum::Result.new(error:, status:, value:)
12
12
  end
13
13
 
14
14
  def failure(error)
15
- build_result(error: error)
15
+ build_result(error:)
16
16
  end
17
17
 
18
18
  def success(value)
19
- build_result(value: value)
19
+ build_result(value:)
20
20
  end
21
21
  end
22
22
  end
@@ -143,7 +143,7 @@ module Cuprum
143
143
  # @see #status
144
144
  # @see #value
145
145
  def to_cuprum_result
146
- Cuprum::Result.new(error: error, status: status, value: value)
146
+ Cuprum::Result.new(error:, status:, value:)
147
147
  end
148
148
 
149
149
  # @return [Array<Object, nil>] the value, if any, for each result.
@@ -156,7 +156,7 @@ module Cuprum
156
156
  def build_error
157
157
  return if errors.compact.empty?
158
158
 
159
- Cuprum::Errors::MultipleErrors.new(errors: errors)
159
+ Cuprum::Errors::MultipleErrors.new(errors:)
160
160
  end
161
161
 
162
162
  def build_status
@@ -104,7 +104,7 @@ module Cuprum::RSpec
104
104
  # @param actual [Object] the actual object to match.
105
105
  #
106
106
  # @return [Boolean] false if the actual object is a result; otherwise true.
107
- def does_not_match?(actual)
107
+ def does_not_match?(actual) # rubocop:disable Naming/PredicateName
108
108
  @actual = actual
109
109
 
110
110
  raise ArgumentError, negated_matcher_warning if expected_properties?
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/sleeping_king_studios/deferred'
4
+
5
+ require 'cuprum/errors/invalid_parameters'
6
+ require 'cuprum/rspec/be_a_result'
7
+ require 'cuprum/rspec/deferred'
8
+
9
+ module Cuprum::RSpec::Deferred
10
+ # Deferred examples for testing parameter validation.
11
+ #
12
+ # @example With A Validation Type
13
+ # RSpec.describe LaunchRocket do
14
+ # include Cuprum::RSpec::Deferred::ParameterValidationExamples
15
+ #
16
+ # describe '#call' do
17
+ # let(:launch_site) { 'KSC' }
18
+ #
19
+ # def call_command
20
+ # subject.call(launch_site:)
21
+ # end
22
+ #
23
+ # describe 'with invalid parameters' do
24
+ # let(:launch_site) { nil }
25
+ #
26
+ # include_deferred 'should validate the parameter',
27
+ # :launch_site,
28
+ # 'sleeping_king_studios.tools.assertions.presence',
29
+ # as: 'launch site'
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ # @example With A Message
35
+ # RSpec.describe LaunchRocket do
36
+ # include Cuprum::RSpec::Deferred::ParameterValidationExamples
37
+ #
38
+ # describe '#call' do
39
+ # let(:launch_site) { 'KSC' }
40
+ #
41
+ # def call_command
42
+ # subject.call(launch_site:)
43
+ # end
44
+ #
45
+ # describe 'with invalid parameters' do
46
+ # let(:launch_site) { nil }
47
+ #
48
+ # include_deferred 'should validate the parameter',
49
+ # :launch_site,
50
+ # message: "launch site can't be blank"
51
+ # end
52
+ # end
53
+ # end
54
+ module ParameterValidationExamples
55
+ include Cuprum::RSpec::Matchers
56
+ include RSpec::SleepingKingStudios::Deferred::Provider
57
+
58
+ deferred_examples 'should validate the parameter' \
59
+ do |name, type = nil, message: nil, **options|
60
+ it 'should return a failing result with InvalidParameters error' do
61
+ expected_failure =
62
+ message ||
63
+ tools.assertions.error_message_for(type, as: name, **options)
64
+ expected_error = Cuprum::Errors::InvalidParameters.new(
65
+ command_class: subject.class,
66
+ failures: [expected_failure]
67
+ )
68
+
69
+ expect(call_command)
70
+ .to be_a_failing_result
71
+ .with_error(expected_error)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def tools
78
+ SleepingKingStudios::Tools::Toolbelt.instance
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/rspec'
4
+
5
+ module Cuprum::RSpec
6
+ # Namespace for deferred example groups for validating Cuprum commands.
7
+ module Deferred; end
8
+ end
@@ -30,7 +30,7 @@ module Cuprum::Utils
30
30
  # Minimal class double implementing the #call method.
31
31
  class Spy
32
32
  # Empty method that accepts any parameters and an optional block.
33
- def call(*_args, **_kwargs, &block); end
33
+ def call(*_args, **_kwargs, &); end
34
34
  end
35
35
 
36
36
  class << self
@@ -96,8 +96,8 @@ module Cuprum::Utils
96
96
  Cuprum::Utils::InstanceSpy::Spy.new
97
97
  end
98
98
 
99
- def call_spies_for(command, *args, &block)
100
- spies_for(command).each { |spy| spy.call(*args, &block) }
99
+ def call_spies_for(command, ...)
100
+ spies_for(command).each { |spy| spy.call(...) }
101
101
  end
102
102
 
103
103
  def guard_spy_class!(command_class)
@@ -127,13 +127,13 @@ module Cuprum::Utils
127
127
  end
128
128
 
129
129
  # (see Cuprum::Processing#call)
130
- def call(*args, **kwargs, &block)
130
+ def call(*args, **kwargs, &)
131
131
  if kwargs.empty?
132
- Cuprum::Utils::InstanceSpy.send(:call_spies_for, self, *args, &block)
132
+ Cuprum::Utils::InstanceSpy.send(:call_spies_for, self, *args, &)
133
133
  else
134
134
  # :nocov:
135
135
  Cuprum::Utils::InstanceSpy
136
- .send(:call_spies_for, self, *args, **kwargs, &block)
136
+ .send(:call_spies_for, self, *args, **kwargs, &)
137
137
  # :nocov:
138
138
  end
139
139
 
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ require 'cuprum/utils'
6
+
7
+ module Cuprum::Utils
8
+ # Utility class mapping a method's parameters by parameter name.
9
+ class ParametersMapping
10
+ # Generates a parameters mapping for the given method or Proc.
11
+ #
12
+ # @param callable [#parameters] the method or Proc for which to map
13
+ # parameters.
14
+ #
15
+ # @return [ParametersMapping] the mapping for the callable object.
16
+ def self.build(callable) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
17
+ arguments = []
18
+ block = nil
19
+ keywords = []
20
+ variadic_arguments = nil
21
+ variadic_keywords = nil
22
+
23
+ callable.parameters.each do |(type, name)|
24
+ next if name.nil?
25
+
26
+ case type
27
+ when :opt, :req
28
+ arguments << name
29
+ when :key, :keyreq
30
+ keywords << name
31
+ when :rest
32
+ variadic_arguments = name
33
+ when :keyrest
34
+ variadic_keywords = name
35
+ when :block
36
+ block = name
37
+ end
38
+ end
39
+
40
+ new(
41
+ arguments:,
42
+ block:,
43
+ keywords:,
44
+ variadic_arguments:,
45
+ variadic_keywords:
46
+ )
47
+ end
48
+
49
+ # @param arguments [Array<Symbol>] the named arguments to the method, both
50
+ # required and optional, but excluding variadic args).
51
+ # @param block [Symbol, nil] the name of the block parameter, if any.
52
+ # @param keywords [Array<Symbol>] the keywords for the method, both
53
+ # required and optional, but excluding variadic keywords.
54
+ # @param variadic_arguments [Symbol] the name of the variadic arguments
55
+ # parameter, if any.
56
+ # @param variadic_keywords [Symbol] the name of the variadic keywords
57
+ # parameter, if any.
58
+ def initialize(
59
+ arguments: [],
60
+ keywords: [],
61
+ block: nil,
62
+ variadic_arguments: nil,
63
+ variadic_keywords: nil
64
+ )
65
+ @arguments = arguments.map(&:to_sym).freeze
66
+ @block = block&.to_sym
67
+ @keywords = Set.new(keywords.map(&:to_sym)).freeze
68
+ @variadic_arguments = variadic_arguments&.to_sym
69
+ @variadic_keywords = variadic_keywords&.to_sym
70
+ end
71
+
72
+ # @return [Hash{Symbol=>Integer}] the named arguments to the method, both
73
+ # required and optional, but excluding variadic args).
74
+ attr_reader :arguments
75
+
76
+ # @return [Symbol, nil] the name of the block parameter, if any.
77
+ attr_reader :block
78
+
79
+ # @return [Set<Symbol>] the keywords for the method, both required and
80
+ # optional, but excluding variadic keywords.
81
+ attr_reader :keywords
82
+
83
+ # @return [Symbol] the name of the variadic arguments parameter, if any.
84
+ attr_reader :variadic_arguments
85
+
86
+ # @return [Symbol] the name of the variadic keywords parameter, if any.
87
+ attr_reader :variadic_keywords
88
+
89
+ # @return [Integer] the number of named arguments.
90
+ def arguments_count
91
+ @arguments_count ||= arguments.size
92
+ end
93
+
94
+ # @return [true, false] true if the method has a block parameter; otherwise
95
+ # false.
96
+ def block?
97
+ !@block.nil?
98
+ end
99
+
100
+ # @overload call(*arguments, **keywords, &block)
101
+ # Maps the given parameters to a Hash of parameter names and values.
102
+ #
103
+ # @param arguments [Array] the positional parameters to map.
104
+ # @param keywords [Hash] the keyword parameters to map.
105
+ # @param block [Proc] the block parameter to map, if any.
106
+ #
107
+ # @return [Hash{Symbol=>Object}] the mapped parameters.
108
+ def call(*args, **kwargs, &block_arg) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
109
+ params = {}
110
+ extras = {}
111
+
112
+ arguments.each.with_index { |name, index| params[name] = args[index] }
113
+
114
+ if variadic_arguments?
115
+ params[variadic_arguments] = args[arguments_count..] || []
116
+ end
117
+
118
+ keywords.each { |name| params[name] = nil }
119
+
120
+ kwargs.each do |(name, value)|
121
+ (keywords.include?(name) ? params : extras)[name] = value
122
+ end
123
+
124
+ params[variadic_keywords] = extras if variadic_keywords?
125
+
126
+ params[block] = block_arg if block?
127
+
128
+ params
129
+ end
130
+
131
+ # @return [true, false] true if the method has a variadic arguments
132
+ # parameter; otherwise false.
133
+ def variadic_arguments?
134
+ !@variadic_arguments.nil?
135
+ end
136
+
137
+ # @return [true, false] true if the method has a variadic keywords
138
+ # parameter; otherwise false.
139
+ def variadic_keywords?
140
+ !@variadic_keywords.nil?
141
+ end
142
+ end
143
+ end
@@ -10,7 +10,7 @@ module Cuprum
10
10
  # Major version.
11
11
  MAJOR = 1
12
12
  # Minor version.
13
- MINOR = 2
13
+ MINOR = 3
14
14
  # Patch version.
15
15
  PATCH = 0
16
16
  # Prerelease version.
data/lib/cuprum.rb CHANGED
@@ -2,20 +2,26 @@
2
2
 
3
3
  # Toolkit for implementing business logic as function objects.
4
4
  module Cuprum
5
- autoload :Command, 'cuprum/command'
6
- autoload :Error, 'cuprum/error'
7
- autoload :MapCommand, 'cuprum/map_command'
8
- autoload :Matcher, 'cuprum/matcher'
9
- autoload :Middleware, 'cuprum/middleware'
10
- autoload :Operation, 'cuprum/operation'
11
- autoload :Result, 'cuprum/result'
12
- autoload :ResultList, 'cuprum/result_list'
13
- autoload :Steps, 'cuprum/steps'
5
+ autoload :Command, 'cuprum/command'
6
+ autoload :CommandFactory, 'cuprum/command_factory'
7
+ autoload :Currying, 'cuprum/currying'
8
+ autoload :Error, 'cuprum/error'
9
+ autoload :ExceptionHandling, 'cuprum/exception_handling'
10
+ autoload :MapCommand, 'cuprum/map_command'
11
+ autoload :Matcher, 'cuprum/matcher'
12
+ autoload :Middleware, 'cuprum/middleware'
13
+ autoload :Operation, 'cuprum/operation'
14
+ autoload :ParameterValidation, 'cuprum/parameter_validation'
15
+ autoload :Result, 'cuprum/result'
16
+ autoload :ResultList, 'cuprum/result_list'
17
+ autoload :Steps, 'cuprum/steps'
14
18
 
15
19
  class << self
16
- # @return [String] The current version of the gem.
20
+ # @return [String] the current version of the gem.
17
21
  def version
18
22
  VERSION
19
23
  end
20
24
  end
21
25
  end
26
+
27
+ require 'cuprum/version'