store_model 4.2.1 → 4.3.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/README.md +6 -4
- data/lib/active_model/validations/store_model_validator.rb +13 -0
- data/lib/store_model/combine_errors_strategies/merge_hash_error_strategy.rb +22 -0
- data/lib/store_model/combine_errors_strategies.rb +15 -0
- data/lib/store_model/configuration.rb +9 -0
- data/lib/store_model/model.rb +43 -6
- data/lib/store_model/type_builders.rb +6 -0
- data/lib/store_model/types/hash.rb +53 -0
- data/lib/store_model/types/hash_base.rb +79 -0
- data/lib/store_model/types/hash_polymorphic.rb +57 -0
- data/lib/store_model/types/one.rb +2 -2
- data/lib/store_model/types/one_of.rb +4 -0
- data/lib/store_model/types/one_polymorphic.rb +2 -2
- data/lib/store_model/types.rb +4 -0
- data/lib/store_model/version.rb +1 -1
- data/lib/store_model.rb +56 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c65c5b8e184e6b4f7c402a7a2a82df8228d2b55c7ad6352c3752ac988bc86943
|
4
|
+
data.tar.gz: f2f404a6330ded65170e6256adb71c109592c6187a85d221d6887c965eeb9a1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf0146c740b94e5591d16794431120afc4c28dfb3477757571d4d77fb8833836fccdb94d5776f9199f92164ab36029def584d3a802ee186b531916d82c098e6b
|
7
|
+
data.tar.gz: ae1753153b834b2b02bfa2ec4afc767540833eea27f59e653c3dbf8435bba3c1810999fba5128d71ce97e38e31314bc5622c8a01eb3054e743acd337bfae1b09
|
data/README.md
CHANGED
@@ -131,11 +131,13 @@ end
|
|
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. [
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
@@ -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
|
data/lib/store_model/model.rb
CHANGED
@@ -32,10 +32,18 @@ module StoreModel
|
|
32
32
|
def from_values(values)
|
33
33
|
to_array_type.cast_value(values)
|
34
34
|
end
|
35
|
+
|
36
|
+
# Defines a discriminator attribute with a value
|
37
|
+
# @param discriminator [Symbol, String] attribute name (default: :type)
|
38
|
+
# @param type [Symbol, String] attribute type (default: :string)
|
39
|
+
# @param value [String] the discriminator value
|
40
|
+
def discriminator_attribute(discriminator = "type", value:, type: :string)
|
41
|
+
attribute discriminator, type, default: value
|
42
|
+
end
|
35
43
|
end
|
36
44
|
|
37
45
|
attr_accessor :parent
|
38
|
-
attr_writer :serialize_unknown_attributes, :serialize_enums_using_as_json
|
46
|
+
attr_writer :serialize_unknown_attributes, :serialize_enums_using_as_json, :serialize_empty_attributes
|
39
47
|
|
40
48
|
delegate :each_value, to: :attributes
|
41
49
|
|
@@ -45,7 +53,7 @@ module StoreModel
|
|
45
53
|
# @param options [Hash]
|
46
54
|
#
|
47
55
|
# @return [Hash]
|
48
|
-
def as_json(options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
56
|
+
def as_json(options = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
49
57
|
serialize_unknown_attributes = if options.key?(:serialize_unknown_attributes)
|
50
58
|
options[:serialize_unknown_attributes]
|
51
59
|
else
|
@@ -58,10 +66,20 @@ module StoreModel
|
|
58
66
|
StoreModel.config.serialize_enums_using_as_json
|
59
67
|
end
|
60
68
|
|
69
|
+
serialize_empty_attributes = if options.key?(:serialize_empty_attributes)
|
70
|
+
options[:serialize_empty_attributes]
|
71
|
+
else
|
72
|
+
StoreModel.config.serialize_empty_attributes
|
73
|
+
end
|
74
|
+
|
75
|
+
# If the model is nested, we need to ensure that the serialization
|
76
|
+
|
61
77
|
result = @attributes.keys.each_with_object({}) do |key, values|
|
62
78
|
attr = @attributes.fetch(key)
|
63
|
-
assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json
|
64
|
-
|
79
|
+
assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json,
|
80
|
+
serialize_empty_attributes)
|
81
|
+
serialized = serialized_attribute(attr)
|
82
|
+
values[key] = serialized if serialize_empty_attributes || !serialized.nil?
|
65
83
|
end.with_indifferent_access
|
66
84
|
|
67
85
|
result.merge!(unknown_attributes) if serialize_unknown_attributes
|
@@ -226,6 +244,23 @@ module StoreModel
|
|
226
244
|
end
|
227
245
|
end
|
228
246
|
|
247
|
+
# Returns the value of the `@serialize_empty_attributes` instance
|
248
|
+
# variable. The default value is the value of the globally configured
|
249
|
+
# `serialize_empty_attributes` option.
|
250
|
+
#
|
251
|
+
# This method is used to determine whether empty values should be serialized
|
252
|
+
# when the `as_json` method is called in nested StoreModel::Model
|
253
|
+
# objects.
|
254
|
+
#
|
255
|
+
# @return [Boolean]
|
256
|
+
def serialize_empty_attributes?
|
257
|
+
if @serialize_empty_attributes.nil?
|
258
|
+
StoreModel.config.serialize_empty_attributes || true
|
259
|
+
else
|
260
|
+
@serialize_empty_attributes
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
229
264
|
private
|
230
265
|
|
231
266
|
def attribute?(attribute)
|
@@ -263,16 +298,18 @@ module StoreModel
|
|
263
298
|
|
264
299
|
array.as_json(
|
265
300
|
serialize_unknown_attributes: array.first.serialize_unknown_attributes?,
|
266
|
-
serialize_enums_using_as_json: array.first.serialize_enums_using_as_json
|
301
|
+
serialize_enums_using_as_json: array.first.serialize_enums_using_as_json?,
|
302
|
+
serialize_empty_attributes: array.first.serialize_empty_attributes?
|
267
303
|
)
|
268
304
|
end
|
269
305
|
|
270
|
-
def assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json)
|
306
|
+
def assign_serialization_options(attr, serialize_unknown_attributes, serialize_enums_using_as_json, serialize_empty_attributes) # rubocop:disable Layout/LineLength
|
271
307
|
return unless Array(attr.value).all? { |value| value.is_a?(StoreModel::Model) }
|
272
308
|
|
273
309
|
Array(attr.value).each do |value|
|
274
310
|
value.serialize_unknown_attributes = serialize_unknown_attributes
|
275
311
|
value.serialize_enums_using_as_json = serialize_enums_using_as_json
|
312
|
+
value.serialize_empty_attributes = serialize_empty_attributes
|
276
313
|
end
|
277
314
|
end
|
278
315
|
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
|
@@ -38,7 +38,7 @@ module StoreModel
|
|
38
38
|
elsif value.class.ancestors.include?(StoreModel::Model)
|
39
39
|
value
|
40
40
|
elsif value.respond_to?(:to_h) # Hash itself included
|
41
|
-
extract_model_klass(value).
|
41
|
+
extract_model_klass(value).to_type.cast_value(value.to_h)
|
42
42
|
else
|
43
43
|
raise_cast_error(value)
|
44
44
|
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(
|
data/lib/store_model/types.rb
CHANGED
@@ -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"
|
data/lib/store_model/version.rb
CHANGED
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.
|
4
|
+
version: 4.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- DmitryTsepelev
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
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.
|
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: []
|