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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +130 -1200
  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 +13 -13
  15. data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
  16. data/lib/stannum/constraints/hashes.rb +6 -2
  17. data/lib/stannum/constraints/identity.rb +1 -1
  18. data/lib/stannum/constraints/properties/base.rb +124 -0
  19. data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
  20. data/lib/stannum/constraints/properties/match_property.rb +117 -0
  21. data/lib/stannum/constraints/properties/matching.rb +112 -0
  22. data/lib/stannum/constraints/properties.rb +17 -0
  23. data/lib/stannum/constraints/signature.rb +2 -2
  24. data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
  25. data/lib/stannum/constraints/type.rb +4 -4
  26. data/lib/stannum/constraints/types/array_type.rb +2 -2
  27. data/lib/stannum/constraints/types/hash_type.rb +4 -4
  28. data/lib/stannum/constraints/union.rb +1 -1
  29. data/lib/stannum/constraints/uuid.rb +30 -0
  30. data/lib/stannum/constraints.rb +3 -0
  31. data/lib/stannum/contract.rb +7 -7
  32. data/lib/stannum/contracts/array_contract.rb +2 -7
  33. data/lib/stannum/contracts/base.rb +15 -15
  34. data/lib/stannum/contracts/builder.rb +15 -4
  35. data/lib/stannum/contracts/hash_contract.rb +3 -9
  36. data/lib/stannum/contracts/indifferent_hash_contract.rb +15 -2
  37. data/lib/stannum/contracts/map_contract.rb +6 -10
  38. data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
  39. data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
  40. data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
  41. data/lib/stannum/contracts/parameters_contract.rb +4 -4
  42. data/lib/stannum/contracts/tuple_contract.rb +6 -6
  43. data/lib/stannum/entities/associations.rb +451 -0
  44. data/lib/stannum/entities/attributes.rb +316 -0
  45. data/lib/stannum/entities/constraints.rb +178 -0
  46. data/lib/stannum/entities/primary_key.rb +148 -0
  47. data/lib/stannum/entities/properties.rb +208 -0
  48. data/lib/stannum/entities.rb +16 -0
  49. data/lib/stannum/entity.rb +87 -0
  50. data/lib/stannum/errors.rb +12 -16
  51. data/lib/stannum/messages/default_strategy.rb +2 -2
  52. data/lib/stannum/parameter_validation.rb +10 -10
  53. data/lib/stannum/rspec/match_errors_matcher.rb +7 -7
  54. data/lib/stannum/rspec/validate_parameter.rb +2 -2
  55. data/lib/stannum/rspec/validate_parameter_matcher.rb +22 -20
  56. data/lib/stannum/schema.rb +117 -76
  57. data/lib/stannum/struct.rb +12 -346
  58. data/lib/stannum/support/optional.rb +1 -1
  59. data/lib/stannum/version.rb +4 -4
  60. data/lib/stannum.rb +6 -0
  61. metadata +26 -85
data/config/locales/en.rb CHANGED
@@ -7,6 +7,7 @@
7
7
  absent: 'is nil or empty',
8
8
  anything: 'is a value',
9
9
  does_not_have_methods: 'does not respond to the methods',
10
+ does_not_match_format: 'does not match the expected format',
10
11
  has_methods: 'responds to the methods',
11
12
  hashes: {
12
13
  extra_keys: 'has extra keys',
@@ -15,10 +16,12 @@
15
16
  no_extra_keys: 'does not have extra keys'
16
17
  },
17
18
  invalid: 'is invalid',
19
+ is_a_uuid: 'is a valid UUID',
18
20
  is_boolean: 'is true or false',
19
21
  is_in_list: 'is in the list',
20
22
  is_in_union: 'matches one of the constraints',
21
23
  is_equal_to: 'is equal to',
24
+ is_not_a_uuid: 'is not a valid UUID',
22
25
  is_not_boolean: 'is not true or false',
23
26
  is_not_equal_to: 'is not equal to',
24
27
  is_not_in_list: 'is not in the list',
@@ -39,6 +42,7 @@
39
42
  end
40
43
  end,
41
44
  is_value: 'is the expected value',
45
+ matches_format: 'matches the expected format',
42
46
  parameters: {
43
47
  extra_arguments: 'has extra arguments',
44
48
  extra_keywords: 'has extra keywords',
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum'
4
+
5
+ module Stannum
6
+ # Data object representing an association on an entity.
7
+ class Association # rubocop:disable Metrics/ClassLength
8
+ # Exception raised when calling an abstract method.
9
+ class AbstractAssociationError < StandardError; end
10
+
11
+ # Exception raised when referencing an invalid inverse association..
12
+ class InverseAssociationError < StandardError; end
13
+
14
+ # Builder class for defining association methods on an entity.
15
+ class Builder
16
+ # @param schema [Stannum::Schema] the associations schema on which to
17
+ # define methods.
18
+ def initialize(schema)
19
+ @schema = schema
20
+ end
21
+
22
+ # @return [Stannum::Schema] the associations schema on which to define
23
+ # methods.
24
+ attr_reader :schema
25
+
26
+ # Defines the reader and writer methods for the association.
27
+ #
28
+ # @param association [Stannum::Association]
29
+ def call(association)
30
+ define_reader(association)
31
+ define_writer(association)
32
+ end
33
+
34
+ private
35
+
36
+ def define_reader(association)
37
+ schema.define_method(association.reader_name) do
38
+ association.get_value(self)
39
+ end
40
+ end
41
+
42
+ def define_writer(association)
43
+ schema.define_method(association.writer_name) do |value|
44
+ association.clear_value(self)
45
+
46
+ association.set_value(self, value)
47
+ end
48
+ end
49
+ end
50
+
51
+ # @param name [String, Symbol] The name of the association. Converted to a
52
+ # String.
53
+ # @param options [Hash, nil] Options for the association. Converted to a
54
+ # Hash with Symbol keys. Defaults to an empty Hash.
55
+ # @param type [Class, Module, String] The type of the association. Can be a
56
+ # Class, a Module, or the name of a class or module.
57
+ def initialize(name:, options:, type:)
58
+ validate_name(name)
59
+ validate_options(options)
60
+ validate_type(type)
61
+
62
+ @name = name.to_s
63
+ @options = tools.hash_tools.convert_keys_to_symbols(options || {})
64
+
65
+ @type, @resolved_type = resolve_type(type)
66
+ end
67
+
68
+ # @return [String] the name of the association.
69
+ attr_reader :name
70
+
71
+ # @return [Hash] the association options.
72
+ attr_reader :options
73
+
74
+ # @return [String] the name of the association type Class or Module.
75
+ attr_reader :type
76
+
77
+ # @api private
78
+ #
79
+ # Adds the given value to the association for the entity.
80
+ #
81
+ # @param entity [Stannum::Entity] the entity to update.
82
+ # @param value [Object] the new value for the association.
83
+ # @param update_inverse [Boolean] if true, updates the inverse association
84
+ # (if any). Defaults to false.
85
+ #
86
+ # @return [void]
87
+ def add_value(entity, value, update_inverse: true) # rubocop:disable Lint/UnusedMethodArgument
88
+ raise AbstractAssociationError,
89
+ "#{self.class} is an abstract class - use an association subclass"
90
+ end
91
+
92
+ # @api private
93
+ #
94
+ # Removes the value of the association for the entity.
95
+ #
96
+ # @param entity [Stannum::Entity] the entity to update.
97
+ #
98
+ # @return [void]
99
+ def clear_value(entity, update_inverse: true) # rubocop:disable Lint/UnusedMethodArgument
100
+ raise AbstractAssociationError,
101
+ "#{self.class} is an abstract class - use an association subclass"
102
+ end
103
+
104
+ # @return [String, nil] the name of the original entity class.
105
+ def entity_class_name
106
+ @options[:entity_class_name]
107
+ end
108
+
109
+ # @return [true, false] true if the association has defines a foreign key;
110
+ # otherwise false.
111
+ def foreign_key?
112
+ false
113
+ end
114
+
115
+ # @return [String?] the name of the foreign key, if any.
116
+ def foreign_key_name
117
+ nil
118
+ end
119
+
120
+ # @return [Class, Stannum::Constraint, nil] the type of the foreign key, if
121
+ # any.
122
+ def foreign_key_type
123
+ nil
124
+ end
125
+
126
+ # @api private
127
+ #
128
+ # Retrieves the value of the association for the entity.
129
+ #
130
+ # @param entity [Stannum::Entity] the entity to update.
131
+ #
132
+ # @return [Object] the value of the association.
133
+ def get_value(entity) # rubocop:disable Lint/UnusedMethodArgument
134
+ raise AbstractAssociationError,
135
+ "#{self.class} is an abstract class - use an association subclass"
136
+ end
137
+
138
+ # @return [Boolean] true if the association has an inverse association;
139
+ # otherwise false.
140
+ def inverse?
141
+ !!@options[:inverse]
142
+ end
143
+
144
+ # @return [String] the name of the inverse association, if any.
145
+ def inverse_name
146
+ @inverse_name ||= resolve_inverse_name
147
+ end
148
+
149
+ # @return [false] true if the association is a plural association;
150
+ # otherwise false.
151
+ def many?
152
+ false
153
+ end
154
+
155
+ # @return [false] true if the association is a singular association;
156
+ # otherwise false.
157
+ def one?
158
+ false
159
+ end
160
+
161
+ # @return [Symbol] the name of the reader method for the association.
162
+ def reader_name
163
+ @reader_name ||= name.intern
164
+ end
165
+
166
+ # @api private
167
+ #
168
+ # Removes the given value from the association for the entity.
169
+ #
170
+ # @param entity [Stannum::Entity] the entity to update.
171
+ # @param value [Stannum::Entity] the association value to remove.
172
+ # @param update_inverse [Boolean] if true, updates the inverse association
173
+ # (if any). Defaults to false.
174
+ #
175
+ # @return [void]
176
+ def remove_value(entity, value, update_inverse: true) # rubocop:disable Lint/UnusedMethodArgument
177
+ raise AbstractAssociationError,
178
+ "#{self.class} is an abstract class - use an association subclass"
179
+ end
180
+
181
+ # @return [Stannum::Association] the inverse association, if any.
182
+ def resolved_inverse
183
+ return @resolved_inverse if @resolved_inverse
184
+
185
+ return unless inverse?
186
+
187
+ @resolved_inverse = resolved_type.associations[inverse_name]
188
+ rescue KeyError => exception
189
+ raise InverseAssociationError,
190
+ "unable to resolve inverse association #{exception.key.inspect}"
191
+ end
192
+
193
+ # @return [Module] the type of the association.
194
+ def resolved_type
195
+ return @resolved_type if @resolved_type
196
+
197
+ @resolved_type = Object.const_get(type)
198
+
199
+ unless @resolved_type.is_a?(Module)
200
+ raise NameError, "constant #{type} is not a Class or Module"
201
+ end
202
+
203
+ @resolved_type
204
+ end
205
+
206
+ # @api private
207
+ #
208
+ # Replaces the association for the entity with the given value.
209
+ #
210
+ # @param entity [Stannum::Entity] the entity to update.
211
+ # @param value [Object] the new value for the association.
212
+ # @param update_inverse [Boolean] if true, updates the inverse association
213
+ # (if any). Defaults to false.
214
+ #
215
+ # @return [void]
216
+ def set_value(entity, value, update_inverse: true) # rubocop:disable Lint/UnusedMethodArgument
217
+ raise AbstractAssociationError,
218
+ "#{self.class} is an abstract class - use an association subclass"
219
+ end
220
+
221
+ # @return [Symbol] the name of the writer method for the association.
222
+ def writer_name
223
+ @writer_name ||= :"#{name}="
224
+ end
225
+
226
+ private
227
+
228
+ def plural_class_name
229
+ @plural_class_name ||= tools.string_tools.pluralize(singular_class_name)
230
+ end
231
+
232
+ def resolve_inverse_name # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
233
+ return unless inverse?
234
+
235
+ return options[:inverse_name].to_s if options[:inverse_name]
236
+
237
+ if resolved_type.associations.key?(singular_class_name)
238
+ return singular_class_name
239
+ end
240
+
241
+ if resolved_type.associations.key?(plural_class_name)
242
+ return plural_class_name
243
+ end
244
+
245
+ raise InverseAssociationError,
246
+ 'unable to resolve inverse association ' \
247
+ "#{singular_class_name.inspect} or #{plural_class_name.inspect}"
248
+ end
249
+
250
+ def resolve_type(type)
251
+ return [type, nil] if type.is_a?(String)
252
+
253
+ [type.to_s, type]
254
+ end
255
+
256
+ def singular_class_name
257
+ @singular_class_name ||=
258
+ entity_class_name
259
+ .split('::')
260
+ .last
261
+ .then { |str| tools.string_tools.underscore(str) }
262
+ end
263
+
264
+ def tools
265
+ SleepingKingStudios::Tools::Toolbelt.instance
266
+ end
267
+
268
+ def validate_name(name)
269
+ tools.assertions.validate_name(name, as: 'name')
270
+ end
271
+
272
+ def validate_options(options)
273
+ return if options.nil? || options.is_a?(Hash)
274
+
275
+ raise ArgumentError, 'options must be a Hash or nil'
276
+ end
277
+
278
+ def validate_type(type)
279
+ raise ArgumentError, "type can't be blank" if type.nil?
280
+
281
+ return if type.is_a?(Module)
282
+
283
+ if type.is_a?(String)
284
+ return unless type.empty?
285
+
286
+ raise ArgumentError, "type can't be blank"
287
+ end
288
+
289
+ raise ArgumentError,
290
+ 'type must be a Class, a Module, or the name of a class or module'
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'stannum/associations'
6
+
7
+ module Stannum::Associations
8
+ # Data object representing a plural association.
9
+ class Many < Stannum::Association
10
+ # Wrapper object for an entity's plural association.
11
+ class Proxy
12
+ include Enumerable
13
+ extend Forwardable
14
+
15
+ # @param association [Stannum::Associations::Many] the association being
16
+ # wrapped.
17
+ # @param entity [Stannum::Entity] the entity instance whose association is
18
+ # being wrapped.
19
+ def initialize(association:, entity:)
20
+ @association = association
21
+ @entity = entity
22
+ end
23
+
24
+ # @!method [](index)
25
+ # Retrieves the value of the association at the specified index.
26
+ #
27
+ # @param index [Integer] the index of the value to retrieve.
28
+ #
29
+ # @return [Stannum::Entity] the association value at the index.
30
+ def_delegator :data, :[]
31
+
32
+ # @!method each(&block)
33
+ # @overload each
34
+ # @return [Enumerator] an enumerator over each item in the entity's
35
+ # association data.
36
+ #
37
+ # @overload each(&block)
38
+ # @yield Yields each item in the entity's association data.
39
+ # @yieldparam item [Stannum::Entity] the associated entity.
40
+ def_delegator :data, :each
41
+
42
+ # @!method empty?
43
+ # @return [true, false] true if the association has no items; otherwise
44
+ # false.
45
+ def_delegator :data, :empty?
46
+
47
+ # @!method size
48
+ # @return [Integer] the number of items in the association.
49
+ def_delegator :data, :size
50
+ alias size count
51
+
52
+ # @param other [Object] the object to compare.
53
+ #
54
+ # @return [true, false] true if the object has matching data; otherwise
55
+ # false.
56
+ def ==(other)
57
+ return false if other.nil?
58
+
59
+ return false unless other.respond_to?(:to_a)
60
+
61
+ to_a == other.to_a
62
+ end
63
+
64
+ # Appends the entity to the association.
65
+ #
66
+ # If the entity is already in the association data, this method does
67
+ # nothing.
68
+ #
69
+ # @param value [Stannum::Entity] the entity to add.
70
+ #
71
+ # @return [self] the association proxy.
72
+ def add(value)
73
+ unless value.is_a?(association.resolved_type)
74
+ message =
75
+ 'invalid association item - must be an instance of ' \
76
+ "#{association.resolved_type.name}"
77
+
78
+ raise ArgumentError, message
79
+ end
80
+
81
+ association.add_value(entity, value)
82
+
83
+ self
84
+ end
85
+ alias << add
86
+ alias push add
87
+
88
+ # @return [String] a human-readable string representation of the object.
89
+ def inspect
90
+ "#{super[...55]} data=[#{each.map(&:inspect).join(', ')}]>"
91
+ end
92
+
93
+ # Removes the entity from the association.
94
+ #
95
+ # If the entity is not in the association data, this method does nothing.
96
+ #
97
+ # @param value [Stannum::Entity] the entity to remove.
98
+ #
99
+ # @return [self] the association proxy.
100
+ def remove(value)
101
+ unless value.is_a?(association.resolved_type)
102
+ message =
103
+ 'invalid association item - must be an instance of ' \
104
+ "#{association.resolved_type.name}"
105
+
106
+ raise ArgumentError, message
107
+ end
108
+
109
+ association.remove_value(entity, value)
110
+
111
+ self
112
+ end
113
+ alias delete remove
114
+
115
+ private
116
+
117
+ attr_reader :association
118
+
119
+ attr_reader :entity
120
+
121
+ def data
122
+ entity.read_association(association.name, safe: false) || []
123
+ end
124
+ end
125
+
126
+ # (see Stannum::Association#add_value)
127
+ def add_value(entity, value, update_inverse: true)
128
+ return unless value
129
+
130
+ data = entity.read_association(name, safe: false) || []
131
+
132
+ data, changed = add_item(data:, value:)
133
+
134
+ entity.write_association(name, data, safe: false) if changed
135
+
136
+ update_item_inverse(entity:, value:) if inverse? && update_inverse
137
+
138
+ nil
139
+ end
140
+
141
+ # (see Stannum::Association#clear_value)
142
+ def clear_value(entity, update_inverse: true)
143
+ data = entity.read_association(name, safe: false) || []
144
+
145
+ entity.write_association(name, [], safe: false)
146
+
147
+ if inverse? && update_inverse
148
+ data.each { |item| remove_item_inverse(value: item) }
149
+ end
150
+
151
+ nil
152
+ end
153
+
154
+ # (see Stannum::Association#get_value)
155
+ def get_value(entity)
156
+ entity.association_proxy_for(self)
157
+ end
158
+
159
+ # @return [true] true if the association is a plural association; otherwise
160
+ # false.
161
+ def many?
162
+ true
163
+ end
164
+
165
+ # (see Stannum::Association#remove_value)
166
+ def remove_value(entity, value, update_inverse: true)
167
+ return unless value
168
+
169
+ data = entity.read_association(name, safe: false) || []
170
+
171
+ data, changed = remove_item(data:, value:)
172
+
173
+ return unless changed
174
+
175
+ entity.write_association(name, data, safe: false)
176
+
177
+ remove_item_inverse(value:) if inverse? && update_inverse
178
+
179
+ nil
180
+ end
181
+
182
+ # (see Stannum::Association#resolved_inverse)
183
+ def resolved_inverse
184
+ return @resolved_inverse if @resolved_inverse
185
+
186
+ inverse = super
187
+
188
+ return inverse unless inverse&.many?
189
+
190
+ raise InverseAssociationError,
191
+ "invalid inverse association #{inverse_name.inspect} - :many to " \
192
+ ':many associations are not currently supported'
193
+ end
194
+
195
+ # (see Stannum::Association#set_value)
196
+ def set_value(entity, value, update_inverse: true)
197
+ data = entity.read_association(name, safe: false) || []
198
+
199
+ value&.each do |item|
200
+ next unless item
201
+
202
+ data, _ = add_item(data:, value: item)
203
+
204
+ update_item_inverse(entity:, value: item) if inverse? && update_inverse
205
+ end
206
+
207
+ entity.write_association(name, data, safe: false)
208
+
209
+ nil
210
+ end
211
+
212
+ private
213
+
214
+ def add_item(data:, value:)
215
+ return data, false if data.include?(value)
216
+
217
+ data = [*data, value]
218
+
219
+ [data, true]
220
+ end
221
+
222
+ def remove_item(data:, value:)
223
+ return data, false unless data.include?(value)
224
+
225
+ data = data.reject { |item| item == value }
226
+
227
+ [data, true]
228
+ end
229
+
230
+ def remove_item_inverse(value:, inverse_value: nil, update_inverse: false)
231
+ inverse_value ||= resolved_inverse.get_value(value)
232
+
233
+ return unless inverse_value
234
+
235
+ resolved_inverse.remove_value(value, inverse_value, update_inverse:)
236
+ end
237
+
238
+ def update_item_inverse(entity:, value:)
239
+ inverse_value = resolved_inverse.get_value(value)
240
+
241
+ return if entity == inverse_value
242
+
243
+ if inverse_value
244
+ remove_item_inverse(inverse_value:, value:, update_inverse: true)
245
+ end
246
+
247
+ resolved_inverse.add_value(value, entity, update_inverse: false)
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/associations'
4
+
5
+ module Stannum::Associations
6
+ # Data object representing a singular association.
7
+ class One < Stannum::Association
8
+ # (see Stannum::Association#add_value)
9
+ def add_value(entity, value, update_inverse: true)
10
+ set_value(entity, value, update_inverse:)
11
+ end
12
+
13
+ # (see Stannum::Association#clear_value)
14
+ def clear_value(entity, update_inverse: true)
15
+ previous_value = entity.read_association(name, safe: false)
16
+
17
+ if update_inverse && previous_value && inverse?
18
+ resolved_inverse.remove_value(
19
+ previous_value,
20
+ entity,
21
+ update_inverse: false
22
+ )
23
+ end
24
+
25
+ entity.write_attribute(foreign_key_name, nil, safe: false) if foreign_key?
26
+
27
+ entity.write_association(name, nil, safe: false)
28
+ end
29
+
30
+ # @return [Boolean] true if the association has a foreign key; otherwise
31
+ # false.
32
+ def foreign_key?
33
+ return @has_foreign_key unless @has_foreign_key.nil?
34
+
35
+ value = options[:foreign_key_name]
36
+
37
+ return @has_foreign_key = false if value.nil? || value == false
38
+
39
+ @has_foreign_key = true
40
+ end
41
+
42
+ # @return [String?] the name of the foreign key, if any.
43
+ def foreign_key_name
44
+ return nil unless foreign_key?
45
+
46
+ @foreign_key_name ||= options[:foreign_key_name].to_s
47
+ end
48
+
49
+ # @return [Class, Stannum::Constraint, nil] the type of the foreign key, if
50
+ # any.
51
+ def foreign_key_type
52
+ return nil unless foreign_key?
53
+
54
+ @foreign_key_type ||= options[:foreign_key_type]
55
+ end
56
+
57
+ # (see Stannum::Association#get_value)
58
+ def get_value(entity)
59
+ entity.read_association(name, safe: false)
60
+ end
61
+
62
+ # @return [true] true if the association is a singular association;
63
+ # otherwise false.
64
+ def one?
65
+ true
66
+ end
67
+
68
+ # (see Stannum::Association#remove_value)
69
+ def remove_value(entity, value, update_inverse: true)
70
+ previous_value = entity.read_association(name, safe: false)
71
+
72
+ return unless matching_value?(value, previous_value)
73
+
74
+ clear_value(entity, update_inverse:)
75
+ end
76
+
77
+ # (see Stannum::Association#set_value)
78
+ def set_value(entity, value, update_inverse: true) # rubocop:disable Metrics/MethodLength
79
+ if foreign_key?
80
+ entity.write_attribute(
81
+ foreign_key_name,
82
+ value&.primary_key,
83
+ safe: false
84
+ )
85
+ end
86
+
87
+ entity.write_association(name, value, safe: false)
88
+
89
+ return unless update_inverse && value && inverse?
90
+
91
+ previous_inverse = resolved_inverse.get_value(value)
92
+
93
+ resolved_inverse.remove_value(value, previous_inverse) if previous_inverse
94
+
95
+ resolved_inverse.add_value(value, entity, update_inverse: false)
96
+ end
97
+
98
+ private
99
+
100
+ def matching_value?(value, previous_value)
101
+ return true if value == previous_value
102
+
103
+ foreign_key? && (value == previous_value&.primary_key)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum'
4
+
5
+ module Stannum
6
+ # Namespace for modules implementing Association functionality.
7
+ module Associations
8
+ autoload :Many, 'stannum/associations/many'
9
+ autoload :One, 'stannum/associations/one'
10
+ end
11
+ end