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,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