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 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: