stannum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +105 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1327 -0
  7. data/config/locales/en.rb +47 -0
  8. data/lib/stannum/attribute.rb +115 -0
  9. data/lib/stannum/constraint.rb +65 -0
  10. data/lib/stannum/constraints/absence.rb +42 -0
  11. data/lib/stannum/constraints/anything.rb +28 -0
  12. data/lib/stannum/constraints/base.rb +285 -0
  13. data/lib/stannum/constraints/boolean.rb +33 -0
  14. data/lib/stannum/constraints/delegator.rb +71 -0
  15. data/lib/stannum/constraints/enum.rb +64 -0
  16. data/lib/stannum/constraints/equality.rb +47 -0
  17. data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
  18. data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
  19. data/lib/stannum/constraints/hashes.rb +11 -0
  20. data/lib/stannum/constraints/identity.rb +46 -0
  21. data/lib/stannum/constraints/nothing.rb +28 -0
  22. data/lib/stannum/constraints/presence.rb +42 -0
  23. data/lib/stannum/constraints/signature.rb +92 -0
  24. data/lib/stannum/constraints/signatures/map.rb +17 -0
  25. data/lib/stannum/constraints/signatures/tuple.rb +17 -0
  26. data/lib/stannum/constraints/signatures.rb +11 -0
  27. data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
  28. data/lib/stannum/constraints/tuples.rb +10 -0
  29. data/lib/stannum/constraints/type.rb +113 -0
  30. data/lib/stannum/constraints/types/array_type.rb +148 -0
  31. data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
  32. data/lib/stannum/constraints/types/date_time_type.rb +16 -0
  33. data/lib/stannum/constraints/types/date_type.rb +16 -0
  34. data/lib/stannum/constraints/types/float_type.rb +14 -0
  35. data/lib/stannum/constraints/types/hash_type.rb +205 -0
  36. data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
  37. data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
  38. data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
  39. data/lib/stannum/constraints/types/integer_type.rb +14 -0
  40. data/lib/stannum/constraints/types/nil_type.rb +20 -0
  41. data/lib/stannum/constraints/types/proc_type.rb +14 -0
  42. data/lib/stannum/constraints/types/string_type.rb +14 -0
  43. data/lib/stannum/constraints/types/symbol_type.rb +14 -0
  44. data/lib/stannum/constraints/types/time_type.rb +14 -0
  45. data/lib/stannum/constraints/types.rb +25 -0
  46. data/lib/stannum/constraints/union.rb +85 -0
  47. data/lib/stannum/constraints.rb +26 -0
  48. data/lib/stannum/contract.rb +243 -0
  49. data/lib/stannum/contracts/array_contract.rb +108 -0
  50. data/lib/stannum/contracts/base.rb +597 -0
  51. data/lib/stannum/contracts/builder.rb +72 -0
  52. data/lib/stannum/contracts/definition.rb +74 -0
  53. data/lib/stannum/contracts/hash_contract.rb +136 -0
  54. data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
  55. data/lib/stannum/contracts/map_contract.rb +199 -0
  56. data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
  57. data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
  58. data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
  59. data/lib/stannum/contracts/parameters.rb +15 -0
  60. data/lib/stannum/contracts/parameters_contract.rb +530 -0
  61. data/lib/stannum/contracts/tuple_contract.rb +213 -0
  62. data/lib/stannum/contracts.rb +19 -0
  63. data/lib/stannum/errors.rb +730 -0
  64. data/lib/stannum/messages/default_strategy.rb +124 -0
  65. data/lib/stannum/messages.rb +25 -0
  66. data/lib/stannum/parameter_validation.rb +216 -0
  67. data/lib/stannum/rspec/match_errors.rb +17 -0
  68. data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
  69. data/lib/stannum/rspec/validate_parameter.rb +23 -0
  70. data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
  71. data/lib/stannum/rspec.rb +8 -0
  72. data/lib/stannum/schema.rb +131 -0
  73. data/lib/stannum/struct.rb +444 -0
  74. data/lib/stannum/support/coercion.rb +114 -0
  75. data/lib/stannum/support/optional.rb +69 -0
  76. data/lib/stannum/support.rb +8 -0
  77. data/lib/stannum/version.rb +57 -0
  78. data/lib/stannum.rb +27 -0
  79. 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum'
4
+
5
+ module Stannum
6
+ # Namespace for RSpec extensions for testing Stannum applications.
7
+ module RSpec; end
8
+ 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