stannum 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/DEVELOPMENT.md +105 -0
- data/LICENSE +22 -0
- data/README.md +1327 -0
- data/config/locales/en.rb +47 -0
- data/lib/stannum/attribute.rb +115 -0
- data/lib/stannum/constraint.rb +65 -0
- data/lib/stannum/constraints/absence.rb +42 -0
- data/lib/stannum/constraints/anything.rb +28 -0
- data/lib/stannum/constraints/base.rb +285 -0
- data/lib/stannum/constraints/boolean.rb +33 -0
- data/lib/stannum/constraints/delegator.rb +71 -0
- data/lib/stannum/constraints/enum.rb +64 -0
- data/lib/stannum/constraints/equality.rb +47 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
- data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
- data/lib/stannum/constraints/hashes.rb +11 -0
- data/lib/stannum/constraints/identity.rb +46 -0
- data/lib/stannum/constraints/nothing.rb +28 -0
- data/lib/stannum/constraints/presence.rb +42 -0
- data/lib/stannum/constraints/signature.rb +92 -0
- data/lib/stannum/constraints/signatures/map.rb +17 -0
- data/lib/stannum/constraints/signatures/tuple.rb +17 -0
- data/lib/stannum/constraints/signatures.rb +11 -0
- data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
- data/lib/stannum/constraints/tuples.rb +10 -0
- data/lib/stannum/constraints/type.rb +113 -0
- data/lib/stannum/constraints/types/array_type.rb +148 -0
- data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
- data/lib/stannum/constraints/types/date_time_type.rb +16 -0
- data/lib/stannum/constraints/types/date_type.rb +16 -0
- data/lib/stannum/constraints/types/float_type.rb +14 -0
- data/lib/stannum/constraints/types/hash_type.rb +205 -0
- data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
- data/lib/stannum/constraints/types/integer_type.rb +14 -0
- data/lib/stannum/constraints/types/nil_type.rb +20 -0
- data/lib/stannum/constraints/types/proc_type.rb +14 -0
- data/lib/stannum/constraints/types/string_type.rb +14 -0
- data/lib/stannum/constraints/types/symbol_type.rb +14 -0
- data/lib/stannum/constraints/types/time_type.rb +14 -0
- data/lib/stannum/constraints/types.rb +25 -0
- data/lib/stannum/constraints/union.rb +85 -0
- data/lib/stannum/constraints.rb +26 -0
- data/lib/stannum/contract.rb +243 -0
- data/lib/stannum/contracts/array_contract.rb +108 -0
- data/lib/stannum/contracts/base.rb +597 -0
- data/lib/stannum/contracts/builder.rb +72 -0
- data/lib/stannum/contracts/definition.rb +74 -0
- data/lib/stannum/contracts/hash_contract.rb +136 -0
- data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
- data/lib/stannum/contracts/map_contract.rb +199 -0
- data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
- data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
- data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
- data/lib/stannum/contracts/parameters.rb +15 -0
- data/lib/stannum/contracts/parameters_contract.rb +530 -0
- data/lib/stannum/contracts/tuple_contract.rb +213 -0
- data/lib/stannum/contracts.rb +19 -0
- data/lib/stannum/errors.rb +730 -0
- data/lib/stannum/messages/default_strategy.rb +124 -0
- data/lib/stannum/messages.rb +25 -0
- data/lib/stannum/parameter_validation.rb +216 -0
- data/lib/stannum/rspec/match_errors.rb +17 -0
- data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
- data/lib/stannum/rspec/validate_parameter.rb +23 -0
- data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
- data/lib/stannum/rspec.rb +8 -0
- data/lib/stannum/schema.rb +131 -0
- data/lib/stannum/struct.rb +444 -0
- data/lib/stannum/support/coercion.rb +114 -0
- data/lib/stannum/support/optional.rb +69 -0
- data/lib/stannum/support.rb +8 -0
- data/lib/stannum/version.rb +57 -0
- data/lib/stannum.rb +27 -0
- metadata +216 -0
@@ -0,0 +1,506 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/sleeping_king_studios/matchers/core/deep_matcher'
|
5
|
+
rescue NameError
|
6
|
+
# Optional dependency.
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'stannum/rspec'
|
10
|
+
require 'stannum/support/coercion'
|
11
|
+
|
12
|
+
module Stannum::RSpec
|
13
|
+
# Asserts that the command validates the given method parameter.
|
14
|
+
class ValidateParameterMatcher # rubocop:disable Metrics/ClassLength
|
15
|
+
include RSpec::Mocks::ExampleMethods
|
16
|
+
|
17
|
+
class InvalidParameterHandledError < StandardError; end
|
18
|
+
private_constant :InvalidParameterHandledError
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# @private
|
22
|
+
def add_parameter_mapping(map:, match:)
|
23
|
+
raise ArgumentError, 'map must be a Proc' unless map.is_a?(Proc)
|
24
|
+
raise ArgumentError, 'match must be a Proc' unless match.is_a?(Proc)
|
25
|
+
|
26
|
+
parameter_mappings << { match: match, map: map }
|
27
|
+
end
|
28
|
+
|
29
|
+
# @private
|
30
|
+
def map_parameters(actual:, method_name:)
|
31
|
+
parameter_mappings.each do |keywords|
|
32
|
+
match = keywords.fetch(:match)
|
33
|
+
map = keywords.fetch(:map)
|
34
|
+
|
35
|
+
next unless match.call(actual: actual, method_name: method_name)
|
36
|
+
|
37
|
+
return map.call(actual: actual, method_name: method_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
unwrapped_method(actual: actual, method_name: method_name).parameters
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_parameter_mappings
|
46
|
+
[
|
47
|
+
{
|
48
|
+
match: lambda do |actual:, method_name:, **_|
|
49
|
+
actual.is_a?(Class) && method_name == :new
|
50
|
+
end,
|
51
|
+
map: lambda do |actual:, **_|
|
52
|
+
actual.instance_method(:initialize).parameters
|
53
|
+
end
|
54
|
+
}
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
def parameter_mappings
|
59
|
+
@parameter_mappings ||= default_parameter_mappings
|
60
|
+
end
|
61
|
+
|
62
|
+
def unwrapped_method(actual:, method_name:)
|
63
|
+
method = actual.method(method_name)
|
64
|
+
validations = Stannum::ParameterValidation::MethodValidations
|
65
|
+
|
66
|
+
until method.nil?
|
67
|
+
return method unless method.owner.is_a?(validations)
|
68
|
+
|
69
|
+
method = method.super_method
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param method_name [String, Symbol] The name of the method with validated
|
75
|
+
# parameters.
|
76
|
+
# @param parameter_name [String, Symbol] The name of the validated method
|
77
|
+
# parameter.
|
78
|
+
def initialize(method_name:, parameter_name:)
|
79
|
+
@method_name = method_name.intern
|
80
|
+
@parameter_name = parameter_name.intern
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Stannum::Constraints::Base, nil] the constraint used to generate
|
84
|
+
# the expected error(s).
|
85
|
+
attr_reader :expected_constraint
|
86
|
+
|
87
|
+
# @return [String, Symbol] the name of the method with validated parameters.
|
88
|
+
attr_reader :method_name
|
89
|
+
|
90
|
+
# @return [Hash] the configured parameters to match.
|
91
|
+
attr_reader :parameters
|
92
|
+
|
93
|
+
# @return [String, Symbol] the name of the validated method parameter.
|
94
|
+
attr_reader :parameter_name
|
95
|
+
|
96
|
+
# @return [Object] the invalid value for the validated parameter.
|
97
|
+
attr_reader :parameter_value
|
98
|
+
|
99
|
+
# @return [String] a short description of the matcher and expected
|
100
|
+
# properties.
|
101
|
+
def description
|
102
|
+
"validate the #{parameter_name.inspect} #{parameter_type || 'parameter'}"
|
103
|
+
end
|
104
|
+
|
105
|
+
# Asserts that the object does not validate the specified method parameter.
|
106
|
+
#
|
107
|
+
# @param actual [Object] The object to match.
|
108
|
+
#
|
109
|
+
# @return [true, false] false if the object validates the parameter,
|
110
|
+
# otherwise true.
|
111
|
+
def does_not_match?(actual)
|
112
|
+
disallow_fluent_options!
|
113
|
+
|
114
|
+
@actual = actual
|
115
|
+
@failure_reason = nil
|
116
|
+
|
117
|
+
return true unless supports_parameter_validation?
|
118
|
+
return false unless responds_to_method?
|
119
|
+
return true unless validates_method?
|
120
|
+
return false unless method_has_parameter?
|
121
|
+
|
122
|
+
!validates_method_parameter?
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [String] a summary message describing a failed expectation.
|
126
|
+
def failure_message # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
127
|
+
message = "expected ##{method_name} to #{description}"
|
128
|
+
reason =
|
129
|
+
case @failure_reason
|
130
|
+
when :does_not_respond_to_method
|
131
|
+
"the object does not respond to ##{method_name}"
|
132
|
+
when :does_not_support_parameter_validation
|
133
|
+
'the object does not implement parameter validation'
|
134
|
+
when :does_not_validate_method
|
135
|
+
"the object does not validate the parameters of ##{method_name}"
|
136
|
+
when :errors_do_not_match
|
137
|
+
"the errors do not match:\n\n#{equality_matcher_failure_message}"
|
138
|
+
when :method_does_not_have_parameter
|
139
|
+
"##{method_name} does not have a #{parameter_name.inspect} parameter"
|
140
|
+
when :parameter_not_validated
|
141
|
+
"##{method_name} does not expect a #{parameter_name.inspect}" \
|
142
|
+
" #{parameter_type}"
|
143
|
+
when :valid_parameter_value
|
144
|
+
"#{valid_value.inspect} is a valid value for the" \
|
145
|
+
" #{parameter_name.inspect} #{parameter_type}"
|
146
|
+
end
|
147
|
+
|
148
|
+
[message, reason].compact.join(', but ')
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [String] a summary message describing a failed negated
|
152
|
+
# expectation.
|
153
|
+
def failure_message_when_negated
|
154
|
+
message = "expected ##{method_name} not to #{description}"
|
155
|
+
reason =
|
156
|
+
case @failure_reason
|
157
|
+
when :does_not_respond_to_method
|
158
|
+
"the object does not respond to ##{method_name}"
|
159
|
+
when :method_does_not_have_parameter
|
160
|
+
"##{method_name} does not have a #{parameter_name.inspect} parameter"
|
161
|
+
end
|
162
|
+
|
163
|
+
[message, reason].compact.join(', but ')
|
164
|
+
end
|
165
|
+
|
166
|
+
# Asserts that the object validates the specified method parameter.
|
167
|
+
#
|
168
|
+
# @param actual [Object] The object to match.
|
169
|
+
#
|
170
|
+
# @return [true, false] true if the object validates the parameter,
|
171
|
+
# otherwise false.
|
172
|
+
def matches?(actual) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
173
|
+
@actual = actual
|
174
|
+
@failure_reason = nil
|
175
|
+
|
176
|
+
return false unless supports_parameter_validation?
|
177
|
+
return false unless responds_to_method?
|
178
|
+
return false unless validates_method?
|
179
|
+
return false unless method_has_parameter?
|
180
|
+
|
181
|
+
if @expected_constraint.nil? && @parameters.nil? && @parameter_value.nil?
|
182
|
+
return validates_method_parameter?
|
183
|
+
end
|
184
|
+
|
185
|
+
call_validated_method
|
186
|
+
|
187
|
+
return false if extra_parameter?
|
188
|
+
return false if valid_parameter?
|
189
|
+
|
190
|
+
matches_expected_error?
|
191
|
+
end
|
192
|
+
|
193
|
+
# Specifies a constraint or type used to validate the parameter.
|
194
|
+
#
|
195
|
+
# @param constraint [Stannum::Constraints::Base, Class, Module] The
|
196
|
+
# constraint or type.
|
197
|
+
#
|
198
|
+
# @return [Stannum::RSpec::ValidateParameterMatcher] the matcher.
|
199
|
+
def using_constraint(constraint, **options)
|
200
|
+
@expected_constraint = Stannum::Support::Coercion.type_constraint(
|
201
|
+
constraint,
|
202
|
+
as: 'constraint',
|
203
|
+
**options
|
204
|
+
)
|
205
|
+
|
206
|
+
self
|
207
|
+
end
|
208
|
+
|
209
|
+
# Specifies custom parameters to test.
|
210
|
+
#
|
211
|
+
# The matcher will pass if and only if the method fails validation with the
|
212
|
+
# specified parameters.
|
213
|
+
#
|
214
|
+
# @param arguments [Array] A list of arguments to test.
|
215
|
+
# @param keywords [Hash] A hash of keywords to test.
|
216
|
+
#
|
217
|
+
# @return [Stannum::RSpec::ValidateParameterMatcher] the matcher.
|
218
|
+
def with_parameters(*arguments, **keywords, &block)
|
219
|
+
if @parameter_value
|
220
|
+
raise 'cannot use both #with_parameters and #with_value'
|
221
|
+
end
|
222
|
+
|
223
|
+
@parameters = [
|
224
|
+
arguments,
|
225
|
+
keywords,
|
226
|
+
block
|
227
|
+
]
|
228
|
+
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
# Specifies an invalid value for the parameter.
|
233
|
+
#
|
234
|
+
# @param parameter_value [Object] The invalid value for the validated
|
235
|
+
# parameter.
|
236
|
+
#
|
237
|
+
# @return [Stannum::RSpec::ValidateParameterMatcher] the matcher.
|
238
|
+
def with_value(parameter_value)
|
239
|
+
raise 'cannot use both #with_parameters and #with_value' if @parameters
|
240
|
+
|
241
|
+
@parameter_value = parameter_value
|
242
|
+
|
243
|
+
self
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
attr_reader :actual
|
249
|
+
|
250
|
+
attr_reader :parameter_index
|
251
|
+
|
252
|
+
attr_reader :parameter_type
|
253
|
+
|
254
|
+
def build_parameters
|
255
|
+
case parameter_type
|
256
|
+
when :argument
|
257
|
+
@parameter_index = find_parameter_index(method_parameters)
|
258
|
+
|
259
|
+
[[*Array.new(parameter_index, nil), parameter_value], {}, nil]
|
260
|
+
when :keyword
|
261
|
+
[[], { parameter_name => parameter_value }, nil]
|
262
|
+
when :block
|
263
|
+
[[], {}, parameter_value]
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def call_validated_method
|
268
|
+
arguments, keywords, block = @parameters || build_parameters
|
269
|
+
|
270
|
+
mock_validation_handler do
|
271
|
+
actual.send(method_name, *arguments, **keywords, &block)
|
272
|
+
rescue InvalidParameterHandledError
|
273
|
+
# Do nothing.
|
274
|
+
end
|
275
|
+
rescue ArgumentError
|
276
|
+
# Do nothing.
|
277
|
+
end
|
278
|
+
|
279
|
+
def disallow_fluent_options! # rubocop:disable Metrics/MethodLength
|
280
|
+
unless @expected_constraint.nil?
|
281
|
+
raise RuntimeError,
|
282
|
+
'#does_not_match? with #using_constraint is not supported',
|
283
|
+
caller[1..-1]
|
284
|
+
end
|
285
|
+
|
286
|
+
unless @parameters.nil?
|
287
|
+
raise RuntimeError,
|
288
|
+
'#does_not_match? with #with_parameters is not supported',
|
289
|
+
caller[1..-1]
|
290
|
+
end
|
291
|
+
|
292
|
+
return if @parameter_value.nil?
|
293
|
+
|
294
|
+
raise RuntimeError,
|
295
|
+
'#does_not_match? with #with_value is not supported',
|
296
|
+
caller[1..-1]
|
297
|
+
end
|
298
|
+
|
299
|
+
def equality_matcher
|
300
|
+
RSpec::SleepingKingStudios::Matchers::Core::DeepMatcher
|
301
|
+
.new(@expected_errors.to_a)
|
302
|
+
rescue NameError
|
303
|
+
# :nocov:
|
304
|
+
RSpec::Matchers::BuiltIn::Eq.new(@expected_errors.to_a)
|
305
|
+
# :nocov:
|
306
|
+
end
|
307
|
+
|
308
|
+
def equality_matcher_failure_message
|
309
|
+
equality_matcher
|
310
|
+
.tap { |matcher| matcher.matches?(scoped_errors.to_a) }
|
311
|
+
.failure_message
|
312
|
+
end
|
313
|
+
|
314
|
+
def extra_parameter?
|
315
|
+
extra_arguments_type =
|
316
|
+
Stannum::Contracts::Parameters::ArgumentsContract::EXTRA_ARGUMENTS_TYPE
|
317
|
+
extra_keywords_type =
|
318
|
+
Stannum::Contracts::Parameters::KeywordsContract::EXTRA_KEYWORDS_TYPE
|
319
|
+
|
320
|
+
return false unless scoped_errors(indexed: true).any? do |error|
|
321
|
+
error[:type] == extra_arguments_type ||
|
322
|
+
error[:type] == extra_keywords_type
|
323
|
+
end
|
324
|
+
|
325
|
+
@failure_reason = :parameter_not_validated
|
326
|
+
|
327
|
+
true
|
328
|
+
end
|
329
|
+
|
330
|
+
def find_parameter_index(parameters)
|
331
|
+
parameters.index { |_, name| name == parameter_name }
|
332
|
+
end
|
333
|
+
|
334
|
+
def find_parameter_type
|
335
|
+
parameters = method_parameters
|
336
|
+
type, _ = parameters.find { |_, name| name == parameter_name }
|
337
|
+
|
338
|
+
case type
|
339
|
+
when :req, :opt
|
340
|
+
:argument
|
341
|
+
when :keyreq, :key
|
342
|
+
:keyword
|
343
|
+
when :block
|
344
|
+
:block
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def matches_expected_error?
|
349
|
+
return true unless expected_constraint
|
350
|
+
|
351
|
+
@expected_errors = expected_constraint.errors_for(parameter_value)
|
352
|
+
|
353
|
+
if @expected_errors.all? { |error| scoped_errors.include?(error) }
|
354
|
+
return true
|
355
|
+
end
|
356
|
+
|
357
|
+
@failure_reason = :errors_do_not_match
|
358
|
+
|
359
|
+
false
|
360
|
+
end
|
361
|
+
|
362
|
+
def method_has_parameter?
|
363
|
+
@parameter_type = find_parameter_type
|
364
|
+
|
365
|
+
return true unless parameter_type.nil?
|
366
|
+
|
367
|
+
@failure_reason = :method_does_not_have_parameter
|
368
|
+
|
369
|
+
false
|
370
|
+
end
|
371
|
+
|
372
|
+
def method_parameters
|
373
|
+
@method_parameters ||=
|
374
|
+
self.class.map_parameters(actual: actual, method_name: method_name)
|
375
|
+
end
|
376
|
+
|
377
|
+
def mock_validation_handler
|
378
|
+
@validation_handler_called = false
|
379
|
+
@validation_errors = nil
|
380
|
+
|
381
|
+
allow(actual).to receive(:handle_invalid_parameters) do |keywords|
|
382
|
+
@validation_handler_called = true
|
383
|
+
@validation_errors = keywords[:errors]
|
384
|
+
|
385
|
+
raise InvalidParameterHandledError
|
386
|
+
end
|
387
|
+
|
388
|
+
yield
|
389
|
+
|
390
|
+
allow(actual).to receive(:handle_invalid_parameters).and_call_original
|
391
|
+
end
|
392
|
+
|
393
|
+
def responds_to_method?
|
394
|
+
return true if actual.respond_to?(method_name)
|
395
|
+
|
396
|
+
@failure_reason = :does_not_respond_to_method
|
397
|
+
|
398
|
+
false
|
399
|
+
end
|
400
|
+
|
401
|
+
def scoped_errors(indexed: false) # rubocop:disable Metrics/MethodLength
|
402
|
+
return [] if @validation_errors.nil?
|
403
|
+
|
404
|
+
case parameter_type
|
405
|
+
when :argument
|
406
|
+
@parameter_index ||= find_parameter_index(method_parameters)
|
407
|
+
parameter_key = indexed ? parameter_index : parameter_name
|
408
|
+
|
409
|
+
@validation_errors[:arguments][parameter_key]
|
410
|
+
when :keyword
|
411
|
+
@validation_errors[:keywords][parameter_name]
|
412
|
+
when :block
|
413
|
+
@validation_errors[:block]
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def supports_parameter_validation?
|
418
|
+
return true if actual.is_a?(Stannum::ParameterValidation)
|
419
|
+
|
420
|
+
@failure_reason = :does_not_support_parameter_validation
|
421
|
+
|
422
|
+
false
|
423
|
+
end
|
424
|
+
|
425
|
+
def valid_parameter?
|
426
|
+
return false unless scoped_errors.empty?
|
427
|
+
|
428
|
+
@failure_reason = :valid_parameter_value
|
429
|
+
|
430
|
+
true
|
431
|
+
end
|
432
|
+
|
433
|
+
def valid_value # rubocop:disable Metrics/MethodLength
|
434
|
+
return @parameter_value if @parameter_value
|
435
|
+
|
436
|
+
return nil unless @parameters
|
437
|
+
|
438
|
+
case parameter_type
|
439
|
+
when :argument
|
440
|
+
@parameter_index ||= find_parameter_index(method_parameters)
|
441
|
+
|
442
|
+
parameters[0][parameter_index]
|
443
|
+
when :keyword
|
444
|
+
parameters[1][parameter_name]
|
445
|
+
when :block
|
446
|
+
parameters[2]
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def validates_method?
|
451
|
+
return true if validation_contracts.include?(method_name)
|
452
|
+
|
453
|
+
@failure_reason = :does_not_validate_method
|
454
|
+
|
455
|
+
false
|
456
|
+
end
|
457
|
+
|
458
|
+
def validates_method_argument?
|
459
|
+
contract = validation_contracts.fetch(method_name)
|
460
|
+
|
461
|
+
contract.send(:arguments_contract).each_constraint.any? do |definition|
|
462
|
+
definition.property_name == parameter_name
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def validates_method_block?
|
467
|
+
contract = validation_contracts.fetch(method_name)
|
468
|
+
|
469
|
+
!contract.send(:block_constraint).nil?
|
470
|
+
end
|
471
|
+
|
472
|
+
def validates_method_keyword?
|
473
|
+
contract = validation_contracts.fetch(method_name)
|
474
|
+
|
475
|
+
contract.send(:keywords_contract).each_constraint.any? do |definition|
|
476
|
+
definition.property_name == parameter_name
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
def validates_method_parameter? # rubocop:disable Metrics/MethodLength
|
481
|
+
validates_parameter =
|
482
|
+
case parameter_type
|
483
|
+
when :argument
|
484
|
+
validates_method_argument?
|
485
|
+
when :keyword
|
486
|
+
validates_method_keyword?
|
487
|
+
when :block
|
488
|
+
validates_method_block?
|
489
|
+
end
|
490
|
+
|
491
|
+
return true if validates_parameter
|
492
|
+
|
493
|
+
@failure_reason = :parameter_not_validated
|
494
|
+
|
495
|
+
false
|
496
|
+
end
|
497
|
+
|
498
|
+
def validation_contracts
|
499
|
+
if actual.is_a?(Module)
|
500
|
+
actual.singleton_class::MethodValidations.contracts
|
501
|
+
else
|
502
|
+
actual.class::MethodValidations.contracts
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require 'stannum/attribute'
|
6
|
+
|
7
|
+
module Stannum
|
8
|
+
# Abstract class for defining attribute methods for a struct.
|
9
|
+
#
|
10
|
+
# @see Stannum::Attribute.
|
11
|
+
class Schema < Module
|
12
|
+
extend Forwardable
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
super
|
17
|
+
|
18
|
+
@attributes = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# @!method each
|
22
|
+
# Iterates through the the attributes by name and attribute object.
|
23
|
+
#
|
24
|
+
# @yieldparam name [String] The name of the attribute.
|
25
|
+
# @yieldparam attribute [Stannum::Attribute] The attribute object.
|
26
|
+
|
27
|
+
# @!method each_key
|
28
|
+
# Iterates through the the attributes by name.
|
29
|
+
#
|
30
|
+
# @yieldparam name [String] The name of the attribute.
|
31
|
+
|
32
|
+
# @!method each_value
|
33
|
+
# Iterates through the the attributes by attribute object.
|
34
|
+
#
|
35
|
+
# @yieldparam attribute [Stannum::Attribute] The attribute object.
|
36
|
+
|
37
|
+
def_delegators :attributes,
|
38
|
+
:each,
|
39
|
+
:each_key,
|
40
|
+
:each_value
|
41
|
+
|
42
|
+
# Retrieves the named attribute object.
|
43
|
+
#
|
44
|
+
# @param key [String, Symbol] The name of the requested attribute.
|
45
|
+
#
|
46
|
+
# @return [Stannum::Attribute] The attribute object.
|
47
|
+
#
|
48
|
+
# @raise ArgumentError if the key is invalid.
|
49
|
+
# @raise KeyError if the attribute is not defined.
|
50
|
+
def [](key)
|
51
|
+
validate_key(key)
|
52
|
+
|
53
|
+
attributes.fetch(key.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
# rubocop:disable Metrics/MethodLength
|
57
|
+
|
58
|
+
# @api private
|
59
|
+
#
|
60
|
+
# Defines an attribute and adds the attribute to the contract.
|
61
|
+
#
|
62
|
+
# This method should not be called directly. Instead, define attributes via
|
63
|
+
# the Struct.attribute class method.
|
64
|
+
#
|
65
|
+
# @see Stannum::Struct
|
66
|
+
def define_attribute(name:, options:, type:)
|
67
|
+
attribute = Stannum::Attribute.new(
|
68
|
+
name: name,
|
69
|
+
options: options,
|
70
|
+
type: type
|
71
|
+
)
|
72
|
+
|
73
|
+
if @attributes.key?(attribute.name)
|
74
|
+
raise ArgumentError, "attribute #{name.inspect} already exists"
|
75
|
+
end
|
76
|
+
|
77
|
+
define_reader(attribute.name, attribute.reader_name)
|
78
|
+
define_writer(attribute.name, attribute.writer_name, attribute.default)
|
79
|
+
|
80
|
+
@attributes[attribute.name] = attribute
|
81
|
+
end
|
82
|
+
# rubocop:enable Metrics/MethodLength
|
83
|
+
|
84
|
+
# Checks if the given attribute is defined.
|
85
|
+
#
|
86
|
+
# @param key [String, Symbol] the name of the attribute to check.
|
87
|
+
#
|
88
|
+
# @return [Boolean] true if the attribute is defined; otherwise false.
|
89
|
+
def key?(key)
|
90
|
+
validate_key(key)
|
91
|
+
|
92
|
+
attributes.key?(key.to_s)
|
93
|
+
end
|
94
|
+
alias has_key? key?
|
95
|
+
|
96
|
+
# @private
|
97
|
+
def own_attributes
|
98
|
+
@attributes
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def attributes
|
104
|
+
ancestors
|
105
|
+
.reverse_each
|
106
|
+
.select { |mod| mod.is_a?(Stannum::Schema) }
|
107
|
+
.map(&:own_attributes)
|
108
|
+
.reduce(&:merge)
|
109
|
+
end
|
110
|
+
|
111
|
+
def define_reader(attr_name, reader_name)
|
112
|
+
define_method(reader_name) { @attributes[attr_name] }
|
113
|
+
end
|
114
|
+
|
115
|
+
def define_writer(attr_name, writer_name, default_value)
|
116
|
+
define_method(writer_name) do |value|
|
117
|
+
@attributes[attr_name] = value.nil? ? default_value : value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate_key(key)
|
122
|
+
raise ArgumentError, "key can't be blank" if key.nil?
|
123
|
+
|
124
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
125
|
+
raise ArgumentError, 'key must be a String or Symbol'
|
126
|
+
end
|
127
|
+
|
128
|
+
raise ArgumentError, "key can't be blank" if key.to_s.empty?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|