serialize_attributes 0.4.0 → 0.6.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: 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: