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.
@@ -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
- ActiveModel::Attributes.prepend(Attributes)
11
- prepend(Base)
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
@@ -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,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.to_sym)
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) unless @mapping.key?(value.to_sym)
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) unless @mapping.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
@@ -14,6 +14,7 @@ module StoreModel
14
14
  # @return [StoreModel::Types::Many]
15
15
  def initialize(model_klass)
16
16
  @model_klass = model_klass
17
+ super()
17
18
  end
18
19
 
19
20
  # Returns type
@@ -4,18 +4,8 @@ require "active_model"
4
4
 
5
5
  module StoreModel
6
6
  module Types
7
- # Implements ActiveModel::Type::Value type for handling an array of
8
- # StoreModel::Model
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, serialize_unknown_attributes: true)
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
@@ -16,6 +16,7 @@ module StoreModel
16
16
  # @return [StoreModel::Types::PolymorphicArrayType ]
17
17
  def initialize(model_wrapper)
18
18
  @model_wrapper = model_wrapper
19
+ super()
19
20
  end
20
21
 
21
22
  # Returns type
@@ -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
- case value
32
- when String then decode_and_initialize(value)
33
- when Hash then model_instance(value)
34
- when @model_klass, nil then value
35
- else raise_cast_error(value)
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 Hash, @model_klass
50
- ActiveSupport::JSON.encode(value, serialize_unknown_attributes: true)
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 ActiveModel::Type::Value type for handling an instance of StoreModel::Model
8
- class OneBase < 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
-
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
@@ -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
@@ -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
- case value
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
- ActiveSupport::JSON.encode(value, serialize_unknown_attributes: true)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StoreModel # :nodoc:
4
- VERSION = "1.6.1"
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