stannum 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +25 -1
- data/README.md +129 -1263
- data/config/locales/en.rb +4 -0
- data/lib/stannum/association.rb +293 -0
- data/lib/stannum/associations/many.rb +250 -0
- data/lib/stannum/associations/one.rb +106 -0
- data/lib/stannum/associations.rb +11 -0
- data/lib/stannum/attribute.rb +86 -8
- data/lib/stannum/constraints/base.rb +3 -5
- data/lib/stannum/constraints/enum.rb +1 -1
- data/lib/stannum/constraints/equality.rb +1 -1
- data/lib/stannum/constraints/format.rb +72 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +7 -12
- data/lib/stannum/constraints/identity.rb +1 -1
- data/lib/stannum/constraints/properties/base.rb +1 -1
- data/lib/stannum/constraints/properties/do_not_match_property.rb +11 -11
- data/lib/stannum/constraints/properties/match_property.rb +11 -11
- data/lib/stannum/constraints/properties/matching.rb +7 -7
- data/lib/stannum/constraints/signature.rb +2 -2
- data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
- data/lib/stannum/constraints/type.rb +3 -3
- data/lib/stannum/constraints/types/array_type.rb +2 -2
- data/lib/stannum/constraints/types/hash_type.rb +4 -4
- data/lib/stannum/constraints/union.rb +1 -1
- data/lib/stannum/constraints/uuid.rb +30 -0
- data/lib/stannum/constraints.rb +2 -0
- data/lib/stannum/contract.rb +7 -7
- data/lib/stannum/contracts/array_contract.rb +2 -7
- data/lib/stannum/contracts/base.rb +15 -15
- data/lib/stannum/contracts/builder.rb +2 -2
- data/lib/stannum/contracts/hash_contract.rb +3 -9
- data/lib/stannum/contracts/indifferent_hash_contract.rb +2 -2
- data/lib/stannum/contracts/map_contract.rb +6 -10
- data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
- data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
- data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
- data/lib/stannum/contracts/parameters_contract.rb +4 -4
- data/lib/stannum/contracts/tuple_contract.rb +5 -5
- data/lib/stannum/entities/associations.rb +451 -0
- data/lib/stannum/entities/attributes.rb +116 -18
- data/lib/stannum/entities/constraints.rb +3 -2
- data/lib/stannum/entities/primary_key.rb +148 -0
- data/lib/stannum/entities/properties.rb +30 -8
- data/lib/stannum/entities.rb +5 -2
- data/lib/stannum/entity.rb +4 -0
- data/lib/stannum/errors.rb +9 -13
- data/lib/stannum/messages/default_strategy.rb +2 -2
- data/lib/stannum/parameter_validation.rb +10 -10
- data/lib/stannum/rspec/match_errors_matcher.rb +1 -1
- data/lib/stannum/rspec/validate_parameter.rb +2 -2
- data/lib/stannum/rspec/validate_parameter_matcher.rb +15 -13
- data/lib/stannum/schema.rb +62 -62
- data/lib/stannum/support/optional.rb +1 -1
- data/lib/stannum/version.rb +4 -4
- data/lib/stannum.rb +3 -0
- metadata +14 -79
@@ -0,0 +1,451 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/entities'
|
4
|
+
require 'stannum/schema'
|
5
|
+
|
6
|
+
module Stannum::Entities
|
7
|
+
# Methods for defining and accessing entity associations.
|
8
|
+
module Associations # rubocop:disable Metrics/ModuleLength
|
9
|
+
# Class methods to extend the class when including Associations.
|
10
|
+
module ClassMethods # rubocop:disable Metrics/ModuleLength
|
11
|
+
START_WITH_CAPITAL_LETTER = /\A[A-Z]/
|
12
|
+
private_constant :START_WITH_CAPITAL_LETTER
|
13
|
+
|
14
|
+
# Defines an association on the entity.
|
15
|
+
#
|
16
|
+
# When an association is defined, each of the following steps is executed:
|
17
|
+
#
|
18
|
+
# - Adds the association to ::Associations and the .associations class
|
19
|
+
# method.
|
20
|
+
# - Adds the association to #association and the associated methods, such
|
21
|
+
# as #assign_associations, #[] and #[]=.
|
22
|
+
# - Defines reader and writer methods.
|
23
|
+
#
|
24
|
+
# @overload association(arity, assoc_name, **options)
|
25
|
+
# Defines an association with the given name. The class of the
|
26
|
+
# associated object is determined automatically based on the association
|
27
|
+
# name, or can be specified with the :class_name keyword.
|
28
|
+
#
|
29
|
+
# @param arity [:one, :many] :one if the association has one item, or
|
30
|
+
# :many if the association can have multiple items.
|
31
|
+
# @param assoc_name [String, Symbol] the name of the association.
|
32
|
+
# @param options [Hash] additional options for the association.
|
33
|
+
#
|
34
|
+
# @option options [String] :class_name the name of the associated class.
|
35
|
+
# @option options [true, Hash] :foreign_key the foreign key options for
|
36
|
+
# the association. Can be true, or a Hash containing :name and/or
|
37
|
+
# :type keys.
|
38
|
+
#
|
39
|
+
# @return [Symbol] the association name as a symbol.
|
40
|
+
#
|
41
|
+
# @overload association(arity, assoc_type, **options)
|
42
|
+
# Defines an association with the given class. The name of the
|
43
|
+
# association is determined automatically based on the association
|
44
|
+
# class.
|
45
|
+
#
|
46
|
+
# @param arity [:one, :many] :one if the association has one item, or
|
47
|
+
# :many if the association can have multiple items.
|
48
|
+
# @param assoc_type [String, Symbol, Class] the type of the associated
|
49
|
+
# @param options [Hash] additional options for the association.
|
50
|
+
#
|
51
|
+
# @option options [true, Hash] :foreign_key the foreign key options for
|
52
|
+
# the association. Can be true, or a Hash containing :name and/or
|
53
|
+
# :type keys.
|
54
|
+
#
|
55
|
+
# @return [Symbol] the association name as a symbol.
|
56
|
+
def association(arity, class_or_name, **options) # rubocop:disable Metrics/MethodLength
|
57
|
+
assoc_class =
|
58
|
+
resolve_association_class(arity)
|
59
|
+
assoc_name, assoc_type, options =
|
60
|
+
resolve_parameters(arity, class_or_name, options)
|
61
|
+
|
62
|
+
association = associations.define(
|
63
|
+
definition_class: assoc_class,
|
64
|
+
name: assoc_name,
|
65
|
+
type: assoc_type,
|
66
|
+
options: parse_options(assoc_name, **options)
|
67
|
+
)
|
68
|
+
define_foreign_key(association) if association.foreign_key?
|
69
|
+
|
70
|
+
association.name.intern
|
71
|
+
end
|
72
|
+
alias define_association association
|
73
|
+
|
74
|
+
# @return [Stannum::Schema] The associations Schema object for the Entity.
|
75
|
+
def associations
|
76
|
+
self::Associations
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Class] the default type for foreign key attributes.
|
80
|
+
def default_foreign_key_type
|
81
|
+
(defined?(primary_key_type) && primary_key_type) || Integer
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def association_name_for(arity, class_or_name, configured) # rubocop:disable Metrics/MethodLength
|
87
|
+
if configured
|
88
|
+
raise ArgumentError,
|
89
|
+
%(ambiguous class name "#{class_or_name}" or "#{configured}" ) \
|
90
|
+
'- do not provide both a class and a :class_name keyword'
|
91
|
+
end
|
92
|
+
|
93
|
+
assoc_name = tools.string_tools.underscore(
|
94
|
+
class_or_name.to_s.split('::').last
|
95
|
+
)
|
96
|
+
assoc_name = tools.string_tools.singularize(assoc_name) if arity == :one
|
97
|
+
assoc_name = tools.string_tools.pluralize(assoc_name) if arity == :many
|
98
|
+
|
99
|
+
assoc_name
|
100
|
+
end
|
101
|
+
|
102
|
+
def class_name?(class_or_name)
|
103
|
+
START_WITH_CAPITAL_LETTER.match?(class_or_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
def define_foreign_key(association)
|
107
|
+
define_attribute(
|
108
|
+
association.foreign_key_name,
|
109
|
+
association.foreign_key_type,
|
110
|
+
association_name: association.name,
|
111
|
+
foreign_key: true,
|
112
|
+
required: false
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def included(other)
|
117
|
+
super
|
118
|
+
|
119
|
+
other.include(Stannum::Entities::Associations)
|
120
|
+
|
121
|
+
Stannum::Entities::Associations.apply(other) if other.is_a?(Class)
|
122
|
+
end
|
123
|
+
|
124
|
+
def inherited(other)
|
125
|
+
super
|
126
|
+
|
127
|
+
Stannum::Entities::Associations.apply(other)
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_foreign_key_options(assoc_name, foreign_key) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
131
|
+
return {} if foreign_key == false
|
132
|
+
|
133
|
+
foreign_key = {} if foreign_key == true
|
134
|
+
|
135
|
+
if foreign_key.is_a?(String) || foreign_key.is_a?(Symbol)
|
136
|
+
foreign_key = { name: foreign_key.to_s }
|
137
|
+
end
|
138
|
+
|
139
|
+
unless foreign_key.is_a?(Hash)
|
140
|
+
raise InvalidOptionError, "invalid foreign key #{foreign_key.inspect}"
|
141
|
+
end
|
142
|
+
|
143
|
+
name = foreign_key.fetch(:name) { "#{assoc_name}_id" }
|
144
|
+
type = foreign_key.fetch(:type) { default_foreign_key_type }
|
145
|
+
|
146
|
+
{
|
147
|
+
foreign_key_name: name,
|
148
|
+
foreign_key_type: type
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_inverse_options(inverse)
|
153
|
+
hsh = {
|
154
|
+
entity_class_name: name,
|
155
|
+
inverse: true
|
156
|
+
}
|
157
|
+
|
158
|
+
if inverse.is_a?(String) || inverse.is_a?(Symbol)
|
159
|
+
hsh[:inverse_name] = inverse.to_s
|
160
|
+
end
|
161
|
+
|
162
|
+
hsh
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_options(assoc_name, **options) # rubocop:disable Metrics/MethodLength
|
166
|
+
if options.key?(:foreign_key)
|
167
|
+
options = options.merge(
|
168
|
+
parse_foreign_key_options(assoc_name, options.delete(:foreign_key))
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
if options[:inverse] != false
|
173
|
+
options = options.merge(
|
174
|
+
parse_inverse_options(options.delete(:inverse))
|
175
|
+
)
|
176
|
+
end
|
177
|
+
|
178
|
+
options
|
179
|
+
end
|
180
|
+
|
181
|
+
def resolve_association_class(arity)
|
182
|
+
return Stannum::Associations::One if arity == :one
|
183
|
+
|
184
|
+
return Stannum::Associations::Many if arity == :many
|
185
|
+
|
186
|
+
raise ArgumentError, 'association arity must be :one or :many'
|
187
|
+
end
|
188
|
+
|
189
|
+
def resolve_association_type(assoc_name)
|
190
|
+
tools.string_tools.chain(assoc_name, :singularize, :camelize)
|
191
|
+
end
|
192
|
+
|
193
|
+
def resolve_parameters(arity, class_or_name, options)
|
194
|
+
class_name = options.delete(:class_name)
|
195
|
+
|
196
|
+
if class_or_name.is_a?(Module) || class_name?(class_or_name)
|
197
|
+
assoc_name = association_name_for(arity, class_or_name, class_name)
|
198
|
+
assoc_type = class_or_name
|
199
|
+
|
200
|
+
return [assoc_name, assoc_type, options]
|
201
|
+
end
|
202
|
+
|
203
|
+
assoc_name = tools.string_tools.underscore(class_or_name.to_s)
|
204
|
+
assoc_type = class_name || resolve_association_type(assoc_name)
|
205
|
+
|
206
|
+
[assoc_name, assoc_type, options]
|
207
|
+
end
|
208
|
+
|
209
|
+
def tools
|
210
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Exception class raised when an invalid option value is set.
|
215
|
+
class InvalidOptionError < StandardError; end
|
216
|
+
|
217
|
+
class << self
|
218
|
+
# Generates Associations schema for the class.
|
219
|
+
#
|
220
|
+
# Creates a new Stannum::Schema and sets it as the class's :Associations
|
221
|
+
# constant. If the superclass is an entity class (and already defines its
|
222
|
+
# own Associations, includes the superclass Associations in the class
|
223
|
+
# Associations). Finally, includes the class Associations in the class.
|
224
|
+
#
|
225
|
+
# @param other [Class] the class to which attributes are added.
|
226
|
+
def apply(other)
|
227
|
+
return unless other.is_a?(Class)
|
228
|
+
|
229
|
+
return if entity_class?(other)
|
230
|
+
|
231
|
+
other.const_set(:Associations, build_schema)
|
232
|
+
|
233
|
+
if entity_class?(other.superclass)
|
234
|
+
other::Associations.include(other.superclass::Associations)
|
235
|
+
end
|
236
|
+
|
237
|
+
other.include(other::Associations)
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
def build_schema
|
243
|
+
Stannum::Schema.new(
|
244
|
+
property_class: Stannum::Association,
|
245
|
+
property_name: 'associations'
|
246
|
+
)
|
247
|
+
end
|
248
|
+
|
249
|
+
def entity_class?(other)
|
250
|
+
other.const_defined?(:Associations, false)
|
251
|
+
end
|
252
|
+
|
253
|
+
def included(other)
|
254
|
+
super
|
255
|
+
|
256
|
+
other.extend(self::ClassMethods)
|
257
|
+
|
258
|
+
apply(other) if other.is_a?(Class)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# @param properties [Hash] the properties used to initialize the entity.
|
263
|
+
def initialize(**properties)
|
264
|
+
@associations = {}
|
265
|
+
@association_proxies = {}
|
266
|
+
|
267
|
+
super
|
268
|
+
end
|
269
|
+
|
270
|
+
# Updates the struct's associations with the given values.
|
271
|
+
#
|
272
|
+
# This method is used to update some (but not all) of the associations of
|
273
|
+
# the struct. For each key in the hash, it calls the corresponding writer
|
274
|
+
# method with the value for that association.
|
275
|
+
#
|
276
|
+
# Any associations that are not in the given hash are unchanged, as are any
|
277
|
+
# properties that are not associations.
|
278
|
+
#
|
279
|
+
# If the associations hash includes any keys that do not correspond to an
|
280
|
+
# association, the struct will raise an error.
|
281
|
+
#
|
282
|
+
# @param associations [Hash] The associations for the struct.
|
283
|
+
#
|
284
|
+
# @raise ArgumentError if any key is not a valid association.
|
285
|
+
#
|
286
|
+
# @see #associations=
|
287
|
+
def assign_associations(associations)
|
288
|
+
unless associations.is_a?(Hash)
|
289
|
+
raise ArgumentError, 'associations must be a Hash'
|
290
|
+
end
|
291
|
+
|
292
|
+
set_associations(associations, force: false)
|
293
|
+
end
|
294
|
+
|
295
|
+
# @private
|
296
|
+
def association_proxy_for(association)
|
297
|
+
@association_proxies[association.name] ||=
|
298
|
+
Stannum::Associations::Many::Proxy.new(association:, entity: self)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Collects the entity associations.
|
302
|
+
#
|
303
|
+
# @return [Hash<String, Object>] the entity associations.
|
304
|
+
def associations
|
305
|
+
@associations.dup
|
306
|
+
end
|
307
|
+
|
308
|
+
# Replaces the entity's associations with the given values.
|
309
|
+
#
|
310
|
+
# This method is used to update all of the associations of the entity. For
|
311
|
+
# each association, the writer method is called with the value from the
|
312
|
+
# hash. Non-association properties are unchanged.
|
313
|
+
#
|
314
|
+
# If the associations hash includes any keys that do not correspond to a
|
315
|
+
# valid association, the entity will raise an error.
|
316
|
+
#
|
317
|
+
# @param associations [Hash] the associations to assign to the entity.
|
318
|
+
#
|
319
|
+
# @raise ArgumentError if any key is not a valid association.
|
320
|
+
#
|
321
|
+
# @see #assign_attributes
|
322
|
+
def associations=(associations)
|
323
|
+
unless associations.is_a?(Hash)
|
324
|
+
raise ArgumentError, 'associations must be a Hash'
|
325
|
+
end
|
326
|
+
|
327
|
+
set_associations(associations, force: true)
|
328
|
+
end
|
329
|
+
|
330
|
+
# (see Stannum::Entities::Properties#properties)
|
331
|
+
def properties
|
332
|
+
super.merge(associations)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Retrieves the association value for the requested key.
|
336
|
+
#
|
337
|
+
# If the :safe flag is set, will verify that the association name is valid
|
338
|
+
# (a non-empty String or Symbol) and that there is a defined association by
|
339
|
+
# that name. By default, :safe is set to true.
|
340
|
+
#
|
341
|
+
# @param key [String, Symbol] the key of the association to retrieve.
|
342
|
+
# @param safe [Boolean] if true, validates the association key.
|
343
|
+
#
|
344
|
+
# @return [Object] the value of the requested association.
|
345
|
+
#
|
346
|
+
# @api private
|
347
|
+
def read_association(key, safe: true)
|
348
|
+
if safe
|
349
|
+
tools.assertions.validate_name(key, as: 'association')
|
350
|
+
|
351
|
+
unless self.class.associations.key?(key.to_s)
|
352
|
+
raise ArgumentError, "unknown association #{key.inspect}"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
@associations[key.to_s]
|
357
|
+
end
|
358
|
+
|
359
|
+
# Assigns the association value for the requested key.
|
360
|
+
#
|
361
|
+
# If the :safe flag is set, will verify that the association name is valid
|
362
|
+
# (a non-empty String or Symbol) and that there is a defined association by
|
363
|
+
# that name. By default, :safe is set to true.
|
364
|
+
#
|
365
|
+
# @param key [String, Symbol] the key of the association to assign.
|
366
|
+
# @param value [Object] the value to assign.
|
367
|
+
# @param safe [Boolean] if true, validates the association key.
|
368
|
+
#
|
369
|
+
# @return [Object] the assigned value.
|
370
|
+
#
|
371
|
+
# @api private
|
372
|
+
def write_association(key, value, safe: true)
|
373
|
+
if safe
|
374
|
+
tools.assertions.validate_name(key, as: 'association')
|
375
|
+
|
376
|
+
unless self.class.associations.key?(key.to_s)
|
377
|
+
raise ArgumentError, "unknown association #{key.inspect}"
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
@associations[key.to_s] = value
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
def get_property(key)
|
387
|
+
return @associations[key.to_s] if associations.key?(key.to_s)
|
388
|
+
|
389
|
+
super
|
390
|
+
end
|
391
|
+
|
392
|
+
def inspect_association(value, **options) # rubocop:disable Metrics/MethodLength
|
393
|
+
if value.nil?
|
394
|
+
'nil'
|
395
|
+
elsif value.is_a?(Array)
|
396
|
+
value
|
397
|
+
.map { |item| inspect_association(item, **options) }
|
398
|
+
.join(', ')
|
399
|
+
.then { |str| "[#{str}]" }
|
400
|
+
elsif value.respond_to?(:inspect_with_options)
|
401
|
+
value.inspect_with_options(**options)
|
402
|
+
else
|
403
|
+
value.inspect
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def inspect_properties(**options)
|
408
|
+
return super unless options.fetch(:associations, true)
|
409
|
+
|
410
|
+
@associations.reduce(super) do |memo, (key, value)|
|
411
|
+
mapped = inspect_association(value, **options, associations: false)
|
412
|
+
|
413
|
+
"#{memo} #{key}: #{mapped}"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def set_associations(associations, force:)
|
418
|
+
associations, non_matching =
|
419
|
+
bisect_properties(associations, self.class.associations)
|
420
|
+
|
421
|
+
unless non_matching.empty?
|
422
|
+
handle_invalid_properties(non_matching, as: 'association')
|
423
|
+
end
|
424
|
+
|
425
|
+
write_associations(associations, force:)
|
426
|
+
end
|
427
|
+
|
428
|
+
def set_properties(properties, force:)
|
429
|
+
associations, non_matching =
|
430
|
+
bisect_properties(properties, self.class.associations)
|
431
|
+
|
432
|
+
super(non_matching, force:)
|
433
|
+
|
434
|
+
write_associations(associations, force:)
|
435
|
+
end
|
436
|
+
|
437
|
+
def set_property(key, value)
|
438
|
+
return super unless associations.key?(key.to_s)
|
439
|
+
|
440
|
+
send(self.class.associations[key.to_s].writer_name, value)
|
441
|
+
end
|
442
|
+
|
443
|
+
def write_associations(associations, force:)
|
444
|
+
self.class.associations.each do |assoc_name, association|
|
445
|
+
next unless associations.key?(assoc_name) || force
|
446
|
+
|
447
|
+
send(association.writer_name, associations[assoc_name])
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
@@ -1,11 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'stannum/entities'
|
4
|
-
require 'stannum/schema'
|
5
4
|
|
6
5
|
module Stannum::Entities
|
7
6
|
# Methods for defining and accessing entity attributes.
|
8
|
-
module Attributes
|
7
|
+
module Attributes # rubocop:disable Metrics/ModuleLength
|
9
8
|
# Class methods to extend the class when including Attributes.
|
10
9
|
module ClassMethods
|
11
10
|
# Defines an attribute on the entity.
|
@@ -25,17 +24,20 @@ module Stannum::Entities
|
|
25
24
|
#
|
26
25
|
# @option options [Object] :default The default value for the attribute.
|
27
26
|
# Defaults to nil.
|
27
|
+
# @option options [Boolean] :primary_key true if the attribute represents
|
28
|
+
# the primary key for the entity; otherwise false. Defaults to false.
|
28
29
|
#
|
29
|
-
# @return [Symbol]
|
30
|
+
# @return [Symbol] the attribute name as a symbol.
|
30
31
|
def attribute(attr_name, attr_type, **options)
|
31
|
-
attributes.
|
32
|
+
attributes.define(
|
32
33
|
name: attr_name,
|
33
34
|
type: attr_type,
|
34
|
-
options:
|
35
|
+
options:
|
35
36
|
)
|
36
37
|
|
37
38
|
attr_name.intern
|
38
39
|
end
|
40
|
+
alias define_attribute attribute
|
39
41
|
|
40
42
|
# @return [Stannum::Schema] The attributes Schema object for the Entity.
|
41
43
|
def attributes
|
@@ -73,7 +75,7 @@ module Stannum::Entities
|
|
73
75
|
|
74
76
|
return if entity_class?(other)
|
75
77
|
|
76
|
-
other.const_set(:Attributes,
|
78
|
+
other.const_set(:Attributes, build_schema)
|
77
79
|
|
78
80
|
if entity_class?(other.superclass)
|
79
81
|
other::Attributes.include(other.superclass::Attributes)
|
@@ -84,6 +86,13 @@ module Stannum::Entities
|
|
84
86
|
|
85
87
|
private
|
86
88
|
|
89
|
+
def build_schema
|
90
|
+
Stannum::Schema.new(
|
91
|
+
property_class: Stannum::Attribute,
|
92
|
+
property_name: 'attributes'
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
87
96
|
def entity_class?(other)
|
88
97
|
other.const_defined?(:Attributes, false)
|
89
98
|
end
|
@@ -101,6 +110,8 @@ module Stannum::Entities
|
|
101
110
|
def initialize(**properties)
|
102
111
|
@attributes = {}
|
103
112
|
|
113
|
+
self.class.attributes.each_key { |key| @attributes[key] = nil }
|
114
|
+
|
104
115
|
super
|
105
116
|
end
|
106
117
|
|
@@ -117,9 +128,9 @@ module Stannum::Entities
|
|
117
128
|
# If the attributes hash includes any keys that do not correspond to an
|
118
129
|
# attribute, the struct will raise an error.
|
119
130
|
#
|
120
|
-
# @param attributes [Hash] The
|
131
|
+
# @param attributes [Hash] The attributes for the struct.
|
121
132
|
#
|
122
|
-
# @raise ArgumentError if
|
133
|
+
# @raise ArgumentError if any key is not a valid attribute.
|
123
134
|
#
|
124
135
|
# @see #attributes=
|
125
136
|
def assign_attributes(attributes)
|
@@ -132,7 +143,7 @@ module Stannum::Entities
|
|
132
143
|
|
133
144
|
# Collects the entity attributes.
|
134
145
|
#
|
135
|
-
# @
|
146
|
+
# @return [Hash<String, Object>] the entity attributes.
|
136
147
|
def attributes
|
137
148
|
@attributes.dup
|
138
149
|
end
|
@@ -166,16 +177,100 @@ module Stannum::Entities
|
|
166
177
|
super.merge(attributes)
|
167
178
|
end
|
168
179
|
|
180
|
+
# Retrieves the attribute value for the requested key.
|
181
|
+
#
|
182
|
+
# If the :safe flag is set, will verify that the attribute name is valid (a
|
183
|
+
# non-empty String or Symbol) and that there is a defined attribute by that
|
184
|
+
# name. By default, :safe is set to true.
|
185
|
+
#
|
186
|
+
# @param key [String, Symbol] the key of the attribute to retrieve.
|
187
|
+
# @param safe [Boolean] if true, validates the attribute key.
|
188
|
+
#
|
189
|
+
# @return [Object] the value of the requested attribute.
|
190
|
+
#
|
191
|
+
# @api private
|
192
|
+
def read_attribute(key, safe: true)
|
193
|
+
if safe
|
194
|
+
tools.assertions.validate_name(key, as: 'attribute')
|
195
|
+
|
196
|
+
unless self.class.attributes.key?(key.to_s)
|
197
|
+
raise ArgumentError, "unknown attribute #{key.inspect}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
@attributes[key.to_s]
|
202
|
+
end
|
203
|
+
|
204
|
+
# Assigns the attribute value for the requested key.
|
205
|
+
#
|
206
|
+
# If the :safe flag is set, will verify that the attribute name is valid (a
|
207
|
+
# non-empty String or Symbol) and that there is a defined attribute by that
|
208
|
+
# name. By default, :safe is set to true.
|
209
|
+
#
|
210
|
+
# @param key [String, Symbol] the key of the attribute to assign.
|
211
|
+
# @param value [Object] the value to assign.
|
212
|
+
# @param safe [Boolean] if true, validates the attribute key.
|
213
|
+
#
|
214
|
+
# @return [Object] the assigned value.
|
215
|
+
#
|
216
|
+
# @api private
|
217
|
+
def write_attribute(key, value, safe: true)
|
218
|
+
if safe
|
219
|
+
tools.assertions.validate_name(key, as: 'attribute')
|
220
|
+
|
221
|
+
unless self.class.attributes.key?(key.to_s)
|
222
|
+
raise ArgumentError, "unknown attribute #{key.inspect}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
@attributes[key.to_s] = value
|
227
|
+
end
|
228
|
+
|
169
229
|
private
|
170
230
|
|
231
|
+
def apply_defaults_for(attributes) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
232
|
+
with_value, with_proc = bisect_attributes_by_default_type
|
233
|
+
|
234
|
+
with_value.each do |attribute|
|
235
|
+
next unless @attributes[attribute.name].nil?
|
236
|
+
next unless attributes.key?(attribute.name)
|
237
|
+
|
238
|
+
send(attribute.writer_name, attribute.default)
|
239
|
+
end
|
240
|
+
|
241
|
+
with_proc.each do |attribute|
|
242
|
+
next unless @attributes[attribute.name].nil?
|
243
|
+
next unless attributes.key?(attribute.name)
|
244
|
+
|
245
|
+
send(attribute.writer_name, attribute.default_value_for(self))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def bisect_attributes_by_default_type
|
250
|
+
with_value = []
|
251
|
+
with_proc = []
|
252
|
+
|
253
|
+
self.class.attributes.each_value do |attribute|
|
254
|
+
next unless attribute.default?
|
255
|
+
|
256
|
+
(attribute.default.is_a?(Proc) ? with_proc : with_value) << attribute
|
257
|
+
end
|
258
|
+
|
259
|
+
[with_value, with_proc]
|
260
|
+
end
|
261
|
+
|
171
262
|
def get_property(key)
|
172
263
|
return @attributes[key.to_s] if attributes.key?(key.to_s)
|
173
264
|
|
174
265
|
super
|
175
266
|
end
|
176
267
|
|
177
|
-
def
|
178
|
-
super
|
268
|
+
def inspect_properties(**options)
|
269
|
+
return super unless options.fetch(:attributes, true)
|
270
|
+
|
271
|
+
@attributes.reduce(super) do |memo, (key, value)|
|
272
|
+
"#{memo} #{key}: #{value.inspect}"
|
273
|
+
end
|
179
274
|
end
|
180
275
|
|
181
276
|
def set_attributes(attributes, force:)
|
@@ -186,16 +281,16 @@ module Stannum::Entities
|
|
186
281
|
handle_invalid_properties(non_matching, as: 'attribute')
|
187
282
|
end
|
188
283
|
|
189
|
-
write_attributes(attributes, force:
|
284
|
+
write_attributes(attributes, force:)
|
190
285
|
end
|
191
286
|
|
192
287
|
def set_properties(properties, force:)
|
193
288
|
attributes, non_matching =
|
194
289
|
bisect_properties(properties, self.class.attributes)
|
195
290
|
|
196
|
-
super(non_matching, force:
|
291
|
+
super(non_matching, force:)
|
197
292
|
|
198
|
-
write_attributes(attributes, force:
|
293
|
+
write_attributes(attributes, force:)
|
199
294
|
end
|
200
295
|
|
201
296
|
def set_property(key, value)
|
@@ -208,11 +303,14 @@ module Stannum::Entities
|
|
208
303
|
self.class.attributes.each do |attr_name, attribute|
|
209
304
|
next unless attributes.key?(attr_name) || force
|
210
305
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
306
|
+
if attributes[attr_name].nil? && attribute.default?
|
307
|
+
@attributes[attr_name] = nil
|
308
|
+
else
|
309
|
+
send(attribute.writer_name, attributes[attr_name])
|
310
|
+
end
|
215
311
|
end
|
312
|
+
|
313
|
+
apply_defaults_for(force ? @attributes : attributes)
|
216
314
|
end
|
217
315
|
end
|
218
316
|
end
|
@@ -29,6 +29,7 @@ module Stannum::Entities
|
|
29
29
|
|
30
30
|
returned
|
31
31
|
end
|
32
|
+
alias define_attribute attribute
|
32
33
|
end
|
33
34
|
|
34
35
|
# Class methods to extend the class when including Constraints.
|
@@ -76,11 +77,11 @@ module Stannum::Entities
|
|
76
77
|
# @param attr_name [String, Symbol] The name of the attribute or
|
77
78
|
# property to constrain.
|
78
79
|
# @param constraint [Stannum::Constraints::Base] The constraint to add.
|
79
|
-
def constraint(attr_name = nil, constraint = nil, &
|
80
|
+
def constraint(attr_name = nil, constraint = nil, &)
|
80
81
|
attr_name, constraint = resolve_constraint(attr_name, constraint)
|
81
82
|
|
82
83
|
if block_given?
|
83
|
-
constraint = Stannum::Constraint.new(&
|
84
|
+
constraint = Stannum::Constraint.new(&)
|
84
85
|
else
|
85
86
|
validate_constraint(constraint)
|
86
87
|
end
|