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