serialize_attributes 0.3.1 → 0.5.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 +49 -0
- data/lib/serialize_attributes/store.rb +78 -30
- data/lib/serialize_attributes/types/enum.rb +53 -0
- data/lib/serialize_attributes/version.rb +1 -1
- data/lib/serialize_attributes.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4fa19f41c2a597b312db2432d1df9a2efad508c91feebd885caab60f68304ac
|
4
|
+
data.tar.gz: a84b9c79d3bc59c8a416d8c2c3f2f08f46bdf3a25d8de465c8fb61593ce181aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5f6f45154a0b8f28b8712f81714c4d4eccccb9a58b7a54a0d2fe24499c101656d6416d80cf34eac0632de6444b8fe0b57503cbec6a5a2f8ffdea21badd7428c
|
7
|
+
data.tar.gz: 336dfe149b07fb43a171d417a79d33f1fc67eec5d6399cb73a803f38f0fc2afa72cafd1da859c489ad1c92551291337599dfb55827678f46c6717845288fc273
|
data/README.md
CHANGED
@@ -7,6 +7,7 @@ class MyModel
|
|
7
7
|
serialize_attributes :settings do
|
8
8
|
attribute :user_name, :string
|
9
9
|
attribute :subscribed, :boolean, default: false
|
10
|
+
attribute :subscriptions, :string, array: true
|
10
11
|
end
|
11
12
|
end
|
12
13
|
```
|
@@ -144,6 +145,54 @@ specify a `default` attribute yourself explicitly:
|
|
144
145
|
attribute :emails, :string, array: true, default: ["unknown@example.com"]
|
145
146
|
```
|
146
147
|
|
148
|
+
### Enumerated ("enum") types
|
149
|
+
|
150
|
+
Since enum types are a common thing when managing external data, there is a special enum
|
151
|
+
type defined by the library:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class MyModel
|
155
|
+
serialize_attributes :settings do
|
156
|
+
attribute :state, :enum, of: ["open", "closed"]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
Unlike `ActiveRecord::Enum`, enums here work by attaching an inclusion validator to your
|
162
|
+
model. So for example, with the above code, I'll get a validation failure by default:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
MyModel.new(state: nil).tap(&:valid?).errors
|
166
|
+
#=> { state: "is not included in the list" }
|
167
|
+
```
|
168
|
+
|
169
|
+
If you wish to allow nil values in your enum, you should add it to the `of` collection:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
attribute :state, :enum, of: [nil, "open", "closed"]
|
173
|
+
```
|
174
|
+
|
175
|
+
The column is probably now the source of truth for correct values, so you can also
|
176
|
+
introspect the store to fetch these from elsewhere (e.g. for building documentation):
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
MyModel.serialized_attributes_store(:settings).enum_options(:state)
|
180
|
+
#=> ["open", "closed"]
|
181
|
+
```
|
182
|
+
|
183
|
+
Finally, you can also use complex types within the enum itself, by passing an additional
|
184
|
+
`type:` attribute. Values will then be cast or deserialized per that type, and the result
|
185
|
+
of the casting is what is validated, e.g:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
attribute :state, :enum, of: [nil, true, false], type: :boolean
|
189
|
+
```
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
MyModel.new(state: "f").state
|
193
|
+
#=> false
|
194
|
+
```
|
195
|
+
|
147
196
|
### Usage with ActiveModel alone
|
148
197
|
|
149
198
|
It's also possible to use this library without `ActiveRecord`:
|
@@ -4,7 +4,8 @@ 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)
|
7
|
+
def initialize(model_class, column_name, &block)
|
8
|
+
# :nodoc:
|
8
9
|
@model_class = model_class
|
9
10
|
@column_name = column_name
|
10
11
|
@attributes = {}
|
@@ -18,29 +19,38 @@ module SerializeAttributes
|
|
18
19
|
# Get a list of the attributes managed by this store. Pass an optional `type` argument
|
19
20
|
# to filter attributes by their type.
|
20
21
|
#
|
21
|
-
# Model.
|
22
|
-
# => [:user_name, :subscribed]
|
22
|
+
# Model.serialized_attributes_store(:settings).attribute_names
|
23
|
+
# => [:user_name, :subscribed, :subscriptions]
|
23
24
|
#
|
24
|
-
# Model.
|
25
|
-
# => [:user_name]
|
26
|
-
|
25
|
+
# Model.serialized_attributes_store(:settings).attribute_names(type: :string)
|
26
|
+
# => [:user_name, :subscriptions]
|
27
|
+
#
|
28
|
+
# Model.serialized_attributes_store(:settings).attribute_names(type: :string, array: true)
|
29
|
+
# => [:subscriptions]
|
30
|
+
#
|
31
|
+
# Model.serialized_attributes_store(:settings).attribute_names(type: :string, array: false)
|
32
|
+
# => [:user_name]
|
33
|
+
#
|
34
|
+
#
|
35
|
+
def attribute_names(type: nil, array: nil)
|
36
|
+
attributes = @attributes
|
37
|
+
attributes = @attributes.select { |_, v| v.is_a?(ArrayWrapper) == array } unless array.nil?
|
27
38
|
if type
|
28
|
-
|
29
|
-
@attributes.select do |_, v|
|
30
|
-
v.is_a?(type)
|
31
|
-
end
|
39
|
+
attributes_for_type(attributes, type)
|
32
40
|
else
|
33
|
-
|
41
|
+
attributes
|
34
42
|
end.keys
|
35
43
|
end
|
36
44
|
|
37
|
-
# Get a list of
|
45
|
+
# Get a list of enumerated options for the column `name` in this store.
|
38
46
|
#
|
39
|
-
# Model.
|
40
|
-
# => [
|
41
|
-
def
|
42
|
-
type =
|
43
|
-
|
47
|
+
# Model.serialized_attributes_store(:settings).enum_options(:enumy)
|
48
|
+
# => [nil, "placed", "confirmed"]
|
49
|
+
def enum_options(name)
|
50
|
+
type = @attributes.fetch(name.to_sym)
|
51
|
+
raise ArgumentError, "`#{name}` attribute is not an enum type" unless type.respond_to?(:options)
|
52
|
+
|
53
|
+
type.options
|
44
54
|
end
|
45
55
|
|
46
56
|
# Cast a stored attribute against a given name
|
@@ -48,7 +58,10 @@ module SerializeAttributes
|
|
48
58
|
# Model.serialized_attributes_store(:settings).cast(:user_name, 42)
|
49
59
|
# => "42"
|
50
60
|
def cast(name, value)
|
51
|
-
@attributes
|
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)
|
52
65
|
end
|
53
66
|
|
54
67
|
# Deserialize a stored attribute using the value from the database (or elsewhere)
|
@@ -56,7 +69,12 @@ module SerializeAttributes
|
|
56
69
|
# Model.serialized_attributes_store(:settings).deserialize(:subscribed, "0")
|
57
70
|
# => false
|
58
71
|
def deserialize(name, value)
|
59
|
-
@attributes[name.to_sym]
|
72
|
+
attribute = @attributes[name.to_sym]
|
73
|
+
if attribute.nil?
|
74
|
+
raise "The attribute #{name} is not define in serialize_attribute method in the #{@model_class} class."
|
75
|
+
end
|
76
|
+
|
77
|
+
attribute.deserialize(value)
|
60
78
|
end
|
61
79
|
|
62
80
|
# Retrieve the default value for a given block. If the default is a Proc, it can be
|
@@ -73,23 +91,48 @@ module SerializeAttributes
|
|
73
91
|
|
74
92
|
private
|
75
93
|
|
76
|
-
def
|
94
|
+
def attributes_for_type(attributes, type)
|
95
|
+
type = ActiveModel::Type.lookup(type)&.class if type.is_a?(Symbol)
|
96
|
+
attributes.select do |_, v|
|
97
|
+
v = v.__getobj__ if v.is_a?(ArrayWrapper)
|
98
|
+
v.is_a?(type)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
NO_DEFAULT = Object.new
|
103
|
+
|
104
|
+
def attribute(name, type, default: NO_DEFAULT, array: false, **type_options)
|
77
105
|
name = name.to_sym
|
78
|
-
|
79
|
-
|
80
|
-
|
106
|
+
type = ActiveModel::Type.lookup(type, **type_options) if type.is_a?(Symbol)
|
107
|
+
|
108
|
+
if array
|
109
|
+
raise ArgumentError, "Enum-arrays not currently supported" if type.is_a?(Types::Enum)
|
110
|
+
|
111
|
+
type = ArrayWrapper.new(type)
|
112
|
+
end
|
81
113
|
|
82
114
|
@attributes[name] = type
|
83
115
|
|
84
|
-
if
|
85
|
-
@defaults[name] =
|
86
|
-
elsif
|
116
|
+
if default != NO_DEFAULT
|
117
|
+
@defaults[name] = default
|
118
|
+
elsif array
|
87
119
|
@defaults[name] = []
|
88
120
|
end
|
89
121
|
|
122
|
+
type.attach_validations_to(@model_class, name) if type.respond_to?(:attach_validations_to)
|
123
|
+
|
90
124
|
@model_class.module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
91
125
|
def #{name} # def user_name
|
92
|
-
|
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
|
+
#
|
93
136
|
if store.key?("#{name}") # if store.key?("user_name")
|
94
137
|
store["#{name}"] # store["user_name"]
|
95
138
|
else # else
|
@@ -98,20 +141,25 @@ module SerializeAttributes
|
|
98
141
|
.default(:#{name}, self) # .default(:user_name, self)
|
99
142
|
end # end
|
100
143
|
end # end
|
101
|
-
|
144
|
+
#
|
102
145
|
def #{name}=(value) # def user_name=(value)
|
103
146
|
cast_value = self.class # cast_value = self.class
|
104
147
|
.serialized_attributes_store(:#{@column_name}) # .serialized_attributes_store(:settings)
|
105
148
|
.cast(:#{name}, value) # .cast(:user_name, value)
|
106
149
|
store = public_send(:#{@column_name}) # store = public_send(:settings)
|
107
150
|
#
|
108
|
-
if #{
|
151
|
+
if #{array} && cast_value == ArrayWrapper::EMPTY # if array && cast_value == ArrayWrapper::EMPTY
|
109
152
|
store.delete("#{name}") # store.delete("user_name")
|
110
153
|
else # else
|
111
154
|
store.merge!("#{name}" => cast_value) # store.merge!("user_name" => cast_value)
|
112
155
|
end # end
|
156
|
+
public_send(:#{@column_name}=, store) # public_send(:settings=, store)
|
113
157
|
#
|
114
|
-
|
158
|
+
values_before_typecast = store.values # values_before_typecast = store.values
|
159
|
+
values_after_typecast = # values_after_typecast =
|
160
|
+
public_send(:#{@column_name}).values # public_send(:settings).values
|
161
|
+
@_bad_typcasting = # @_bad_typcasting =
|
162
|
+
values_before_typecast != values_after_typecast # values_before_typecast != values_after_typecast
|
115
163
|
end # end
|
116
164
|
RUBY
|
117
165
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
|
+
|
5
|
+
module SerializeAttributes
|
6
|
+
module Types
|
7
|
+
# A custom type which can only hold one of a set of predetermined values.
|
8
|
+
class Enum
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
# Construct a type instance.
|
12
|
+
#
|
13
|
+
# @param of [Array] One or more possible values that this type can take
|
14
|
+
# @param type [Symbol] An optional ActiveModel::Type instance, or symbol, for
|
15
|
+
# casting/uncasting (only required if enum has non-primitive types)
|
16
|
+
#
|
17
|
+
# @example Required to be one of two values
|
18
|
+
# attribute :state, :enum, of: ["placed", "confirmed"]
|
19
|
+
#
|
20
|
+
# @example Optionally allowing nil
|
21
|
+
# attribute :folding, :enum, of: [nil, "top-fold", "bottom-fold"]
|
22
|
+
#
|
23
|
+
# @example Casting input/output using another type
|
24
|
+
# attribute :loves_pizza, :enum, of: [true], type: :boolean
|
25
|
+
# # object.loves_pizza = "t"
|
26
|
+
# #=> true
|
27
|
+
def initialize(of: [], type: nil)
|
28
|
+
@options = of.freeze
|
29
|
+
@type = resolve_type(type)
|
30
|
+
end
|
31
|
+
|
32
|
+
def attach_validations_to(object, field_name)
|
33
|
+
object.validates_inclusion_of(field_name, in: @options)
|
34
|
+
end
|
35
|
+
|
36
|
+
delegate_missing_to :@type
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
UNTYPED_TYPE = ActiveModel::Type::Value.new
|
41
|
+
|
42
|
+
def resolve_type(given)
|
43
|
+
case given
|
44
|
+
in Symbol then ActiveModel::Type.lookup(given)
|
45
|
+
in nil then UNTYPED_TYPE
|
46
|
+
in _ then given
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ActiveModel::Type.register(:enum, SerializeAttributes::Types::Enum)
|
data/lib/serialize_attributes.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "serialize_attributes/version"
|
4
4
|
require "serialize_attributes/store"
|
5
|
+
require "serialize_attributes/types/enum"
|
5
6
|
|
6
7
|
# Serialize ActiveModel attributes in JSON using type casting
|
7
8
|
module SerializeAttributes
|
@@ -37,7 +38,7 @@ module SerializeAttributes
|
|
37
38
|
# Person.serialized_attribute_names(:settings, :string)
|
38
39
|
# => [:user_name]
|
39
40
|
def serialized_attribute_names(column_name, type = nil)
|
40
|
-
serialized_attributes_store(column_name).attribute_names(type)
|
41
|
+
serialized_attributes_store(column_name).attribute_names(type: type)
|
41
42
|
end
|
42
43
|
end
|
43
44
|
|
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.
|
4
|
+
version: 0.5.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-
|
11
|
+
date: 2022-07-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- Rakefile
|
37
37
|
- lib/serialize_attributes.rb
|
38
38
|
- lib/serialize_attributes/store.rb
|
39
|
+
- lib/serialize_attributes/types/enum.rb
|
39
40
|
- lib/serialize_attributes/version.rb
|
40
41
|
homepage: https://github.com/zaikio/serialize_attributes
|
41
42
|
licenses:
|