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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -1
  3. data/README.md +129 -1263
  4. data/config/locales/en.rb +4 -0
  5. data/lib/stannum/association.rb +293 -0
  6. data/lib/stannum/associations/many.rb +250 -0
  7. data/lib/stannum/associations/one.rb +106 -0
  8. data/lib/stannum/associations.rb +11 -0
  9. data/lib/stannum/attribute.rb +86 -8
  10. data/lib/stannum/constraints/base.rb +3 -5
  11. data/lib/stannum/constraints/enum.rb +1 -1
  12. data/lib/stannum/constraints/equality.rb +1 -1
  13. data/lib/stannum/constraints/format.rb +72 -0
  14. data/lib/stannum/constraints/hashes/extra_keys.rb +7 -12
  15. data/lib/stannum/constraints/identity.rb +1 -1
  16. data/lib/stannum/constraints/properties/base.rb +1 -1
  17. data/lib/stannum/constraints/properties/do_not_match_property.rb +11 -11
  18. data/lib/stannum/constraints/properties/match_property.rb +11 -11
  19. data/lib/stannum/constraints/properties/matching.rb +7 -7
  20. data/lib/stannum/constraints/signature.rb +2 -2
  21. data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
  22. data/lib/stannum/constraints/type.rb +3 -3
  23. data/lib/stannum/constraints/types/array_type.rb +2 -2
  24. data/lib/stannum/constraints/types/hash_type.rb +4 -4
  25. data/lib/stannum/constraints/union.rb +1 -1
  26. data/lib/stannum/constraints/uuid.rb +30 -0
  27. data/lib/stannum/constraints.rb +2 -0
  28. data/lib/stannum/contract.rb +7 -7
  29. data/lib/stannum/contracts/array_contract.rb +2 -7
  30. data/lib/stannum/contracts/base.rb +15 -15
  31. data/lib/stannum/contracts/builder.rb +2 -2
  32. data/lib/stannum/contracts/hash_contract.rb +3 -9
  33. data/lib/stannum/contracts/indifferent_hash_contract.rb +2 -2
  34. data/lib/stannum/contracts/map_contract.rb +6 -10
  35. data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
  36. data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
  37. data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
  38. data/lib/stannum/contracts/parameters_contract.rb +4 -4
  39. data/lib/stannum/contracts/tuple_contract.rb +5 -5
  40. data/lib/stannum/entities/associations.rb +451 -0
  41. data/lib/stannum/entities/attributes.rb +116 -18
  42. data/lib/stannum/entities/constraints.rb +3 -2
  43. data/lib/stannum/entities/primary_key.rb +148 -0
  44. data/lib/stannum/entities/properties.rb +30 -8
  45. data/lib/stannum/entities.rb +5 -2
  46. data/lib/stannum/entity.rb +4 -0
  47. data/lib/stannum/errors.rb +9 -13
  48. data/lib/stannum/messages/default_strategy.rb +2 -2
  49. data/lib/stannum/parameter_validation.rb +10 -10
  50. data/lib/stannum/rspec/match_errors_matcher.rb +1 -1
  51. data/lib/stannum/rspec/validate_parameter.rb +2 -2
  52. data/lib/stannum/rspec/validate_parameter_matcher.rb +15 -13
  53. data/lib/stannum/schema.rb +62 -62
  54. data/lib/stannum/support/optional.rb +1 -1
  55. data/lib/stannum/version.rb +4 -4
  56. data/lib/stannum.rb +3 -0
  57. 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] The attribute name as a symbol.
30
+ # @return [Symbol] the attribute name as a symbol.
30
31
  def attribute(attr_name, attr_type, **options)
31
- attributes.define_attribute(
32
+ attributes.define(
32
33
  name: attr_name,
33
34
  type: attr_type,
34
- options: 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, Stannum::Schema.new)
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 initial attributes for the struct.
131
+ # @param attributes [Hash] The attributes for the struct.
121
132
  #
122
- # @raise ArgumentError if the key is not a valid attribute.
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
- # @param attributes [Hash<String, Object>] the entity attributes.
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 inspectable_properties
178
- super().merge(attributes)
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: 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: force)
291
+ super(non_matching, force:)
197
292
 
198
- write_attributes(attributes, force: 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
- send(
212
- attribute.writer_name,
213
- attributes[attr_name].nil? ? attribute.default : attributes[attr_name]
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, &block)
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(&block)
84
+ constraint = Stannum::Constraint.new(&)
84
85
  else
85
86
  validate_constraint(constraint)
86
87
  end