store_model 1.6.1 → 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 +4 -4
- data/README.md +54 -6
- data/lib/active_model/validations/store_model_validator.rb +13 -0
- data/lib/store_model/combine_errors_strategies/mark_invalid_error_strategy.rb +2 -2
- 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 +20 -0
- data/lib/store_model/enum.rb +19 -10
- data/lib/store_model/ext/parent_assignment.rb +2 -2
- data/lib/store_model/model.rb +148 -5
- data/lib/store_model/nested_attributes.rb +89 -15
- data/lib/store_model/railtie.rb +4 -2
- data/lib/store_model/type_builders.rb +6 -0
- data/lib/store_model/types/base.rb +25 -0
- data/lib/store_model/types/enum_type.rb +8 -6
- 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/many.rb +1 -0
- data/lib/store_model/types/many_base.rb +7 -17
- data/lib/store_model/types/many_polymorphic.rb +1 -0
- data/lib/store_model/types/one.rb +47 -8
- data/lib/store_model/types/one_base.rb +2 -15
- data/lib/store_model/types/one_of.rb +4 -0
- data/lib/store_model/types/one_polymorphic.rb +20 -9
- data/lib/store_model/types/raw_json.rb +20 -0
- data/lib/store_model/types.rb +7 -0
- data/lib/store_model/version.rb +1 -1
- data/lib/store_model.rb +56 -0
- metadata +14 -25
data/lib/store_model/railtie.rb
CHANGED
|
@@ -7,8 +7,10 @@ module StoreModel # :nodoc:
|
|
|
7
7
|
class Railtie < Rails::Railtie # :nodoc:
|
|
8
8
|
config.to_prepare do |_app|
|
|
9
9
|
ActiveSupport.on_load(:active_record) do
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if StoreModel.config.enable_parent_assignment
|
|
11
|
+
ActiveModel::Attributes.prepend(Attributes)
|
|
12
|
+
prepend(Base)
|
|
13
|
+
end
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module StoreModel
|
|
6
|
+
module Types
|
|
7
|
+
# Base type for StoreModel::Model
|
|
8
|
+
class Base < ActiveModel::Type::Value
|
|
9
|
+
attr_reader :model_klass
|
|
10
|
+
|
|
11
|
+
# Returns type
|
|
12
|
+
#
|
|
13
|
+
# @return [Symbol]
|
|
14
|
+
def type
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
def raise_cast_error(_value)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -11,8 +11,10 @@ module StoreModel
|
|
|
11
11
|
# @param mapping [Hash] mapping for enum values
|
|
12
12
|
#
|
|
13
13
|
# @return [StoreModel::Types::EnumType]
|
|
14
|
-
def initialize(mapping)
|
|
14
|
+
def initialize(mapping, raise_on_invalid_values)
|
|
15
15
|
@mapping = mapping
|
|
16
|
+
@raise_on_invalid_values = raise_on_invalid_values
|
|
17
|
+
super()
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
# Returns type
|
|
@@ -31,8 +33,8 @@ module StoreModel
|
|
|
31
33
|
return if value.blank?
|
|
32
34
|
|
|
33
35
|
case value
|
|
34
|
-
when String, Symbol then cast_symbol_value(value
|
|
35
|
-
when Integer then cast_integer_value(value)
|
|
36
|
+
when String, Symbol then cast_symbol_value(value)
|
|
37
|
+
when Integer, Float then cast_integer_value(value)
|
|
36
38
|
else
|
|
37
39
|
raise StoreModel::Types::CastError,
|
|
38
40
|
"failed casting #{value.inspect}, only String, Symbol or " \
|
|
@@ -43,12 +45,12 @@ module StoreModel
|
|
|
43
45
|
private
|
|
44
46
|
|
|
45
47
|
def cast_symbol_value(value)
|
|
46
|
-
raise_invalid_value!(value)
|
|
47
|
-
@mapping[value.to_sym]
|
|
48
|
+
raise_invalid_value!(value) if @raise_on_invalid_values && !@mapping.key?(value.to_sym)
|
|
49
|
+
@mapping[value.to_sym] || value
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def cast_integer_value(value)
|
|
51
|
-
raise_invalid_value!(value)
|
|
53
|
+
raise_invalid_value!(value) if @raise_on_invalid_values && !@mapping.value?(value)
|
|
52
54
|
value
|
|
53
55
|
end
|
|
54
56
|
|
|
@@ -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
|
|
@@ -4,18 +4,8 @@ require "active_model"
|
|
|
4
4
|
|
|
5
5
|
module StoreModel
|
|
6
6
|
module Types
|
|
7
|
-
# Implements
|
|
8
|
-
|
|
9
|
-
class ManyBase < ActiveModel::Type::Value
|
|
10
|
-
attr_reader :model_klass
|
|
11
|
-
|
|
12
|
-
# Returns type
|
|
13
|
-
#
|
|
14
|
-
# @return [Symbol]
|
|
15
|
-
def type
|
|
16
|
-
raise NotImplementedError
|
|
17
|
-
end
|
|
18
|
-
|
|
7
|
+
# Implements type for handling an array of StoreModel::Model
|
|
8
|
+
class ManyBase < Base
|
|
19
9
|
# Casts +value+ from DB or user to StoreModel::Model instance
|
|
20
10
|
#
|
|
21
11
|
# @param value [Object] a value to cast
|
|
@@ -40,7 +30,11 @@ module StoreModel
|
|
|
40
30
|
def serialize(value)
|
|
41
31
|
case value
|
|
42
32
|
when Array
|
|
43
|
-
ActiveSupport::JSON.encode(value
|
|
33
|
+
return ActiveSupport::JSON.encode(value) if value.empty? || value.any? { |v| !v.is_a?(StoreModel::Model) }
|
|
34
|
+
|
|
35
|
+
ActiveSupport::JSON.encode(value,
|
|
36
|
+
serialize_unknown_attributes: value.first.serialize_unknown_attributes?,
|
|
37
|
+
serialize_enums_using_as_json: value.first.serialize_enums_using_as_json?)
|
|
44
38
|
else
|
|
45
39
|
super
|
|
46
40
|
end
|
|
@@ -66,10 +60,6 @@ module StoreModel
|
|
|
66
60
|
raise NotImplementedError
|
|
67
61
|
end
|
|
68
62
|
|
|
69
|
-
def raise_cast_error(_value)
|
|
70
|
-
raise NotImplementedError
|
|
71
|
-
end
|
|
72
|
-
|
|
73
63
|
private
|
|
74
64
|
|
|
75
65
|
# rubocop:disable Style/RescueModifier
|
|
@@ -13,6 +13,7 @@ module StoreModel
|
|
|
13
13
|
# @return [StoreModel::Types::One]
|
|
14
14
|
def initialize(model_klass)
|
|
15
15
|
@model_klass = model_klass
|
|
16
|
+
super()
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Returns type
|
|
@@ -27,12 +28,17 @@ module StoreModel
|
|
|
27
28
|
# @param value [Object] a value to cast
|
|
28
29
|
#
|
|
29
30
|
# @return StoreModel::Model
|
|
30
|
-
def cast_value(value)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
def cast_value(value) # rubocop:disable Metrics/MethodLength
|
|
32
|
+
return nil if value.nil?
|
|
33
|
+
|
|
34
|
+
if value.is_a?(String)
|
|
35
|
+
decode_and_initialize(value)
|
|
36
|
+
elsif value.is_a?(@model_klass)
|
|
37
|
+
value
|
|
38
|
+
elsif value.respond_to?(:to_h) # Hash itself included
|
|
39
|
+
model_instance(value.to_h)
|
|
40
|
+
else
|
|
41
|
+
raise_cast_error(value)
|
|
36
42
|
end
|
|
37
43
|
rescue ActiveModel::UnknownAttributeError => e
|
|
38
44
|
handle_unknown_attribute(value, e)
|
|
@@ -46,13 +52,38 @@ module StoreModel
|
|
|
46
52
|
# @return [String] serialized value
|
|
47
53
|
def serialize(value)
|
|
48
54
|
case value
|
|
49
|
-
when
|
|
50
|
-
ActiveSupport::JSON.encode(value,
|
|
55
|
+
when @model_klass
|
|
56
|
+
ActiveSupport::JSON.encode(value,
|
|
57
|
+
serialize_unknown_attributes: value.serialize_unknown_attributes?,
|
|
58
|
+
serialize_enums_using_as_json: value.serialize_enums_using_as_json?)
|
|
59
|
+
when ::Hash
|
|
60
|
+
ActiveSupport::JSON.encode(value)
|
|
51
61
|
else
|
|
52
62
|
super
|
|
53
63
|
end
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
# Converts a value from database input to the appropriate ruby type.
|
|
67
|
+
#
|
|
68
|
+
# @param value [String] value to deserialize
|
|
69
|
+
#
|
|
70
|
+
# @return [Object] deserialized value
|
|
71
|
+
|
|
72
|
+
# rubocop:disable Style/RescueModifier
|
|
73
|
+
def deserialize(value)
|
|
74
|
+
case value
|
|
75
|
+
when String
|
|
76
|
+
payload = ActiveSupport::JSON.decode(value) rescue {}
|
|
77
|
+
model_instance(deserialize_by_types(payload))
|
|
78
|
+
when ::Hash
|
|
79
|
+
model_instance(deserialize_by_types(value))
|
|
80
|
+
when nil
|
|
81
|
+
nil
|
|
82
|
+
else raise_cast_error(value)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
# rubocop:enable Style/RescueModifier
|
|
86
|
+
|
|
56
87
|
private
|
|
57
88
|
|
|
58
89
|
def raise_cast_error(value)
|
|
@@ -63,6 +94,14 @@ module StoreModel
|
|
|
63
94
|
|
|
64
95
|
def model_instance(value)
|
|
65
96
|
@model_klass.new(value)
|
|
97
|
+
rescue ActiveModel::UnknownAttributeError => e
|
|
98
|
+
handle_unknown_attribute(value, e)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def deserialize_by_types(hash)
|
|
102
|
+
@model_klass.attribute_types.each.with_object(hash.dup) do |(key, type), value|
|
|
103
|
+
value[key] = type.deserialize(hash[key]) if hash.key?(key)
|
|
104
|
+
end
|
|
66
105
|
end
|
|
67
106
|
end
|
|
68
107
|
end
|
|
@@ -4,17 +4,8 @@ require "active_model"
|
|
|
4
4
|
|
|
5
5
|
module StoreModel
|
|
6
6
|
module Types
|
|
7
|
-
# Implements
|
|
8
|
-
class OneBase <
|
|
9
|
-
attr_reader :model_klass
|
|
10
|
-
|
|
11
|
-
# Returns type
|
|
12
|
-
#
|
|
13
|
-
# @return [Symbol]
|
|
14
|
-
def type
|
|
15
|
-
raise NotImplementedError
|
|
16
|
-
end
|
|
17
|
-
|
|
7
|
+
# Implements type for handling an instance of StoreModel::Model
|
|
8
|
+
class OneBase < Base
|
|
18
9
|
# Casts +value+ from DB or user to StoreModel::Model instance
|
|
19
10
|
#
|
|
20
11
|
# @param value [Object] a value to cast
|
|
@@ -36,10 +27,6 @@ module StoreModel
|
|
|
36
27
|
|
|
37
28
|
protected
|
|
38
29
|
|
|
39
|
-
def raise_cast_error(_value)
|
|
40
|
-
raise NotImplementedError
|
|
41
|
-
end
|
|
42
|
-
|
|
43
30
|
def model_instance(_value)
|
|
44
31
|
raise NotImplementedError
|
|
45
32
|
end
|
|
@@ -15,6 +15,7 @@ module StoreModel
|
|
|
15
15
|
# @return [StoreModel::Types::OnePolymorphic ]
|
|
16
16
|
def initialize(model_wrapper)
|
|
17
17
|
@model_wrapper = model_wrapper
|
|
18
|
+
super()
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
# Returns type
|
|
@@ -29,15 +30,17 @@ module StoreModel
|
|
|
29
30
|
# @param value [Object] a value to cast
|
|
30
31
|
#
|
|
31
32
|
# @return StoreModel::Model
|
|
32
|
-
def cast_value(value)
|
|
33
|
-
|
|
34
|
-
when String then decode_and_initialize(value)
|
|
35
|
-
when Hash then extract_model_klass(value).new(value)
|
|
36
|
-
when nil then value
|
|
37
|
-
else
|
|
38
|
-
raise_cast_error(value) unless value.class.ancestors.include?(StoreModel::Model)
|
|
33
|
+
def cast_value(value) # rubocop:disable Metrics/MethodLength
|
|
34
|
+
return nil if value.nil?
|
|
39
35
|
|
|
36
|
+
if value.is_a?(String)
|
|
37
|
+
decode_and_initialize(value)
|
|
38
|
+
elsif value.class.ancestors.include?(StoreModel::Model)
|
|
40
39
|
value
|
|
40
|
+
elsif value.respond_to?(:to_h) # Hash itself included
|
|
41
|
+
extract_model_klass(value).to_type.cast_value(value.to_h)
|
|
42
|
+
else
|
|
43
|
+
raise_cast_error(value)
|
|
41
44
|
end
|
|
42
45
|
rescue ActiveModel::UnknownAttributeError => e
|
|
43
46
|
handle_unknown_attribute(value, e)
|
|
@@ -50,9 +53,17 @@ module StoreModel
|
|
|
50
53
|
#
|
|
51
54
|
# @return [String] serialized value
|
|
52
55
|
def serialize(value)
|
|
53
|
-
return super unless value.is_a?(Hash) || implements_model?(value.class)
|
|
56
|
+
return super unless value.is_a?(::Hash) || implements_model?(value.class)
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
if value.is_a?(StoreModel::Model)
|
|
59
|
+
ActiveSupport::JSON.encode(
|
|
60
|
+
value,
|
|
61
|
+
serialize_unknown_attributes: value.serialize_unknown_attributes?,
|
|
62
|
+
serialize_enums_using_as_json: value.serialize_enums_using_as_json?
|
|
63
|
+
)
|
|
64
|
+
else
|
|
65
|
+
ActiveSupport::JSON.encode(value)
|
|
66
|
+
end
|
|
56
67
|
end
|
|
57
68
|
|
|
58
69
|
protected
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StoreModel
|
|
4
|
+
module Types
|
|
5
|
+
# Implements #encode_json and #as_json methods.
|
|
6
|
+
# By wrapping serialized objects in this type, it prevents duplicate
|
|
7
|
+
# JSON serialization passes on nested object. It is named as Encoder
|
|
8
|
+
# as it will not work to inflate typed attributes and is intended
|
|
9
|
+
# to be used internally.
|
|
10
|
+
class RawJSONEncoder < String
|
|
11
|
+
def encode_json(_encoder)
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def as_json(_options = nil)
|
|
16
|
+
JSON.parse(self)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/store_model/types.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "store_model/types/polymorphic_helper"
|
|
4
|
+
require "store_model/types/base"
|
|
4
5
|
|
|
5
6
|
require "store_model/types/one_base"
|
|
6
7
|
require "store_model/types/one"
|
|
@@ -10,10 +11,16 @@ require "store_model/types/many_base"
|
|
|
10
11
|
require "store_model/types/many"
|
|
11
12
|
require "store_model/types/many_polymorphic"
|
|
12
13
|
|
|
14
|
+
require "store_model/types/hash_base"
|
|
15
|
+
require "store_model/types/hash"
|
|
16
|
+
require "store_model/types/hash_polymorphic"
|
|
17
|
+
|
|
13
18
|
require "store_model/types/enum_type"
|
|
14
19
|
|
|
15
20
|
require "store_model/types/one_of"
|
|
16
21
|
|
|
22
|
+
require "store_model/types/raw_json"
|
|
23
|
+
|
|
17
24
|
module StoreModel
|
|
18
25
|
# Contains all custom types.
|
|
19
26
|
module Types
|
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
|