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.
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,444 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbox/mixin'
4
+
5
+ require 'stannum/schema'
6
+
7
+ module Stannum
8
+ # Abstract class for defining objects with structured attributes.
9
+ #
10
+ # @example Defining Attributes
11
+ # class Widget
12
+ # include Stannum::Struct
13
+ #
14
+ # attribute :name, String
15
+ # attribute :description, String, optional: true
16
+ # attribute :quantity, Integer, default: 0
17
+ # end
18
+ #
19
+ # widget = Widget.new(name: 'Self-sealing Stem Bolt')
20
+ # widget.name #=> 'Self-sealing Stem Bolt'
21
+ # widget.description #=> nil
22
+ # widget.quantity #=> 0
23
+ # widget.attributes #=>
24
+ # # {
25
+ # # name: 'Self-sealing Stem Bolt',
26
+ # # description: nil,
27
+ # # quantity: 0
28
+ # # }
29
+ #
30
+ # @example Setting Attributes
31
+ # widget.description = 'A stem bolt, but self sealing.'
32
+ # widget.attributes #=>
33
+ # # {
34
+ # # name: 'Self-sealing Stem Bolt',
35
+ # # description: 'A stem bolt, but self sealing.',
36
+ # # quantity: 0
37
+ # # }
38
+ #
39
+ # widget.assign_attributes(quantity: 50)
40
+ # widget.attributes #=>
41
+ # # {
42
+ # # name: 'Self-sealing Stem Bolt',
43
+ # # description: 'A stem bolt, but self sealing.',
44
+ # # quantity: 50
45
+ # # }
46
+ #
47
+ # widget.attributes = (name: 'Inverse Chronoton Emitter')
48
+ # # {
49
+ # # name: 'Inverse Chronoton Emitter',
50
+ # # description: nil,
51
+ # # quantity: 0
52
+ # # }
53
+ #
54
+ # @example Defining Attribute Constraints
55
+ # Widget::Contract.matches?(quantity: -5) #=> false
56
+ # Widget::Contract.matches?(name: 'Capacitor', quantity: -5) #=> true
57
+ #
58
+ # class Widget
59
+ # constraint(:quantity) { |qty| qty >= 0 }
60
+ # end
61
+ #
62
+ # Widget::Contract.matches?(name: 'Capacitor', quantity: -5) #=> false
63
+ # Widget::Contract.matches?(name: 'Capacitor', quantity: 10) #=> true
64
+ #
65
+ # @example Defining Struct Constraints
66
+ # Widget::Contract.matches?(name: 'Diode') #=> true
67
+ #
68
+ # class Widget
69
+ # constraint { |struct| struct.description&.include?(struct.name) }
70
+ # end
71
+ #
72
+ # Widget::Contract.matches?(name: 'Diode') #=> false
73
+ # Widget::Contract.matches?(
74
+ # name: 'Diode',
75
+ # description: 'A low budget Diode',
76
+ # ) #=> true
77
+ module Struct # rubocop:disable Metrics/ModuleLength
78
+ extend SleepingKingStudios::Tools::Toolbox::Mixin
79
+
80
+ # Class methods to extend the class when including Stannum::Struct.
81
+ module ClassMethods
82
+ # rubocop:disable Metrics/MethodLength
83
+
84
+ # Defines an attribute on the struct.
85
+ #
86
+ # When an attribute is defined, each of the following steps is executed:
87
+ #
88
+ # - Adds the attribute to ::Attributes and the .attributes class method.
89
+ # - Adds the attribute to #attributes and the associated methods, such as
90
+ # #assign_attributes, #[] and #[]=.
91
+ # - Defines reader and writer methods.
92
+ # - Adds a type constraint to ::Attributes::Contract, and indirectly to
93
+ # ::Contract.
94
+ #
95
+ # @param attr_name [String, Symbol] The name of the attribute. Must be a
96
+ # non-empty String or Symbol.
97
+ # @param attr_type [Class, String] The type of the attribute. Must be a
98
+ # Class or Module, or the name of a class or module.
99
+ # @param options [Hash] Additional options for the attribute.
100
+ #
101
+ # @option options [Object] :default The default value for the attribute.
102
+ # Defaults to nil.
103
+ #
104
+ # @return [Symbol] The attribute name as a symbol.
105
+ def attribute(attr_name, attr_type, **options)
106
+ attribute = attributes.define_attribute(
107
+ name: attr_name,
108
+ type: attr_type,
109
+ options: options
110
+ )
111
+ constraint = Stannum::Constraints::Type.new(
112
+ attribute.type,
113
+ required: attribute.required?
114
+ )
115
+
116
+ self::Contract.add_constraint(
117
+ constraint,
118
+ property: attribute.reader_name
119
+ )
120
+
121
+ attr_name.intern
122
+ end
123
+ # rubocop:enable Metrics/MethodLength
124
+
125
+ # @return [Stannum::Schema] The Schema object for the Struct.
126
+ def attributes
127
+ self::Attributes
128
+ end
129
+
130
+ # Defines a constraint on the struct or one of its properties.
131
+ #
132
+ # @overload constraint()
133
+ # Defines a constraint on the struct.
134
+ #
135
+ # A new Stannum::Constraint instance will be generated, passing the
136
+ # block from .constraint to the new constraint. This constraint will be
137
+ # added to the contract.
138
+ #
139
+ # @yieldparam struct [Stannum::Struct] The struct at the time the
140
+ # constraint is evaluated.
141
+ #
142
+ # @overload constraint(constraint)
143
+ # Defines a constraint on the struct.
144
+ #
145
+ # The given constraint is added to the contract. When the contract is
146
+ # evaluated, this constraint will be matched against the struct.
147
+ #
148
+ # @param constraint [Stannum::Constraints::Base] The constraint to add.
149
+ #
150
+ # @overload constraint(attr_name)
151
+ # Defines a constraint on the given attribute or property.
152
+ #
153
+ # A new Stannum::Constraint instance will be generated, passing the
154
+ # block from .constraint to the new constraint. This constraint will be
155
+ # added to the contract.
156
+ #
157
+ # @param attr_name [String, Symbol] The name of the attribute or
158
+ # property to constrain.
159
+ #
160
+ # @yieldparam value [Object] The value of the attribute or property of
161
+ # the struct at the time the constraint is evaluated.
162
+ #
163
+ # @overload constraint(attr_name, constraint)
164
+ # Defines a constraint on the given attribute or property.
165
+ #
166
+ # The given constraint is added to the contract. When the contract is
167
+ # evaluated, this constraint will be matched against the value of the
168
+ # attribute or property.
169
+ #
170
+ # @param attr_name [String, Symbol] The name of the attribute or
171
+ # property to constrain.
172
+ # @param constraint [Stannum::Constraints::Base] The constraint to add.
173
+ def constraint(attr_name = nil, constraint = nil, &block)
174
+ attr_name, constraint = resolve_constraint(attr_name, constraint)
175
+
176
+ if block_given?
177
+ constraint = Stannum::Constraint.new(&block)
178
+ else
179
+ validate_constraint(constraint)
180
+ end
181
+
182
+ contract.add_constraint(constraint, property: attr_name)
183
+ end
184
+
185
+ # @return [Stannum::Contract] The Contract object for the Struct.
186
+ def contract
187
+ self::Contract
188
+ end
189
+
190
+ private
191
+
192
+ # @api private
193
+ #
194
+ # Hook to execute when a struct class is subclassed.
195
+ def inherited(other)
196
+ super
197
+
198
+ Struct.build(other) if other.is_a?(Class)
199
+ end
200
+
201
+ def resolve_constraint(attr_name, constraint)
202
+ return [nil, attr_name] if attr_name.is_a?(Stannum::Constraints::Base)
203
+
204
+ validate_attribute_name(attr_name)
205
+
206
+ [attr_name.nil? ? attr_name : attr_name.intern, constraint]
207
+ end
208
+
209
+ def validate_attribute_name(name)
210
+ return if name.nil?
211
+
212
+ unless name.is_a?(String) || name.is_a?(Symbol)
213
+ raise ArgumentError, 'attribute must be a String or Symbol'
214
+ end
215
+
216
+ raise ArgumentError, "attribute can't be blank" if name.empty?
217
+ end
218
+
219
+ def validate_constraint(constraint)
220
+ raise ArgumentError, "constraint can't be blank" if constraint.nil?
221
+
222
+ return if constraint.is_a?(Stannum::Constraints::Base)
223
+
224
+ raise ArgumentError, 'constraint must be a Stannum::Constraints::Base'
225
+ end
226
+ end
227
+
228
+ class << self
229
+ # @private
230
+ def build(struct_class)
231
+ return if struct_class?(struct_class)
232
+
233
+ initialize_attributes(struct_class)
234
+ initialize_contract(struct_class)
235
+ end
236
+
237
+ private
238
+
239
+ def included(other)
240
+ super
241
+
242
+ Struct.build(other) if other.is_a?(Class)
243
+ end
244
+
245
+ def initialize_attributes(struct_class)
246
+ attributes = Stannum::Schema.new
247
+
248
+ struct_class.const_set(:Attributes, attributes)
249
+
250
+ if struct_class?(struct_class.superclass)
251
+ attributes.include(struct_class.superclass::Attributes)
252
+ end
253
+
254
+ struct_class.include(attributes)
255
+ end
256
+
257
+ def initialize_contract(struct_class)
258
+ contract = Stannum::Contract.new
259
+
260
+ struct_class.const_set(:Contract, contract)
261
+
262
+ return unless struct_class?(struct_class.superclass)
263
+
264
+ contract.concat(struct_class.superclass::Contract)
265
+ end
266
+
267
+ def struct_class?(struct_class)
268
+ struct_class.const_defined?(:Attributes, false)
269
+ end
270
+ end
271
+
272
+ # Initializes the struct with the given attributes.
273
+ #
274
+ # For each key in the attributes hash, the corresponding writer method will
275
+ # be called with the attribute value. If the hash does not include the key
276
+ # for an attribute, or if the value is nil, the attribute will be set to
277
+ # its default value.
278
+ #
279
+ # If the attributes hash includes any keys that do not correspond to an
280
+ # attribute, the struct will raise an error.
281
+ #
282
+ # @param attributes [Hash] The initial attributes for the struct.
283
+ #
284
+ # @see #attributes=
285
+ #
286
+ # @raise ArgumentError if given an invalid attributes hash.
287
+ def initialize(attributes = {})
288
+ @attributes = {}
289
+
290
+ self.attributes = attributes
291
+ end
292
+
293
+ # Compares the struct with the other object.
294
+ #
295
+ # The other object must be an instance of the current class. In addition,
296
+ # the attributes hashes of the two objects must be equal.
297
+ #
298
+ # @return true if the object is a matching struct.
299
+ def ==(other)
300
+ return false unless other.class == self.class
301
+
302
+ raw_attributes == other.raw_attributes
303
+ end
304
+
305
+ # Retrieves the attribute with the given key.
306
+ #
307
+ # @param key [String, Symbol] The attribute key.
308
+ #
309
+ # @return [Object] the value of the attribute.
310
+ #
311
+ # @raise ArgumentError if the key is not a valid attribute.
312
+ def [](key)
313
+ validate_attribute_key(key)
314
+
315
+ send(self.class::Attributes[key].reader_name)
316
+ end
317
+
318
+ # Sets the given attribute to the given value.
319
+ #
320
+ # @param key [String, Symbol] The attribute key.
321
+ # @param value [Object] The value for the attribute.
322
+ #
323
+ # @raise ArgumentError if the key is not a valid attribute.
324
+ def []=(key, value)
325
+ validate_attribute_key(key)
326
+
327
+ send(self.class::Attributes[key].writer_name, value)
328
+ end
329
+
330
+ # Updates the struct's attributes with the given values.
331
+ #
332
+ # This method is used to update some (but not all) of the attributes of the
333
+ # struct. For each key in the hash, it calls the corresponding writer method
334
+ # with the value for that attribute. If the value is nil, this will set the
335
+ # attribute value to the default for that attribute.
336
+ #
337
+ # Any attributes that are not in the given hash are unchanged.
338
+ #
339
+ # If the attributes hash includes any keys that do not correspond to an
340
+ # attribute, the struct will raise an error.
341
+ #
342
+ # @param attributes [Hash] The initial attributes for the struct.
343
+ #
344
+ # @raise ArgumentError if the key is not a valid attribute.
345
+ #
346
+ # @see #attributes=
347
+ def assign_attributes(attributes)
348
+ unless attributes.is_a?(Hash)
349
+ raise ArgumentError, 'attributes must be a Hash'
350
+ end
351
+
352
+ attributes.each do |attr_name, value|
353
+ validate_attribute_key(attr_name)
354
+
355
+ attribute = self.class.attributes[attr_name]
356
+
357
+ send(attribute.writer_name, value)
358
+ end
359
+ end
360
+ alias assign assign_attributes
361
+
362
+ # @return [Hash] the current attributes of the struct.
363
+ def attributes
364
+ tools.hash_tools.deep_dup(@attributes)
365
+ end
366
+ alias to_h attributes
367
+
368
+ # Replaces the struct's attributes with the given values.
369
+ #
370
+ # This method is used to update all of the attributes of the struct. For
371
+ # each attribute, the writer method is called with the value from the hash,
372
+ # or nil if the corresponding key is not present in the hash. Any nil or
373
+ # missing keys set the attribute value to the attribute's default value.
374
+ #
375
+ # If the attributes hash includes any keys that do not correspond to an
376
+ # attribute, the struct will raise an error.
377
+ #
378
+ # @param attributes [Hash] The initial attributes for the struct.
379
+ #
380
+ # @raise ArgumentError if the key is not a valid attribute.
381
+ #
382
+ # @see #assign_attributes
383
+ def attributes=(attributes) # rubocop:disable Metrics/MethodLength
384
+ unless attributes.is_a?(Hash)
385
+ raise ArgumentError, 'attributes must be a Hash'
386
+ end
387
+
388
+ attributes.each_key { |attr_name| validate_attribute_key(attr_name) }
389
+
390
+ self.class::Attributes.each_value do |attribute|
391
+ send(
392
+ attribute.writer_name,
393
+ attributes.fetch(
394
+ attribute.name,
395
+ attributes.fetch(attribute.name.intern, attribute.default)
396
+ )
397
+ )
398
+ end
399
+ end
400
+
401
+ # @return [String] a string representation of the struct and its attributes.
402
+ def inspect # rubocop:disable Metrics/AbcSize
403
+ if self.class.attributes.each_key.size.zero?
404
+ return "#<#{self.class.name}>"
405
+ end
406
+
407
+ buffer = +"#<#{self.class.name}"
408
+
409
+ self.class.attributes.each_key.with_index \
410
+ do |attribute, index|
411
+ buffer << ',' unless index.zero?
412
+ buffer << " #{attribute}: #{@attributes[attribute].inspect}"
413
+ end
414
+
415
+ buffer << '>'
416
+ end
417
+
418
+ protected
419
+
420
+ def raw_attributes
421
+ @attributes
422
+ end
423
+
424
+ private
425
+
426
+ def tools
427
+ SleepingKingStudios::Tools::Toolbelt.instance
428
+ end
429
+
430
+ def validate_attribute_key(key)
431
+ raise ArgumentError, "attribute can't be blank" if key.nil?
432
+
433
+ unless key.is_a?(String) || key.is_a?(Symbol)
434
+ raise ArgumentError, 'attribute must be a String or Symbol'
435
+ end
436
+
437
+ raise ArgumentError, "attribute can't be blank" if key.empty?
438
+
439
+ return if self.class::Attributes.key?(key.to_s)
440
+
441
+ raise ArgumentError, "unknown attribute #{key.inspect}"
442
+ end
443
+ end
444
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/support'
4
+
5
+ module Stannum::Support
6
+ # Shared functionality for coercing values to and from constraints.
7
+ module Coercion
8
+ class << self
9
+ # Coerce a Boolean value to a Presence constraint.
10
+ #
11
+ # @param present [true, false, Stannum::Constraints::Base, nil] The
12
+ # expected presence or absence of the value. If true, will return a
13
+ # Presence constraint. If false, will return an Absence constraint.
14
+ # @param allow_nil [true, false] If true, a nil value will be returned
15
+ # instead of raising an exception.
16
+ # @param as [String] A short name for the coerced value, used in
17
+ # generating an error message. Defaults to "present".
18
+ # @param options [Hash<Symbol, Object>] Configuration options for the
19
+ # constraint. Defaults to an empty Hash.
20
+ #
21
+ # @yield Builds a constraint from true or false. If no block is given,
22
+ # creates a Stannum::Constraints::Presence or
23
+ # Stannum::Constraints::Absence constraint.
24
+ # @yieldparam present [true, false] The expected presence or absence of
25
+ # the value.
26
+ # @yieldparam options [Hash<Symbol, Object>] Configuration options for the
27
+ # constraint. Defaults to an empty Hash.
28
+ # @yieldreturn [Stannum::Constraints::Base] the generated constraint.
29
+ #
30
+ # @return [Stannum::Constraints:Base, nil] the generated or given
31
+ # constraint.
32
+ def presence_constraint(
33
+ present,
34
+ allow_nil: false,
35
+ as: 'present',
36
+ **options,
37
+ &block
38
+ )
39
+ return nil if allow_nil && present.nil?
40
+
41
+ if present.is_a?(Stannum::Constraints::Base)
42
+ return present.with_options(**options)
43
+ end
44
+
45
+ if present == true || present == false # rubocop:disable Style/MultipleComparison
46
+ return build_presence_constraint(present, **options, &block)
47
+ end
48
+
49
+ raise ArgumentError,
50
+ "#{as} must be true or false or a constraint",
51
+ caller(1..-1)
52
+ end
53
+
54
+ # Coerce a Class or Module to a Type constraint.
55
+ #
56
+ # @param value [Class, Module, Stannum::Constraints::Base, nil] The value
57
+ # to coerce.
58
+ # @param allow_nil [true, false] If true, a nil value will be returned
59
+ # instead of raising an exception.
60
+ # @param as [String] A short name for the coerced value, used in
61
+ # generating an error message. Defaults to "type".
62
+ # @param options [Hash<Symbol, Object>] Configuration options for the
63
+ # constraint. Defaults to an empty Hash.
64
+ #
65
+ # @yield Builds a constraint from a Class or Module. If no block is given,
66
+ # creates a Stannum::Constraints::Type constraint.
67
+ # @yieldparam value [Class, Module] The Class or Module used to build the
68
+ # constraint.
69
+ # @yieldparam options [Hash<Symbol, Object>] Configuration options for the
70
+ # constraint. Defaults to an empty Hash.
71
+ # @yieldreturn [Stannum::Constraints::Base] the generated constraint.
72
+ #
73
+ # @return [Stannum::Constraints:Base, nil] the generated or given
74
+ # constraint.
75
+ def type_constraint(
76
+ value,
77
+ allow_nil: false,
78
+ as: 'type',
79
+ **options,
80
+ &block
81
+ )
82
+ return nil if allow_nil && value.nil?
83
+
84
+ if value.is_a?(Stannum::Constraints::Base)
85
+ return value.with_options(**options)
86
+ end
87
+
88
+ if value.is_a?(Module)
89
+ return build_type_constraint(value, **options, &block)
90
+ end
91
+
92
+ raise ArgumentError,
93
+ "#{as} must be a Class or Module or a constraint",
94
+ caller(1..-1)
95
+ end
96
+
97
+ private
98
+
99
+ def build_presence_constraint(present, **options)
100
+ return yield(present, **options) if block_given?
101
+
102
+ return Stannum::Constraints::Presence.new(**options) if present
103
+
104
+ Stannum::Constraints::Absence.new(**options)
105
+ end
106
+
107
+ def build_type_constraint(value, **options)
108
+ return yield(value, **options) if block_given?
109
+
110
+ Stannum::Constraints::Type.new(value, **options)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/support'
4
+
5
+ module Stannum::Support
6
+ # Methods for handling optional/required options.
7
+ #
8
+ # @api private
9
+ module Optional
10
+ class << self
11
+ # @private
12
+ def resolve(
13
+ optional: nil,
14
+ required: nil,
15
+ required_by_default: true,
16
+ **options
17
+ )
18
+ default =
19
+ validate_option(required_by_default, as: :required_by_default)
20
+
21
+ options.merge(
22
+ required: required?(
23
+ default: default,
24
+ optional: validate_option(optional, as: :optional),
25
+ required: validate_option(required, as: :required)
26
+ )
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def required?(default:, optional:, required:)
33
+ return default if optional.nil? && required.nil?
34
+
35
+ return !optional if required.nil?
36
+
37
+ return required if optional.nil?
38
+
39
+ return required unless required == optional
40
+
41
+ raise ArgumentError, 'required and optional must match', caller(1..-1)
42
+ end
43
+
44
+ def validate_option(option, as:)
45
+ return option if option.nil? || option == true || option == false
46
+
47
+ raise ArgumentError, "#{as} must be true or false", caller(1..-1)
48
+ end
49
+ end
50
+
51
+ # @return [true, false] false if the property accepts nil values, otherwise
52
+ # true.
53
+ def optional?
54
+ !options[:required]
55
+ end
56
+
57
+ # @return [true, false] true if the property accepts nil values, otherwise
58
+ # false.
59
+ def required?
60
+ options[:required]
61
+ end
62
+
63
+ private
64
+
65
+ def resolve_required_option(**options)
66
+ Stannum::Support::Optional.resolve(**options)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum'
4
+
5
+ module Stannum
6
+ # Namespace for internal code that supports public features.
7
+ module Support; end
8
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stannum
4
+ # @api private
5
+ #
6
+ # The current version of the gem.
7
+ #
8
+ # @see http://semver.org/
9
+ module Version
10
+ # Major version.
11
+ MAJOR = 0
12
+ # Minor version.
13
+ MINOR = 1
14
+ # Patch version.
15
+ PATCH = 0
16
+ # Prerelease version.
17
+ PRERELEASE = nil
18
+ # Build metadata.
19
+ BUILD = nil
20
+
21
+ class << self
22
+ # Generates the gem version string from the Version constants.
23
+ #
24
+ # Inlined here because dependencies may not be loaded when processing a
25
+ # gemspec, which results in the user being unable to install the gem for
26
+ # the first time.
27
+ #
28
+ # @see SleepingKingStudios::Tools::SemanticVersion#to_gem_version
29
+ def to_gem_version
30
+ str = +"#{MAJOR}.#{MINOR}.#{PATCH}"
31
+
32
+ prerelease = value_of(:PRERELEASE)
33
+ str << ".#{prerelease}" if prerelease
34
+
35
+ build = value_of(:BUILD)
36
+ str << ".#{build}" if build
37
+
38
+ str
39
+ end
40
+
41
+ private
42
+
43
+ def value_of(constant)
44
+ return nil unless const_defined?(constant)
45
+
46
+ value = const_get(constant)
47
+
48
+ return nil if value.respond_to?(:empty?) && value.empty?
49
+
50
+ value
51
+ end
52
+ end
53
+ end
54
+
55
+ # The current version of the gem in Rubygems format.
56
+ VERSION = Version.to_gem_version
57
+ end