serialize_attributes 0.6.0 → 1.0.1

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: 60ef1282c29e5ec4ae46242eed3dac95e756ddd5e6c57c95befc19a695bb9a6e
4
- data.tar.gz: 9aa223ae50c2c60344844109bb4772a225796c7d70a0e7b355014bf765b402d9
3
+ metadata.gz: fcfd75fcff7e883cb0063752bb35de287b4683c59e0029bf654d77b0e67b7ec0
4
+ data.tar.gz: adc7cc26ba5b6dca8b543a2a33960419c66cd37149a5e759bdfc820f131fbe60
5
5
  SHA512:
6
- metadata.gz: 867430cdb67e77c32ef2860fced5b004e80d3ac15300da02010addeff5d8100c0d20c84b0f8a80458041ad93896ccd0b86c8250dc3aad198e874eac9842f2bef
7
- data.tar.gz: 5cebc02f04aa76e0d1dca6ecc9b63f645e595710d902d08a207eaf9d13fe11c6c5b6e51f9e52c3990340ada60d24bc1bd6bfdd189dd3137ce0652e8f88850964
6
+ metadata.gz: f160a8efe020f2d47d385f3c2ea76149adcf2d6eff06dd6d9b49e08e0684e7fa1cbcd4abb7e6c8f8ea3d25cebed1c888a2aa7bbb42d4a7fff5f0c5ec30d08d67
7
+ data.tar.gz: a58b02f33d14bd4afa8202a7d69553377ecf2fc2b9b4c011fbc5fdf2ff8460628dc8675d8ad883bfedb98e810779597a3afbe4f01347a9014baa5189d0223afb
data/README.md CHANGED
@@ -148,12 +148,7 @@ class MyModel
148
148
  end
149
149
  ```
150
150
 
151
- Please note that the default value for an array attribute is always `[]`, unless you
152
- specify a `default` attribute yourself explicitly:
153
-
154
- ```ruby
155
- attribute :emails, :string, array: true, default: ["unknown@example.com"]
156
- ```
151
+ Please note that the default value for an array attribute is always `[]`.
157
152
 
158
153
  ### Enumerated ("enum") types
159
154
 
@@ -4,18 +4,19 @@ module SerializeAttributes
4
4
  # SerializeAttributes::Store is the individual store, keyed by name. You can get a
5
5
  # reference to the store by calling `Model.serialized_attributes_store(column_name)`.
6
6
  class Store
7
- def initialize(model_class, column_name, &block)
8
- # :nodoc:
7
+ def initialize(model_class, column_name, &block) # :nodoc:
9
8
  @model_class = model_class
10
9
  @column_name = column_name
11
- @attributes = {}
10
+ @attribute_types = {}
12
11
  @defaults = {}
13
12
 
14
13
  instance_exec(&block)
15
14
  wrap_store_column
16
- [self, @attributes, @defaults].each(&:freeze)
15
+ [self, @attribute_types, @defaults].each(&:freeze)
17
16
  end
18
17
 
18
+ attr_reader :attribute_types
19
+
19
20
  # Get a list of the attributes managed by this store. Pass an optional `type` argument
20
21
  # to filter attributes by their type.
21
22
  #
@@ -33,13 +34,13 @@ module SerializeAttributes
33
34
  #
34
35
  #
35
36
  def attribute_names(type: nil, array: nil)
36
- attributes = @attributes
37
- attributes = @attributes.select { |_, v| v.is_a?(ArrayWrapper) == array } unless array.nil?
37
+ attributes = @attribute_types
38
+ attributes = @attribute_types.select { |_, v| v.is_a?(ArrayWrapper) == array } unless array.nil?
38
39
  if type
39
40
  attributes_for_type(attributes, type)
40
41
  else
41
42
  attributes
42
- end.keys
43
+ end.keys.map(&:to_sym)
43
44
  end
44
45
 
45
46
  # Get a list of enumerated options for the column `name` in this store.
@@ -47,46 +48,44 @@ module SerializeAttributes
47
48
  # Model.serialized_attributes_store(:settings).enum_options(:enumy)
48
49
  # => [nil, "placed", "confirmed"]
49
50
  def enum_options(name)
50
- type = @attributes.fetch(name.to_sym)
51
+ type = @attribute_types.fetch(name.to_s)
51
52
  raise ArgumentError, "`#{name}` attribute is not an enum type" unless type.respond_to?(:options)
52
53
 
53
54
  type.options
54
55
  end
55
56
 
56
- # Cast a stored attribute against a given name
57
+ # Cast a stored attribute against a given name into an
58
+ # ActiveModel::Attribute::FromUser object (the cast value can be got using `#value`).
57
59
  #
58
- # Model.serialized_attributes_store(:settings).cast(:user_name, 42)
60
+ # Model.serialized_attributes_store(:settings).cast(:user_name, 42).value
59
61
  # => "42"
60
62
  def cast(name, value)
61
- # @attributes.fetch(name.to_sym) returns the Type as defined in ActiveModel::Type or
62
- # raise an error if the type is unknown.
63
- # Type::Integer.new.cast("42") => 42
64
- @attributes.fetch(name.to_sym).cast(value)
63
+ type = @attributes_types.fetch(name.to_s)
64
+
65
+ ActiveModel::Attribute.from_user(name, value, type)
65
66
  end
66
67
 
67
- # Deserialize a stored attribute using the value from the database (or elsewhere)
68
+ # Deserialize a stored attribute using the value from the database (or elsewhere) into
69
+ # an ActiveModel::Attribute::FromDatabase object (the cast value can be got using
70
+ # `#value`).
68
71
  #
69
- # Model.serialized_attributes_store(:settings).deserialize(:subscribed, "0")
72
+ # Model.serialized_attributes_store(:settings).deserialize(:subscribed, "0").value
70
73
  # => false
71
74
  def deserialize(name, value)
72
- attribute = @attributes[name.to_sym]
73
- if attribute.nil?
75
+ type = @attribute_types[name.to_s]
76
+ if type.nil?
74
77
  raise "The attribute #{name} is not defined in serialize_attribute method in the #{@model_class} class."
75
78
  end
76
79
 
77
- attribute.deserialize(value)
80
+ ActiveModel::Attribute.from_database(name, value, type)
78
81
  end
79
82
 
80
- # Retrieve the default value for a given block. If the default is a Proc, it can be
81
- # optionally executed in the context of the model.
83
+ # Retrieve the default value for a given block.
82
84
  #
83
85
  # Model.serialized_attributes_store(:settings).default(:subscribed)
84
86
  # #=> false
85
- def default(name, context = nil)
86
- given = @defaults[name]
87
- return (context || self).instance_exec(&given) if given.is_a?(Proc)
88
-
89
- given
87
+ def default(name)
88
+ @defaults[name.to_s]
90
89
  end
91
90
 
92
91
  private
@@ -102,7 +101,7 @@ module SerializeAttributes
102
101
  NO_DEFAULT = Object.new
103
102
 
104
103
  def attribute(name, type, default: NO_DEFAULT, array: false, **type_options)
105
- name = name.to_sym
104
+ name = name.to_s
106
105
  type = ActiveModel::Type.lookup(type, **type_options) if type.is_a?(Symbol)
107
106
 
108
107
  if array
@@ -111,7 +110,7 @@ module SerializeAttributes
111
110
  type = ArrayWrapper.new(type)
112
111
  end
113
112
 
114
- @attributes[name] = type
113
+ @attribute_types[name] = type
115
114
 
116
115
  if default != NO_DEFAULT
117
116
  @defaults[name] = default
@@ -122,68 +121,56 @@ module SerializeAttributes
122
121
  type.attach_validations_to(@model_class, name) if type.respond_to?(:attach_validations_to)
123
122
 
124
123
  @model_class.module_eval <<~RUBY, __FILE__, __LINE__ + 1
125
- def #{name} # def user_name
126
- if @_bad_typcasting # if @_bad_typcasting
127
- store = # store =
128
- read_attribute_before_type_cast( # read_attribute_before_type_cast(
129
- :#{@column_name} # :settings
130
- ) # )
131
- @_bad_typcasting = false # @_bad_typcasting = false
132
- else # else
133
- store = public_send(:#{@column_name}) # store = public_send(:settings)
134
- end # end
135
- #
136
- if store.key?("#{name}") # if store.key?("user_name")
137
- store["#{name}"] # store["user_name"]
138
- else # else
139
- self.class # self.class
140
- .serialized_attributes_store(:#{@column_name}) # .serialized_attributes_store(:settings)
141
- .default(:#{name}, self) # .default(:user_name, self)
142
- end # end
143
- end # end
144
- #
145
- unless #{array} # unless array
146
- def #{name}? # def user_name?
147
- query_attribute("#{name}") # query_attribute(:user_name)
148
- end # end
149
- end # end
150
- #
151
- def #{name}=(value) # def user_name=(value)
152
- cast_value = self.class # cast_value = self.class
153
- .serialized_attributes_store(:#{@column_name}) # .serialized_attributes_store(:settings)
154
- .cast(:#{name}, value) # .cast(:user_name, value)
155
- store = public_send(:#{@column_name}) # store = public_send(:settings)
156
- #
157
- if #{array} && cast_value == ArrayWrapper::EMPTY # if array && cast_value == ArrayWrapper::EMPTY
158
- store.delete("#{name}") # store.delete("user_name")
159
- else # else
160
- store.merge!("#{name}" => cast_value) # store.merge!("user_name" => cast_value)
161
- end # end
162
- public_send(:#{@column_name}=, store) # public_send(:settings=, store)
163
- #
164
- values_before_typecast = store.values # values_before_typecast = store.values
165
- values_after_typecast = # values_after_typecast =
166
- public_send(:#{@column_name}).values # public_send(:settings).values
167
- @_bad_typcasting = # @_bad_typcasting =
168
- values_before_typecast != values_after_typecast # values_before_typecast != values_after_typecast
169
- end # end
124
+ def #{name} # def user_name
125
+ store = public_send(:#{@column_name}) # store = public_send(:settings)
126
+ store.fetch_value("#{name}") # store.fetch_value("user_name")
127
+ end # end
128
+
129
+ unless #{array} # unless false
130
+ def #{name}? # def user_name?
131
+ query_attribute("#{name}") # query_attribute("user_name")
132
+ end # end
133
+ end # end
134
+
135
+ def #{name}=(value) # def user_name=(value)
136
+ store = public_send(:#{@column_name}) # store = public_send(:settings)
137
+ try(:#{@column_name}_will_change!) # try(:settings_will_change!)
138
+ store.write_from_user("#{name}", value) # store.write_from_user("user_name", value)
139
+ end # end
170
140
  RUBY
171
141
  end
172
142
 
173
143
  class ArrayWrapper < SimpleDelegator # :nodoc:
174
- EMPTY = Object.new
175
-
176
144
  def cast(value)
177
- # We don't want to store the null value, because array types _always_ have a default
178
- # configured. So we return this special object here, and check it again before
179
- # updating the underlying store.
180
- return EMPTY unless value
145
+ return [] if value.nil?
181
146
 
182
147
  Array(value)
183
148
  end
184
149
 
185
150
  def deserialize(value)
186
- value.map { __getobj__.deserialize(_1) }
151
+ Array.wrap(value).map { __getobj__.deserialize(_1) }
152
+ end
153
+
154
+ # For arrays of strings (the most common array type), the underlying Type::String in
155
+ # Rails won't do this check if the raw value isn't a String (and returns `nil`):
156
+ #
157
+ # def changed_in_place?(a, b)
158
+ # if a.is_a?(String)
159
+ # ...
160
+ #
161
+ # This means we have to override this check ourselves here.
162
+ def changed_in_place?(raw_old_value, new_value)
163
+ raw_old_value != new_value
164
+ end
165
+ end
166
+
167
+ class AttributeSet < ::ActiveModel::AttributeSet # :nodoc:
168
+ def ==(other)
169
+ attributes == if other.is_a?(Hash)
170
+ other
171
+ else
172
+ other.attributes
173
+ end
187
174
  end
188
175
  end
189
176
 
@@ -193,29 +180,54 @@ module SerializeAttributes
193
180
  @store = store
194
181
  end
195
182
 
183
+ def cast(value)
184
+ case value
185
+ when Hash then deserialize(value.stringify_keys)
186
+ else value
187
+ end
188
+ end
189
+
190
+ def changed_in_place?(raw_original_value, new_value)
191
+ (deserialize(raw_original_value) != new_value) || new_value.each_value.any?(&:changed_in_place?)
192
+ end
193
+
194
+ def serialize(value)
195
+ super(value.values_for_database)
196
+ end
197
+
196
198
  def deserialize(...)
197
- result = __getobj__.deserialize(...)
199
+ result = super
198
200
  return result unless @store && result.respond_to?(:each)
199
201
 
200
- result.each_with_object({}) do |(attribute_name, serialized_value), out|
201
- out[attribute_name] = @store.deserialize(attribute_name, serialized_value)
202
- end
202
+ AttributeSet.new(
203
+ @store.attribute_types.each_with_object({}) do |(attribute, type), out|
204
+ out[attribute] = if result.key?(attribute)
205
+ @store.deserialize(attribute, result[attribute])
206
+ else
207
+ ActiveModel::Attribute.from_user(attribute, @store.default(attribute), type)
208
+ end
209
+ end
210
+ )
203
211
  end
204
212
  end
205
213
 
206
- # This method wraps the original store column and catches the `deserialize` call -
214
+ # This method wraps the original store column and catches several read/write calls;
207
215
  # this gives us a chance to convert the data in the database back into our types.
208
216
  #
209
- # We're using the block form of `.attribute` to avoid loading the database schema just
210
- # to figure out our wrapping type.
217
+ # We using the block form of `.attribute` when the schema is lazily loaded (and has
218
+ # not been loaded yet).
211
219
  def wrap_store_column
212
- return unless @model_class.respond_to?(:attribute_types)
213
-
214
- store = self
220
+ if respond_to?(:schema_loaded?) && !schema_loaded?
221
+ store = self
222
+ @model_class.attribute(@column_name) do
223
+ original_type = @model_class.attribute_types.fetch(@column_name.to_s)
224
+ StoreColumnWrapper.new(original_type, store)
225
+ end
215
226
 
216
- @model_class.attribute(@column_name) do
227
+ else
217
228
  original_type = @model_class.attribute_types.fetch(@column_name.to_s)
218
- StoreColumnWrapper.new(original_type, store)
229
+ type = StoreColumnWrapper.new(original_type, self)
230
+ @model_class.attribute(@column_name, type)
219
231
  end
220
232
  end
221
233
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SerializeAttributes
4
- VERSION = "0.6.0"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serialize_attributes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zaikio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-26 00:00:00.000000000 Z
11
+ date: 2022-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel