store_model 4.2.2 → 4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c266a446e1b239d465203bd52f8a611559ea11b511f3c3f0e737f8ac356d0ae
4
- data.tar.gz: 6d9f845eea8855b7c4f5cf154a4f6d02dcc7d94801f2346ed5c3e83b5f04dc74
3
+ metadata.gz: fe31f43187a97021f54c79cbc8372b7e2b1c5a3c4bcb7b07b13def128bf69f34
4
+ data.tar.gz: '0456813d7c47e3a9888c23bd83dc90c233f8919734766e348c064350c3d502ce'
5
5
  SHA512:
6
- metadata.gz: ad2ed153d5f0bc68ffde00f4254d60e05732fe7e3d0c826fa5a5720a8e77e4af3d0293e10941c1b3001789c1d26dc9cbb1f8624f8492cf914f8a65cddbcec250
7
- data.tar.gz: 350da1596c977c36fbe7df0f311e1969a182501ba520f6e64aa6cbe61b84da8b9d86e6ac89bbd1738263fbc13c1868add33455867fda90b6eeed407f966afa0c
6
+ metadata.gz: 54a697b1a4782fa6bf3a6f84972789f6b42aab7ef9702e243d7fa4b9d7a6c035ea1421a560eb848083dd414b9bd594d577ca82a50642718d80607d742db64561
7
+ data.tar.gz: d591bf765bb19d91cc7972762349075edbb5ec9d633f3b0e4b9c290069af132e35b9d011e4537de43c18f3de76ca863e8b0576d93502ac95b24e4abb250bedd3
data/README.md CHANGED
@@ -125,17 +125,19 @@ end
125
125
  ## Documentation
126
126
 
127
127
  1. [Installation](./docs/installation.md)
128
- 2. StoreModel::Model API:
128
+ 2. `StoreModel::Model` API:
129
129
  * [Instantiation](./docs/instantiation.md)
130
130
  * [Validations](./docs/validations.md)
131
131
  * [Enums](./docs/enums.md)
132
132
  * [Nested models](./docs/nested_models.md)
133
133
  * [Unknown attributes](./docs/unknown_attributes.md)
134
+ * [Empty attributes](./docs/empty_attributes.md)
134
135
  3. [Array of stored models](./docs/array_of_stored_models.md)
135
- 4. [One of](./docs/one_of.md)
136
- 4. [Alternatives](./docs/alternatives.md)
137
- 5. [Defining custom types](./docs/defining_custom_types.md)
138
- 6. [Disabling Parent Tracking](./docs/enable_parent_assignment.md)
136
+ 4. [Hash of stored models](./docs/hash_of_stored_models.md)
137
+ 5. [One of](./docs/one_of.md)
138
+ 6. [Alternatives](./docs/alternatives.md)
139
+ 7. [Defining custom types](./docs/defining_custom_types.md)
140
+ 8. [Disabling Parent Tracking](./docs/enable_parent_assignment.md)
139
141
 
140
142
  ## Credits
141
143
 
@@ -14,6 +14,7 @@ module ActiveModel
14
14
  # @param record [ApplicationRecord] object to validate
15
15
  # @param attribute [String] name of the validated attribute
16
16
  # @param value [Object] value of the validated attribute
17
+ # rubocop:disable Metrics/MethodLength
17
18
  def validate_each(record, attribute, value)
18
19
  if value.nil?
19
20
  record.errors.add(attribute, :blank)
@@ -25,8 +26,11 @@ module ActiveModel
25
26
  call_json_strategy(record, attribute, value)
26
27
  when :array, :polymorphic_array
27
28
  call_array_strategy(record, attribute, value)
29
+ when :hash, :polymorphic_hash
30
+ call_hash_strategy(record, attribute, value)
28
31
  end
29
32
  end
33
+ # rubocop:enable Metrics/MethodLength
30
34
 
31
35
  private
32
36
 
@@ -39,6 +43,11 @@ module ActiveModel
39
43
  array_strategy.call(attribute, record.errors, value) if any_invalid
40
44
  end
41
45
 
46
+ def call_hash_strategy(record, attribute, value)
47
+ invalid_values = value.select { |_k, v| v.invalid?(record.validation_context) }
48
+ hash_strategy.call(attribute, record.errors, invalid_values) if invalid_values.present?
49
+ end
50
+
42
51
  def strategy
43
52
  @strategy ||= StoreModel::CombineErrorsStrategies.configure(options)
44
53
  end
@@ -46,6 +55,10 @@ module ActiveModel
46
55
  def array_strategy
47
56
  @array_strategy ||= StoreModel::CombineErrorsStrategies.configure_array(options)
48
57
  end
58
+
59
+ def hash_strategy
60
+ @hash_strategy ||= StoreModel::CombineErrorsStrategies.configure_hash(options)
61
+ end
49
62
  end
50
63
  end
51
64
  end
@@ -10,8 +10,8 @@ module StoreModel
10
10
  # @param base_errors [ActiveModel::Errors] errors object of the parent record
11
11
  # @param _store_model_errors [ActiveModel::Errors] errors object of the
12
12
  # StoreModel::Model attribute
13
- def call(attribute, base_errors, _store_model_errors)
14
- base_errors.add(attribute, :invalid)
13
+ def call(attribute, base_errors, store_model_errors)
14
+ base_errors.add(attribute, :invalid, errors: store_model_errors)
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StoreModel
4
+ module CombineErrorsStrategies
5
+ # +MergeHashErrorStrategy+ copies errors from the StoreModel::Model to the parent
6
+ # record attribute errors with hash key prefixes.
7
+ class MergeHashErrorStrategy
8
+ # Merges errors on +attribute+ from the child model with parent errors.
9
+ #
10
+ # @param attribute [String] name of the validated attribute
11
+ # @param base_errors [ActiveModel::Errors] errors object of the parent record
12
+ # @param store_models [Hash] a hash of store_models that have been validated
13
+ def call(attribute, base_errors, store_models)
14
+ store_models.each do |key, store_model|
15
+ store_model.errors.full_messages.each do |full_message|
16
+ base_errors.add(attribute, :invalid, message: "[#{key}] #{full_message}")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,6 +3,7 @@
3
3
  require "store_model/combine_errors_strategies/mark_invalid_error_strategy"
4
4
  require "store_model/combine_errors_strategies/merge_error_strategy"
5
5
  require "store_model/combine_errors_strategies/merge_array_error_strategy"
6
+ require "store_model/combine_errors_strategies/merge_hash_error_strategy"
6
7
 
7
8
  module StoreModel
8
9
  # Module with built-in strategies for combining errors.
@@ -37,6 +38,20 @@ module StoreModel
37
38
  )
38
39
  end
39
40
 
41
+ # Finds a hash strategy based on +options+ and global config.
42
+ #
43
+ # @param options [Hash]
44
+ #
45
+ # @return [Object] strategy
46
+ def configure_hash(options)
47
+ configured_strategy = options[:merge_hash_errors] || StoreModel.config.merge_hash_errors
48
+
49
+ get_configured_strategy(
50
+ configured_strategy,
51
+ StoreModel::CombineErrorsStrategies::MergeHashErrorStrategy
52
+ )
53
+ end
54
+
40
55
  def get_configured_strategy(configured_strategy, true_strategy_class)
41
56
  if configured_strategy.respond_to?(:call)
42
57
  configured_strategy
@@ -11,10 +11,18 @@ module StoreModel
11
11
  # @return [Boolean]
12
12
  attr_accessor :merge_array_errors
13
13
 
14
+ # Controls usage of MergeHashErrorStrategy
15
+ # @return [Boolean]
16
+ attr_accessor :merge_hash_errors
17
+
14
18
  # Controls if the result of `as_json` will contain the unknown attributes of the model
15
19
  # @return [Boolean]
16
20
  attr_accessor :serialize_unknown_attributes
17
21
 
22
+ # Controls if the result of `as_json` will contain the nulls attributes of the model
23
+ # @return [Boolean]
24
+ attr_accessor :serialize_empty_attributes
25
+
18
26
  # Controls if the result of `as_json` will serialize enum fields using `as_json`
19
27
  # @return [Boolean]
20
28
  attr_accessor :serialize_enums_using_as_json
@@ -28,6 +36,7 @@ module StoreModel
28
36
  @serialize_unknown_attributes = true
29
37
  @enable_parent_assignment = true
30
38
  @serialize_enums_using_as_json = true
39
+ @serialize_empty_attributes = true
31
40
  end
32
41
  end
33
42
  end
@@ -7,9 +7,9 @@ module StoreModel
7
7
 
8
8
  def assign_parent_to_store_model_relation(attribute)
9
9
  assign_parent_to_singular_store_model(attribute)
10
- return unless attribute.is_a?(Array)
10
+ return if !attribute.is_a?(Array) && !attribute.is_a?(Hash)
11
11
 
12
- attribute.each(&method(:assign_parent_to_singular_store_model))
12
+ (attribute.try(:values) || attribute).each(&method(:assign_parent_to_singular_store_model))
13
13
  end
14
14
 
15
15
  def assign_parent_to_singular_store_model(item)
@@ -8,6 +8,7 @@ require "store_model/nested_attributes"
8
8
  module StoreModel
9
9
  # When included into class configures it to handle JSON column
10
10
  module Model # rubocop:disable Metrics/ModuleLength
11
+ # rubocop:disable Metrics/MethodLength
11
12
  def self.included(base) # :nodoc:
12
13
  base.include ActiveModel::Model
13
14
  base.include ActiveModel::Attributes
@@ -15,6 +16,10 @@ module StoreModel
15
16
  base.include ActiveModel::AttributeMethods
16
17
  base.include StoreModel::NestedAttributes
17
18
 
19
+ if ActiveModel::VERSION::MAJOR >= 8 && ActiveModel::VERSION::MINOR >= 1
20
+ base.include ActiveModel::Attributes::Normalization
21
+ end
22
+
18
23
  base.extend StoreModel::Enum
19
24
  base.extend StoreModel::TypeBuilders
20
25
 
@@ -22,6 +27,7 @@ module StoreModel
22
27
 
23
28
  base.extend(ClassMethods)
24
29
  end
30
+ # rubocop:enable Metrics/MethodLength
25
31
 
26
32
  # Class methods for StoreModel::Model
27
33
  module ClassMethods
@@ -32,10 +38,18 @@ module StoreModel
32
38
  def from_values(values)
33
39
  to_array_type.cast_value(values)
34
40
  end
41
+
42
+ # Defines a discriminator attribute with a value
43
+ # @param discriminator [Symbol, String] attribute name (default: :type)
44
+ # @param type [Symbol, String] attribute type (default: :string)
45
+ # @param value [String] the discriminator value
46
+ def discriminator_attribute(discriminator = "type", value:, type: :string)
47
+ attribute discriminator, type, default: value
48
+ end
35
49
  end
36
50
 
37
51
  attr_accessor :parent
38
- attr_writer :serialize_unknown_attributes, :serialize_enums_using_as_json
52
+ attr_writer :serialize_unknown_attributes, :serialize_enums_using_as_json, :serialize_empty_attributes
39
53
 
40
54
  delegate :each_value, to: :attributes
41
55
 
@@ -45,7 +59,7 @@ module StoreModel
45
59
  # @param options [Hash]
46
60
  #
47
61
  # @return [Hash]
48
- def as_json(options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
62
+ def as_json(options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
49
63
  serialize_unknown_attributes = if options.key?(:serialize_unknown_attributes)
50
64
  options[:serialize_unknown_attributes]
51
65
  else
@@ -58,10 +72,20 @@ module StoreModel
58
72
  StoreModel.config.serialize_enums_using_as_json
59
73
  end
60
74
 
75
+ serialize_empty_attributes = if options.key?(:serialize_empty_attributes)
76
+ options[:serialize_empty_attributes]
77
+ else
78
+ StoreModel.config.serialize_empty_attributes
79
+ end
80
+
81
+ # If the model is nested, we need to ensure that the serialization
82
+
61
83
  result = @attributes.keys.each_with_object({}) do |key, values|
62
84
  attr = @attributes.fetch(key)
63
- assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json)
64
- values[key] = serialized_attribute(attr)
85
+ assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json,
86
+ serialize_empty_attributes)
87
+ serialized = serialized_attribute(attr)
88
+ values[key] = serialized if serialize_empty_attributes || !serialized.nil?
65
89
  end.with_indifferent_access
66
90
 
67
91
  result.merge!(unknown_attributes) if serialize_unknown_attributes
@@ -226,6 +250,23 @@ module StoreModel
226
250
  end
227
251
  end
228
252
 
253
+ # Returns the value of the `@serialize_empty_attributes` instance
254
+ # variable. The default value is the value of the globally configured
255
+ # `serialize_empty_attributes` option.
256
+ #
257
+ # This method is used to determine whether empty values should be serialized
258
+ # when the `as_json` method is called in nested StoreModel::Model
259
+ # objects.
260
+ #
261
+ # @return [Boolean]
262
+ def serialize_empty_attributes?
263
+ if @serialize_empty_attributes.nil?
264
+ StoreModel.config.serialize_empty_attributes || true
265
+ else
266
+ @serialize_empty_attributes
267
+ end
268
+ end
269
+
229
270
  private
230
271
 
231
272
  def attribute?(attribute)
@@ -263,16 +304,18 @@ module StoreModel
263
304
 
264
305
  array.as_json(
265
306
  serialize_unknown_attributes: array.first.serialize_unknown_attributes?,
266
- serialize_enums_using_as_json: array.first.serialize_enums_using_as_json?
307
+ serialize_enums_using_as_json: array.first.serialize_enums_using_as_json?,
308
+ serialize_empty_attributes: array.first.serialize_empty_attributes?
267
309
  )
268
310
  end
269
311
 
270
- def assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json)
312
+ def assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json, serialize_empty_attributes) # rubocop:disable Layout/LineLength
271
313
  return unless Array(attr.value).all? { |value| value.is_a?(StoreModel::Model) }
272
314
 
273
315
  Array(attr.value).each do |value|
274
316
  value.serialize_unknown_attributes = serialize_unknown_attributes
275
317
  value.serialize_enums_using_as_json = serialize_enums_using_as_json
318
+ value.serialize_empty_attributes = serialize_empty_attributes
276
319
  end
277
320
  end
278
321
  end
@@ -14,5 +14,11 @@ module StoreModel
14
14
  def to_array_type
15
15
  Types::Many.new(self)
16
16
  end
17
+
18
+ # Converts StoreModel::Model to Types::Hash
19
+ # @return [Types::Hash]
20
+ def to_hash_type
21
+ Types::Hash.new(self)
22
+ end
17
23
  end
18
24
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module StoreModel
6
+ module Types
7
+ # Implements ActiveModel::Type::Value type for handling a hash of
8
+ # StoreModel::Model
9
+ class Hash < HashBase
10
+ # Initializes type for model class
11
+ #
12
+ # @param model_klass [StoreModel::Model] model class to handle
13
+ #
14
+ # @return [StoreModel::Types::Hash]
15
+ def initialize(model_klass)
16
+ @model_klass = model_klass
17
+ super()
18
+ end
19
+
20
+ # Returns type
21
+ #
22
+ # @return [Symbol]
23
+ def type
24
+ :hash
25
+ end
26
+
27
+ protected
28
+
29
+ def ensure_model_class(hash)
30
+ return {} unless hash.is_a?(::Hash)
31
+
32
+ hash.transform_values do |object|
33
+ next object if object.nil?
34
+
35
+ object.is_a?(@model_klass) ? object : cast_model_type_value(object)
36
+ end
37
+ end
38
+
39
+ def cast_model_type_value(value)
40
+ model_klass_type.cast_value(value)
41
+ end
42
+
43
+ def model_klass_type
44
+ @model_klass_type ||= @model_klass.to_type
45
+ end
46
+
47
+ def raise_cast_error(value)
48
+ raise StoreModel::Types::CastError,
49
+ "failed casting #{value.inspect}, only String or Hash instances are allowed"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module StoreModel
6
+ module Types
7
+ # Implements type for handling a hash of StoreModel::Model
8
+ class HashBase < Base
9
+ # Casts +value+ from DB or user to Hash of StoreModel::Model instances
10
+ #
11
+ # @param value [Object] a value to cast
12
+ #
13
+ # @return Hash
14
+ def cast_value(value)
15
+ case value
16
+ when String then decode_and_initialize(value)
17
+ when ::Hash then ensure_model_class(value)
18
+ when nil then value
19
+ else
20
+ raise_cast_error(value)
21
+ end
22
+ end
23
+
24
+ # Casts a value from the ruby type to a type that the database knows how
25
+ # to understand.
26
+ #
27
+ # @param value [Object] value to serialize
28
+ #
29
+ # @return [String] serialized value
30
+ def serialize(value)
31
+ return super unless value.is_a?(::Hash)
32
+ if value.empty? || value.values.any? { |v| !v.is_a?(StoreModel::Model) }
33
+ return ActiveSupport::JSON.encode(value)
34
+ end
35
+
36
+ ActiveSupport::JSON.encode(
37
+ value,
38
+ serialize_unknown_attributes: value.values.first.serialize_unknown_attributes?,
39
+ serialize_enums_using_as_json: value.values.first.serialize_enums_using_as_json?
40
+ )
41
+ end
42
+
43
+ # Determines whether the mutable value has been modified since it was read
44
+ #
45
+ # @param raw_old_value [Object] old value
46
+ # @param new_value [Object] new value
47
+ #
48
+ # @return [Boolean]
49
+ def changed_in_place?(raw_old_value, new_value)
50
+ cast_value(raw_old_value) != new_value
51
+ end
52
+
53
+ protected
54
+
55
+ def ensure_model_class(_hash)
56
+ raise NotImplementedError
57
+ end
58
+
59
+ def cast_model_type_value(_value)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ private
64
+
65
+ # rubocop:disable Style/RescueModifier
66
+ def decode_and_initialize(hash_value)
67
+ decoded = ActiveSupport::JSON.decode(hash_value) rescue {}
68
+ return {} unless decoded.is_a?(::Hash)
69
+
70
+ decoded.transform_values do |attributes|
71
+ next nil if attributes.nil?
72
+
73
+ cast_model_type_value(attributes)
74
+ end
75
+ end
76
+ # rubocop:enable Style/RescueModifier
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module StoreModel
6
+ module Types
7
+ # Implements ActiveModel::Type::Value type for handling a hash of
8
+ # polymorphic StoreModel::Model
9
+ class HashPolymorphic < HashBase
10
+ include PolymorphicHelper
11
+
12
+ # Initializes type for model class
13
+ #
14
+ # @param model_wrapper [Proc] proc that returns class based on value
15
+ #
16
+ # @return [StoreModel::Types::HashPolymorphic]
17
+ def initialize(model_wrapper)
18
+ @model_wrapper = model_wrapper
19
+ super()
20
+ end
21
+
22
+ # Returns type
23
+ #
24
+ # @return [Symbol]
25
+ def type
26
+ :polymorphic_hash
27
+ end
28
+
29
+ protected
30
+
31
+ def ensure_model_class(hash)
32
+ return {} unless hash.is_a?(::Hash)
33
+
34
+ hash.transform_values do |object|
35
+ next object if object.nil?
36
+ next object if implements_model?(object.class)
37
+
38
+ cast_model_type_value(object)
39
+ end
40
+ end
41
+
42
+ def cast_model_type_value(value)
43
+ model_klass = @model_wrapper.call(value)
44
+
45
+ raise_extract_wrapper_error(model_klass) unless implements_model?(model_klass)
46
+
47
+ model_klass.to_type.cast_value(value)
48
+ end
49
+
50
+ def raise_cast_error(value)
51
+ raise StoreModel::Types::CastError,
52
+ "failed casting #{value.inspect}, only String, " \
53
+ "Hash or instances which implement StoreModel::Model are allowed"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -56,7 +56,7 @@ module StoreModel
56
56
  ActiveSupport::JSON.encode(value,
57
57
  serialize_unknown_attributes: value.serialize_unknown_attributes?,
58
58
  serialize_enums_using_as_json: value.serialize_enums_using_as_json?)
59
- when Hash
59
+ when ::Hash
60
60
  ActiveSupport::JSON.encode(value)
61
61
  else
62
62
  super
@@ -75,7 +75,7 @@ module StoreModel
75
75
  when String
76
76
  payload = ActiveSupport::JSON.decode(value) rescue {}
77
77
  model_instance(deserialize_by_types(payload))
78
- when Hash
78
+ when ::Hash
79
79
  model_instance(deserialize_by_types(value))
80
80
  when nil
81
81
  nil
@@ -18,6 +18,10 @@ module StoreModel
18
18
  def to_array_type
19
19
  Types::ManyPolymorphic.new(@block)
20
20
  end
21
+
22
+ def to_hash_type
23
+ Types::HashPolymorphic.new(@block)
24
+ end
21
25
  end
22
26
  end
23
27
  end
@@ -53,7 +53,7 @@ module StoreModel
53
53
  #
54
54
  # @return [String] serialized value
55
55
  def serialize(value)
56
- return super unless value.is_a?(Hash) || implements_model?(value.class)
56
+ return super unless value.is_a?(::Hash) || implements_model?(value.class)
57
57
 
58
58
  if value.is_a?(StoreModel::Model)
59
59
  ActiveSupport::JSON.encode(
@@ -11,6 +11,10 @@ require "store_model/types/many_base"
11
11
  require "store_model/types/many"
12
12
  require "store_model/types/many_polymorphic"
13
13
 
14
+ require "store_model/types/hash_base"
15
+ require "store_model/types/hash"
16
+ require "store_model/types/hash_polymorphic"
17
+
14
18
  require "store_model/types/enum_type"
15
19
 
16
20
  require "store_model/types/one_of"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StoreModel # :nodoc:
4
- VERSION = "4.2.2"
4
+ VERSION = "4.4.0"
5
5
  end
data/lib/store_model.rb CHANGED
@@ -15,5 +15,61 @@ module StoreModel # :nodoc:
15
15
  def one_of(&block)
16
16
  Types::OneOf.new(&block)
17
17
  end
18
+
19
+ # Creates a union type for polymorphic attributes
20
+ # @param klasses [Array<Class>] array of classes that can be used
21
+ # @param discriminator [String, Symbol] the attribute key to check for type (default: 'type')
22
+ # @return instance [Types::OneOf]
23
+ def union(klasses, discriminator: "type")
24
+ discriminators_and_classes = klasses.map do |cls|
25
+ [cls._default_attributes[discriminator]&.value, cls]
26
+ end
27
+
28
+ validate_missing_discriminators!(discriminator, discriminators_and_classes)
29
+ validate_duplicate_discriminators!(discriminators_and_classes)
30
+
31
+ union_one_of(discriminator, Hash[discriminators_and_classes])
32
+ end
33
+
34
+ private
35
+
36
+ def validate_missing_discriminators!(discriminator, discriminators_and_classes)
37
+ missing_discriminator_classes = discriminators_and_classes.select do |(discriminator_value, _cls)|
38
+ discriminator_value.blank?
39
+ end.map(&:last)
40
+
41
+ return if missing_discriminator_classes.empty?
42
+
43
+ raise "discriminator_attribute not set for #{discriminator} on #{missing_discriminator_classes.join(', ')}"
44
+ end
45
+
46
+ def validate_duplicate_discriminators!(discriminators_and_classes)
47
+ discriminator_counts = discriminators_and_classes.group_by(&:first)
48
+ duplicates = discriminator_counts.select { |_discriminator_value, pairs| pairs.length > 1 }
49
+
50
+ return if duplicates.empty?
51
+
52
+ duplicate_messages = duplicates.map do |discriminator_value, pairs|
53
+ classes = pairs.map(&:last).map(&:name).join(", ")
54
+ "#{discriminator_value.inspect} => [#{classes}]"
55
+ end
56
+
57
+ raise "Duplicate discriminator values found: #{duplicate_messages.join('; ')}"
58
+ end
59
+
60
+ def union_one_of(discriminator, class_map)
61
+ Types::OneOf.new do |attributes|
62
+ next nil unless attributes
63
+
64
+ discriminator_value = attributes.with_indifferent_access[discriminator]
65
+
66
+ raise ArgumentError, "Missing discriminator attribute #{discriminator} for union" if discriminator_value.blank?
67
+
68
+ cls = class_map[discriminator_value]
69
+ raise ArgumentError, "Unknown discriminator value for union: #{discriminator_value}" if cls.blank?
70
+
71
+ cls
72
+ end
73
+ end
18
74
  end
19
75
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: store_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.2
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-06-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -82,6 +81,7 @@ files:
82
81
  - lib/store_model/combine_errors_strategies/mark_invalid_error_strategy.rb
83
82
  - lib/store_model/combine_errors_strategies/merge_array_error_strategy.rb
84
83
  - lib/store_model/combine_errors_strategies/merge_error_strategy.rb
84
+ - lib/store_model/combine_errors_strategies/merge_hash_error_strategy.rb
85
85
  - lib/store_model/configuration.rb
86
86
  - lib/store_model/enum.rb
87
87
  - lib/store_model/ext/active_model/attributes.rb
@@ -94,6 +94,9 @@ files:
94
94
  - lib/store_model/types.rb
95
95
  - lib/store_model/types/base.rb
96
96
  - lib/store_model/types/enum_type.rb
97
+ - lib/store_model/types/hash.rb
98
+ - lib/store_model/types/hash_base.rb
99
+ - lib/store_model/types/hash_polymorphic.rb
97
100
  - lib/store_model/types/many.rb
98
101
  - lib/store_model/types/many_base.rb
99
102
  - lib/store_model/types/many_polymorphic.rb
@@ -108,7 +111,6 @@ homepage: https://github.com/DmitryTsepelev/store_model
108
111
  licenses:
109
112
  - MIT
110
113
  metadata: {}
111
- post_install_message:
112
114
  rdoc_options: []
113
115
  require_paths:
114
116
  - lib
@@ -123,8 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
125
  - !ruby/object:Gem::Version
124
126
  version: '0'
125
127
  requirements: []
126
- rubygems_version: 3.5.11
127
- signing_key:
128
+ rubygems_version: 3.6.7
128
129
  specification_version: 4
129
130
  summary: Gem for working with JSON-backed attributes as ActiveRecord models
130
131
  test_files: []