serialize_attributes 0.6.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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