serialize_attributes 0.3.1 → 0.5.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: 4c06deef436a52e15560111b4055b706d769d684106edaa4d224a7121367cfde
4
- data.tar.gz: e8bf6db9353e825f8df2cae7f79634c39826c69793a91a8dd3dba60a71c6039d
3
+ metadata.gz: f4fa19f41c2a597b312db2432d1df9a2efad508c91feebd885caab60f68304ac
4
+ data.tar.gz: a84b9c79d3bc59c8a416d8c2c3f2f08f46bdf3a25d8de465c8fb61593ce181aa
5
5
  SHA512:
6
- metadata.gz: ff687f3298a043cfac0a8aa00931a2175640ff538c7753be1397d15a11995b234c778e2f6454ae5c4f64dd55fe6c3774beefbff349b4c4025f3020406fddaa51
7
- data.tar.gz: 0471ec6fb74122457517c3dceb602000114257b3cf98d5f11cac75506da0a7e0ab74554545c26caf34f16edc70f6b0f21ae8e5f81a7ffc6324e5009b7e871cb0
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) # :nodoc:
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.serialize_attributes_store(:settings).attribute_names
22
- # => [:user_name, :subscribed]
22
+ # Model.serialized_attributes_store(:settings).attribute_names
23
+ # => [:user_name, :subscribed, :subscriptions]
23
24
  #
24
- # Model.serialize_attributes_store(:settings).attribute_names(:string)
25
- # => [:user_name]
26
- def attribute_names(type = nil)
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
- type = ActiveModel::Type.lookup(type)&.class if type.is_a?(Symbol)
29
- @attributes.select do |_, v|
30
- v.is_a?(type)
31
- end
39
+ attributes_for_type(attributes, type)
32
40
  else
33
- @attributes
41
+ attributes
34
42
  end.keys
35
43
  end
36
44
 
37
- # Get a list of attributes of a certain type
45
+ # Get a list of enumerated options for the column `name` in this store.
38
46
  #
39
- # Model.serialize_attributes_store(:settings).attributes_of_type(:string)
40
- # => [:user_name]
41
- def attributes_of_type(type)
42
- type = ActiveModel::Type.lookup(type) if type.is_a?(Symbol)
43
- @attributes.select { |_, v| v.is_a?(type) }.keys
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[name.to_sym].cast(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)
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].deserialize(value)
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 attribute(name, type, **options)
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
- arr = options.delete(:array) { false }
79
- type = ActiveModel::Type.lookup(type, **options.except(:default)) if type.is_a?(Symbol)
80
- type = ArrayWrapper.new(type) if arr
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 options.key?(:default)
85
- @defaults[name] = options[:default]
86
- elsif arr
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
- store = public_send(:#{@column_name}) # store = public_send(:settings)
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 #{arr} && cast_value == ArrayWrapper::EMPTY # if false && cast_value == ArrayWrapper::EMPTY
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
- self.public_send(:#{@column_name}=, store) # self.public_send(:settings=, store)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SerializeAttributes
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -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.3.1
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-01-25 00:00:00.000000000 Z
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: