serialize_attributes 0.4.0 → 0.6.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: c505eb52395e2c8fe3786c3ef97dd04e5a56f978072d839ba517c0069f9dce93
4
- data.tar.gz: 5ea660579c45ebd3cca56da5c3ea7e07fe9af5fb38a05fd62d3133f118f3d8df
3
+ metadata.gz: 60ef1282c29e5ec4ae46242eed3dac95e756ddd5e6c57c95befc19a695bb9a6e
4
+ data.tar.gz: 9aa223ae50c2c60344844109bb4772a225796c7d70a0e7b355014bf765b402d9
5
5
  SHA512:
6
- metadata.gz: 311846e88d0c92cf41bf5adbd32c1515fbd1d131df0d733cfd444963bedc8594620d05cb12cfe701b40fba851195931f6f53673dcbe25593bf257e2b28c183dc
7
- data.tar.gz: c5e2e60ba000e0afe423cee6c37e20fae5b590a39bcadab76d65712db712359e1d4ae3c0532e44565d673cb1c8f55cd4d535b9fdc057f50d11d3e3744f0803e9
6
+ metadata.gz: 867430cdb67e77c32ef2860fced5b004e80d3ac15300da02010addeff5d8100c0d20c84b0f8a80458041ad93896ccd0b86c8250dc3aad198e874eac9842f2bef
7
+ data.tar.gz: 5cebc02f04aa76e0d1dca6ecc9b63f645e595710d902d08a207eaf9d13fe11c6c5b6e51f9e52c3990340ada60d24bc1bd6bfdd189dd3137ce0652e8f88850964
data/README.md CHANGED
@@ -7,7 +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
+ attribute :subscriptions, :string, array: true
11
11
  end
12
12
  end
13
13
  ```
@@ -63,6 +63,16 @@ record
63
63
  #=> #<MyModel id: 1, settings: { user_name: "Nick", subscribed: true }>
64
64
  ```
65
65
 
66
+ Additionally you can use predicated methods the same way you do with an ActiveRecord's attribute.
67
+ Indeed behind the curtain we use `ActiveRecord::AttributeMethods::Query`.
68
+
69
+ ```ruby
70
+ record.subscribed?
71
+ #=> false
72
+ record.user_name?
73
+ #=> true
74
+ ```
75
+
66
76
  ### Getting all of the stored attributes
67
77
 
68
78
  Default values are not automatically persisted to the database, so there is a helper
@@ -145,6 +155,54 @@ specify a `default` attribute yourself explicitly:
145
155
  attribute :emails, :string, array: true, default: ["unknown@example.com"]
146
156
  ```
147
157
 
158
+ ### Enumerated ("enum") types
159
+
160
+ Since enum types are a common thing when managing external data, there is a special enum
161
+ type defined by the library:
162
+
163
+ ```ruby
164
+ class MyModel
165
+ serialize_attributes :settings do
166
+ attribute :state, :enum, of: ["open", "closed"]
167
+ end
168
+ end
169
+ ```
170
+
171
+ Unlike `ActiveRecord::Enum`, enums here work by attaching an inclusion validator to your
172
+ model. So for example, with the above code, I'll get a validation failure by default:
173
+
174
+ ```ruby
175
+ MyModel.new(state: nil).tap(&:valid?).errors
176
+ #=> { state: "is not included in the list" }
177
+ ```
178
+
179
+ If you wish to allow nil values in your enum, you should add it to the `of` collection:
180
+
181
+ ```ruby
182
+ attribute :state, :enum, of: [nil, "open", "closed"]
183
+ ```
184
+
185
+ The column is probably now the source of truth for correct values, so you can also
186
+ introspect the store to fetch these from elsewhere (e.g. for building documentation):
187
+
188
+ ```ruby
189
+ MyModel.serialized_attributes_store(:settings).enum_options(:state)
190
+ #=> ["open", "closed"]
191
+ ```
192
+
193
+ Finally, you can also use complex types within the enum itself, by passing an additional
194
+ `type:` attribute. Values will then be cast or deserialized per that type, and the result
195
+ of the casting is what is validated, e.g:
196
+
197
+ ```ruby
198
+ attribute :state, :enum, of: [nil, true, false], type: :boolean
199
+ ```
200
+
201
+ ```ruby
202
+ MyModel.new(state: "f").state
203
+ #=> false
204
+ ```
205
+
148
206
  ### Usage with ActiveModel alone
149
207
 
150
208
  It's also possible to use this library without `ActiveRecord`:
@@ -19,16 +19,16 @@ module SerializeAttributes
19
19
  # Get a list of the attributes managed by this store. Pass an optional `type` argument
20
20
  # to filter attributes by their type.
21
21
  #
22
- # Model.serialize_attributes_store(:settings).attribute_names
22
+ # Model.serialized_attributes_store(:settings).attribute_names
23
23
  # => [:user_name, :subscribed, :subscriptions]
24
24
  #
25
- # Model.serialize_attributes_store(:settings).attribute_names(type: :string)
25
+ # Model.serialized_attributes_store(:settings).attribute_names(type: :string)
26
26
  # => [:user_name, :subscriptions]
27
27
  #
28
- # Model.serialize_attributes_store(:settings).attribute_names(type: :string, array: true)
28
+ # Model.serialized_attributes_store(:settings).attribute_names(type: :string, array: true)
29
29
  # => [:subscriptions]
30
30
  #
31
- # Model.serialize_attributes_store(:settings).attribute_names(type: :string, array: false)
31
+ # Model.serialized_attributes_store(:settings).attribute_names(type: :string, array: false)
32
32
  # => [:user_name]
33
33
  #
34
34
  #
@@ -42,12 +42,26 @@ module SerializeAttributes
42
42
  end.keys
43
43
  end
44
44
 
45
+ # Get a list of enumerated options for the column `name` in this store.
46
+ #
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
54
+ end
55
+
45
56
  # Cast a stored attribute against a given name
46
57
  #
47
58
  # Model.serialized_attributes_store(:settings).cast(:user_name, 42)
48
59
  # => "42"
49
60
  def cast(name, value)
50
- @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)
51
65
  end
52
66
 
53
67
  # Deserialize a stored attribute using the value from the database (or elsewhere)
@@ -55,7 +69,12 @@ module SerializeAttributes
55
69
  # Model.serialized_attributes_store(:settings).deserialize(:subscribed, "0")
56
70
  # => false
57
71
  def deserialize(name, value)
58
- @attributes[name.to_sym].deserialize(value)
72
+ attribute = @attributes[name.to_sym]
73
+ if attribute.nil?
74
+ raise "The attribute #{name} is not defined in serialize_attribute method in the #{@model_class} class."
75
+ end
76
+
77
+ attribute.deserialize(value)
59
78
  end
60
79
 
61
80
  # Retrieve the default value for a given block. If the default is a Proc, it can be
@@ -80,23 +99,40 @@ module SerializeAttributes
80
99
  end
81
100
  end
82
101
 
83
- def attribute(name, type, **options)
102
+ NO_DEFAULT = Object.new
103
+
104
+ def attribute(name, type, default: NO_DEFAULT, array: false, **type_options)
84
105
  name = name.to_sym
85
- arr = options.delete(:array) { false }
86
- type = ActiveModel::Type.lookup(type, **options.except(:default)) if type.is_a?(Symbol)
87
- 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
88
113
 
89
114
  @attributes[name] = type
90
115
 
91
- if options.key?(:default)
92
- @defaults[name] = options[:default]
93
- elsif arr
116
+ if default != NO_DEFAULT
117
+ @defaults[name] = default
118
+ elsif array
94
119
  @defaults[name] = []
95
120
  end
96
121
 
122
+ type.attach_validations_to(@model_class, name) if type.respond_to?(:attach_validations_to)
123
+
97
124
  @model_class.module_eval <<~RUBY, __FILE__, __LINE__ + 1
98
125
  def #{name} # def user_name
99
- 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
+ #
100
136
  if store.key?("#{name}") # if store.key?("user_name")
101
137
  store["#{name}"] # store["user_name"]
102
138
  else # else
@@ -105,20 +141,31 @@ module SerializeAttributes
105
141
  .default(:#{name}, self) # .default(:user_name, self)
106
142
  end # end
107
143
  end # end
108
-
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
+ #
109
151
  def #{name}=(value) # def user_name=(value)
110
152
  cast_value = self.class # cast_value = self.class
111
153
  .serialized_attributes_store(:#{@column_name}) # .serialized_attributes_store(:settings)
112
154
  .cast(:#{name}, value) # .cast(:user_name, value)
113
155
  store = public_send(:#{@column_name}) # store = public_send(:settings)
114
156
  #
115
- if #{arr} && cast_value == ArrayWrapper::EMPTY # if false && cast_value == ArrayWrapper::EMPTY
157
+ if #{array} && cast_value == ArrayWrapper::EMPTY # if array && cast_value == ArrayWrapper::EMPTY
116
158
  store.delete("#{name}") # store.delete("user_name")
117
159
  else # else
118
160
  store.merge!("#{name}" => cast_value) # store.merge!("user_name" => cast_value)
119
161
  end # end
162
+ public_send(:#{@column_name}=, store) # public_send(:settings=, store)
120
163
  #
121
- self.public_send(:#{@column_name}=, store) # self.public_send(:settings=, store)
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
122
169
  end # end
123
170
  RUBY
124
171
  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.4.0"
4
+ VERSION = "0.6.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
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.0
4
+ version: 0.6.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-03-07 00:00:00.000000000 Z
11
+ date: 2022-07-26 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: