serialize_attributes 0.6.0 → 1.0.0

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: 9264e3f62465ae9a4a27bf3afa14fa9cd516d14eab7b93e316c56d6cfb88ab4b
4
+ data.tar.gz: 69bf1f4d1ccf209aabee4bb0ee5f128cc6ff93c3bb59dbbe295b39517c2cd5d7
5
5
  SHA512:
6
- metadata.gz: 867430cdb67e77c32ef2860fced5b004e80d3ac15300da02010addeff5d8100c0d20c84b0f8a80458041ad93896ccd0b86c8250dc3aad198e874eac9842f2bef
7
- data.tar.gz: 5cebc02f04aa76e0d1dca6ecc9b63f645e595710d902d08a207eaf9d13fe11c6c5b6e51f9e52c3990340ada60d24bc1bd6bfdd189dd3137ce0652e8f88850964
6
+ metadata.gz: 045a9d1d63e862b7eadb8372d68c002ae2c1c6cf32b00157c5c97d9970012f3c82c5d944a4cc55a632a693c7241875e9355953586d23d262f9355f61f1afc3bc
7
+ data.tar.gz: d9992e49647e63d28b44d5c29e2c74936e7ee1fc305243c92bed744d579142c05c0475c26ce976cbce079f4ba29a67a48cd2cbd43729872daac337ecc45aefd7
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,44 @@ 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
+ end
154
+
155
+ class AttributeSet < ::ActiveModel::AttributeSet # :nodoc:
156
+ def ==(other)
157
+ attributes == if other.is_a?(Hash)
158
+ other
159
+ else
160
+ other.attributes
161
+ end
187
162
  end
188
163
  end
189
164
 
@@ -193,29 +168,54 @@ module SerializeAttributes
193
168
  @store = store
194
169
  end
195
170
 
171
+ def cast(value)
172
+ case value
173
+ when Hash then deserialize(value.stringify_keys)
174
+ else value
175
+ end
176
+ end
177
+
178
+ def changed_in_place?(raw_original_value, new_value)
179
+ AttributeSet.new(raw_original_value) != new_value
180
+ end
181
+
182
+ def serialize(value)
183
+ super(value.values_for_database)
184
+ end
185
+
196
186
  def deserialize(...)
197
- result = __getobj__.deserialize(...)
187
+ result = super
198
188
  return result unless @store && result.respond_to?(:each)
199
189
 
200
- result.each_with_object({}) do |(attribute_name, serialized_value), out|
201
- out[attribute_name] = @store.deserialize(attribute_name, serialized_value)
202
- end
190
+ AttributeSet.new(
191
+ @store.attribute_types.each_with_object({}) do |(attribute, type), out|
192
+ out[attribute] = if result.key?(attribute)
193
+ @store.deserialize(attribute, result[attribute])
194
+ else
195
+ ActiveModel::Attribute.from_user(attribute, @store.default(attribute), type)
196
+ end
197
+ end
198
+ )
203
199
  end
204
200
  end
205
201
 
206
- # This method wraps the original store column and catches the `deserialize` call -
202
+ # This method wraps the original store column and catches several read/write calls;
207
203
  # this gives us a chance to convert the data in the database back into our types.
208
204
  #
209
- # We're using the block form of `.attribute` to avoid loading the database schema just
210
- # to figure out our wrapping type.
205
+ # We using the block form of `.attribute` when the schema is lazily loaded (and has
206
+ # not been loaded yet).
211
207
  def wrap_store_column
212
- return unless @model_class.respond_to?(:attribute_types)
213
-
214
- store = self
208
+ if respond_to?(:schema_loaded?) && !schema_loaded?
209
+ store = self
210
+ @model_class.attribute(@column_name) do
211
+ original_type = @model_class.attribute_types.fetch(@column_name.to_s)
212
+ StoreColumnWrapper.new(original_type, store)
213
+ end
215
214
 
216
- @model_class.attribute(@column_name) do
215
+ else
217
216
  original_type = @model_class.attribute_types.fetch(@column_name.to_s)
218
- StoreColumnWrapper.new(original_type, store)
217
+ type = StoreColumnWrapper.new(original_type, self)
218
+ @model_class.attribute(@column_name, type)
219
219
  end
220
220
  end
221
221
  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.0"
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.0
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-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel