stannum 0.2.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 +49 -0
- data/README.md +130 -1200
- 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 +13 -13
- data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
- data/lib/stannum/constraints/hashes.rb +6 -2
- data/lib/stannum/constraints/identity.rb +1 -1
- data/lib/stannum/constraints/properties/base.rb +124 -0
- data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
- data/lib/stannum/constraints/properties/match_property.rb +117 -0
- data/lib/stannum/constraints/properties/matching.rb +112 -0
- data/lib/stannum/constraints/properties.rb +17 -0
- data/lib/stannum/constraints/signature.rb +2 -2
- data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
- data/lib/stannum/constraints/type.rb +4 -4
- 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 +3 -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 +15 -4
- data/lib/stannum/contracts/hash_contract.rb +3 -9
- data/lib/stannum/contracts/indifferent_hash_contract.rb +15 -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 +6 -6
- data/lib/stannum/entities/associations.rb +451 -0
- data/lib/stannum/entities/attributes.rb +316 -0
- data/lib/stannum/entities/constraints.rb +178 -0
- data/lib/stannum/entities/primary_key.rb +148 -0
- data/lib/stannum/entities/properties.rb +208 -0
- data/lib/stannum/entities.rb +16 -0
- data/lib/stannum/entity.rb +87 -0
- data/lib/stannum/errors.rb +12 -16
- 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 +7 -7
- data/lib/stannum/rspec/validate_parameter.rb +2 -2
- data/lib/stannum/rspec/validate_parameter_matcher.rb +22 -20
- data/lib/stannum/schema.rb +117 -76
- data/lib/stannum/struct.rb +12 -346
- data/lib/stannum/support/optional.rb +1 -1
- data/lib/stannum/version.rb +4 -4
- data/lib/stannum.rb +6 -0
- metadata +26 -85
@@ -52,9 +52,9 @@ module Stannum::Contracts
|
|
52
52
|
&block
|
53
53
|
)
|
54
54
|
super(
|
55
|
-
allow_extra_keys
|
55
|
+
allow_extra_keys:,
|
56
56
|
key_type: Stannum::Constraints::Hashes::IndifferentKey.new,
|
57
|
-
value_type
|
57
|
+
value_type:,
|
58
58
|
**options,
|
59
59
|
&block
|
60
60
|
)
|
@@ -74,5 +74,18 @@ module Stannum::Contracts
|
|
74
74
|
actual.fetch(property) { actual[property.to_s] }
|
75
75
|
end
|
76
76
|
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def add_extra_keys_constraint
|
81
|
+
return if options[:allow_extra_keys]
|
82
|
+
|
83
|
+
keys = -> { expected_keys }
|
84
|
+
|
85
|
+
add_constraint(
|
86
|
+
Stannum::Constraints::Hashes::IndifferentExtraKeys.new(keys),
|
87
|
+
concatenatable: false
|
88
|
+
)
|
89
|
+
end
|
77
90
|
end
|
78
91
|
end
|
@@ -87,13 +87,13 @@ module Stannum::Contracts
|
|
87
87
|
# @overload key(**options) { |value| }
|
88
88
|
# Creates a new Stannum::Constraint object with the given block, and
|
89
89
|
# adds that constraint to the contract for the value at the given key.
|
90
|
-
def key(property, constraint = nil, **options, &
|
90
|
+
def key(property, constraint = nil, **options, &)
|
91
91
|
self.constraint(
|
92
92
|
constraint,
|
93
|
-
property
|
93
|
+
property:,
|
94
94
|
property_type: :key,
|
95
95
|
**options,
|
96
|
-
&
|
96
|
+
&
|
97
97
|
)
|
98
98
|
end
|
99
99
|
end
|
@@ -107,11 +107,7 @@ module Stannum::Contracts
|
|
107
107
|
**options,
|
108
108
|
&block
|
109
109
|
)
|
110
|
-
super
|
111
|
-
allow_extra_keys: allow_extra_keys,
|
112
|
-
**options,
|
113
|
-
&block
|
114
|
-
)
|
110
|
+
super
|
115
111
|
end
|
116
112
|
|
117
113
|
# Adds a key constraint to the contract.
|
@@ -136,7 +132,7 @@ module Stannum::Contracts
|
|
136
132
|
constraint,
|
137
133
|
property: key,
|
138
134
|
property_type: :key,
|
139
|
-
sanity
|
135
|
+
sanity:,
|
140
136
|
**options
|
141
137
|
)
|
142
138
|
end
|
@@ -188,7 +184,7 @@ module Stannum::Contracts
|
|
188
184
|
add_constraint Stannum::Constraints::Signatures::Map.new, sanity: true
|
189
185
|
end
|
190
186
|
|
191
|
-
def define_constraints(&
|
187
|
+
def define_constraints(&)
|
192
188
|
add_type_constraint
|
193
189
|
|
194
190
|
add_extra_keys_constraint
|
@@ -103,7 +103,7 @@ module Stannum::Contracts::Parameters
|
|
103
103
|
type = coerce_item_type(item_type)
|
104
104
|
constraint = Stannum::Constraints::Types::ArrayType.new(item_type: type)
|
105
105
|
|
106
|
-
set_variadic_constraint(constraint, as:
|
106
|
+
set_variadic_constraint(constraint, as:)
|
107
107
|
end
|
108
108
|
|
109
109
|
protected
|
@@ -320,8 +320,8 @@ module Stannum::Contracts
|
|
320
320
|
# constraint, otherwise false.
|
321
321
|
#
|
322
322
|
# @return [Stannum::Contracts::ParametersContract::Builder] the builder.
|
323
|
-
def keyword(name, type = nil, **options, &
|
324
|
-
type = resolve_constraint_or_type(type, **options, &
|
323
|
+
def keyword(name, type = nil, **options, &)
|
324
|
+
type = resolve_constraint_or_type(type, **options, &)
|
325
325
|
|
326
326
|
contract.add_keyword_constraint(
|
327
327
|
name,
|
@@ -515,8 +515,8 @@ module Stannum::Contracts
|
|
515
515
|
end
|
516
516
|
end
|
517
517
|
|
518
|
-
def define_constraints(&
|
519
|
-
super
|
518
|
+
def define_constraints(&)
|
519
|
+
super
|
520
520
|
|
521
521
|
add_key_constraint :arguments, arguments_contract
|
522
522
|
add_key_constraint :keywords, keywords_contract
|
@@ -99,7 +99,7 @@ module Stannum::Contracts
|
|
99
99
|
#
|
100
100
|
# @param options [Hash<Symbol, Object>] Options for the constraint.
|
101
101
|
# @yieldparam value [Object] The value of the property when called.
|
102
|
-
def item(constraint = nil, **options, &
|
102
|
+
def item(constraint = nil, **options, &)
|
103
103
|
index = (@current_index += 1)
|
104
104
|
|
105
105
|
self.constraint(
|
@@ -107,7 +107,7 @@ module Stannum::Contracts
|
|
107
107
|
property: index,
|
108
108
|
property_type: :index,
|
109
109
|
**options,
|
110
|
-
&
|
110
|
+
&
|
111
111
|
)
|
112
112
|
end
|
113
113
|
end
|
@@ -117,7 +117,7 @@ module Stannum::Contracts
|
|
117
117
|
# @param options [Hash<Symbol, Object>] Configuration options for the
|
118
118
|
# contract. Defaults to an empty Hash.
|
119
119
|
def initialize(allow_extra_items: false, **options, &block)
|
120
|
-
super
|
120
|
+
super
|
121
121
|
end
|
122
122
|
|
123
123
|
# Adds an index constraint to the contract.
|
@@ -142,7 +142,7 @@ module Stannum::Contracts
|
|
142
142
|
constraint,
|
143
143
|
property: index,
|
144
144
|
property_type: :index,
|
145
|
-
sanity
|
145
|
+
sanity:,
|
146
146
|
**options
|
147
147
|
)
|
148
148
|
end
|
@@ -168,7 +168,7 @@ module Stannum::Contracts
|
|
168
168
|
|
169
169
|
index = 1 + definition.options.fetch(:property, -1)
|
170
170
|
|
171
|
-
index
|
171
|
+
[index, count].max
|
172
172
|
end
|
173
173
|
end
|
174
174
|
|
@@ -202,7 +202,7 @@ module Stannum::Contracts
|
|
202
202
|
add_constraint Stannum::Constraints::Signatures::Tuple.new, sanity: true
|
203
203
|
end
|
204
204
|
|
205
|
-
def define_constraints(&
|
205
|
+
def define_constraints(&)
|
206
206
|
add_type_constraint
|
207
207
|
|
208
208
|
add_extra_items_constraint
|
@@ -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
|