serialize_attributes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 866a54a31babd7bd446a0348107c7262b0ea49b71769ca573a75d37107429b85
4
+ data.tar.gz: 484d600fbc6fb88e9796ee067957e9b337a20f8c3180fad81f2f0b0792620514
5
+ SHA512:
6
+ metadata.gz: b90b797aeb295de0516ab9b991c49fefb430b0943fee75e0e44710fa5ba85688d4c7220d64dd93d3e75566bca4d5b4dc8dd2b2c62ca2130c8fe4cd7c741be5d8
7
+ data.tar.gz: c0fcee019031311f0a597505669b5a42e50d2b935997ef1ba901e9f474ccde426b28f970e35d3b4ab053fd563b3944536c455c23a97bb606c200ad2f281254c2
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Zaikio
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # serialize_attributes
2
+
3
+ Serialize ActiveModel attributes in JSON using type casting:
4
+
5
+ ```ruby
6
+ class MyModel
7
+ serialize_attributes :settings do
8
+ attribute :user_name, :string
9
+ attribute :subscribed, :boolean, default: false
10
+ end
11
+ end
12
+ ```
13
+
14
+ > Unlike similar projects like [`ActiveRecord::TypedStore`](https://github.com/byroot/activerecord-typedstore),
15
+ > underneath this library doesn't use the `store` interface and instead uses the native
16
+ > type coercion provided by ActiveModel (as long as you have an attribute recognised as a
17
+ > Hash-like object).
18
+
19
+ ## Quickstart
20
+
21
+ Add `serialized_atributes` to your Gemfile:
22
+
23
+ ```bash
24
+ $ bundle add serialized_atributes
25
+ ```
26
+
27
+ Next, include `SerializeAttributes` in your model class (or `ApplicationRecord` if you want to make
28
+ it available everywhere). Your model should have a JSON (or JSONB) attribute, for example
29
+ this one is called `settings`:
30
+
31
+ ```ruby
32
+ create_table :my_models do |t|
33
+ t.json :settings, null: false, default: {}
34
+ end
35
+ ```
36
+
37
+ Then, tell the model what attributes we'll be storing there:
38
+
39
+ ```ruby
40
+ class MyModel < ActiveRecord::Base
41
+ include SerializeAttributes
42
+
43
+ serialize_attributes :settings do
44
+ attribute :user_name, :string
45
+ attribute :subscribed, :boolean, default: false
46
+ end
47
+ end
48
+ ```
49
+
50
+ Now you can read/write values on the model and they will be automatically cast to and from
51
+ database values:
52
+
53
+ ```ruby
54
+ record = MyModel.create!(user_name: "Nick")
55
+ #=> #<MyModel id: 1, settings: { user_name: "Nick" }>
56
+
57
+ record.subscribed
58
+ #=> false
59
+
60
+ record.subscribed = true
61
+ record
62
+ #=> #<MyModel id: 1, settings: { user_name: "Nick", subscribed: true }>
63
+ ```
64
+
65
+ ### Getting all of the stored attributes
66
+
67
+ Default values are not automatically persisted to the database, so there is a helper
68
+ method to get the full object including default values:
69
+
70
+ ```ruby
71
+ record = MyModel.new(user_name: "Nick")
72
+ record.serialized_attributes_on(:settings)
73
+ #=> { user_name: "Nick", subscribed: false }
74
+ ```
75
+
76
+ ### Getting a list of attribute names
77
+
78
+ If you wish to programmatically get the list of attributes known to a store, you can use
79
+ `.serialized_attribute_names`. The list is returned in order of definition:
80
+
81
+ ```ruby
82
+ MyModel.serialized_attribute_names(:settings)
83
+ #=> [:user_name, :subscribed]
84
+ ```
85
+
86
+ ### Complex types
87
+
88
+ Underneath, we use the `ActiveModel::Type` mechanism for type coercion, which means
89
+ that more complex and custom types are also supported. For an example, take a look at
90
+ [ActiveModel::Type::Value](https://api.rubyonrails.org/classes/ActiveModel/Type/Value.html).
91
+
92
+ The `#attribute` method
93
+ [has the same interface as ActiveRecord](https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute),
94
+ and supports both symbols and objects for the `cast_type`:
95
+
96
+ ```ruby
97
+ # An example from the Rails docs:
98
+ class MoneyType < ActiveRecord::Type::Integer
99
+ def cast(value)
100
+ if !value.kind_of?(Numeric) && value.include?('$')
101
+ price_in_dollars = value.gsub(/\$/, '').to_f
102
+ super(price_in_dollars * 100)
103
+ else
104
+ super
105
+ end
106
+ end
107
+ end
108
+
109
+ ActiveRecord::Type.register(:money, MoneyType)
110
+ ```
111
+
112
+ ```ruby
113
+ class MyModel
114
+ serialize_attributes :settings do
115
+ attribute :price_in_cents, :money
116
+ end
117
+ end
118
+ ```
119
+
120
+ ### Usage with ActiveModel alone
121
+
122
+ It's also possible to use this library without `ActiveRecord`:
123
+
124
+ ```ruby
125
+ class MyModel
126
+ include ActiveModel::Model
127
+ include ActiveModel::Attributes
128
+ include SerializeAttributes
129
+
130
+ # ActiveModel doesn't include a native Hash type, we can just use the Value
131
+ # type here for demo purposes:
132
+ attribute :settings, ActiveModel::Type::Value
133
+
134
+ serialize_attributes :settings do
135
+ attribute :user_name, :string
136
+ end
137
+ end
138
+ ```
139
+
140
+ ## License
141
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+
5
+ require "bundler/gem_tasks"
6
+
7
+ require "rake/testtask"
8
+
9
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
10
+ load "rails/tasks/engine.rake"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << "test"
14
+ t.pattern = "test/**/*_test.rb"
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SerializeAttributes
4
+ # SerializeAttributes::Store is the individual store, keyed by name. You can get a
5
+ # reference to the store by calling `Model.serialized_attributes_store(column_name)`.
6
+ class Store
7
+ def initialize(model_class, column_name, &block) # :nodoc:
8
+ @model_class = model_class
9
+ @column_name = column_name
10
+ @attributes = {}
11
+ @defaults = {}
12
+
13
+ instance_exec(&block)
14
+ wrap_store_column
15
+ [self, @attributes, @defaults].each(&:freeze)
16
+ end
17
+
18
+ # Get a list of the attributes managed by this store
19
+ def attribute_names = @attributes.keys
20
+
21
+ # Cast a stored attribute against a given name
22
+ #
23
+ # Model.serialized_attributes_store(:settings).cast(:user_name, 42)
24
+ # => "42"
25
+ def cast(name, value)
26
+ @attributes[name.to_sym].cast(value)
27
+ end
28
+
29
+ # Deserialize a stored attribute using the value from the database (or elsewhere)
30
+ #
31
+ # Model.serialized_attributes_store(:settings).deserialize(:subscribed, "0")
32
+ # => false
33
+ def deserialize(name, value)
34
+ @attributes[name.to_sym].deserialize(value)
35
+ end
36
+
37
+ # Retrieve the default value for a given block. If the default is a Proc, it can be
38
+ # optionally executed in the context of the model.
39
+ #
40
+ # Model.serialized_attributes_store(:settings).default(:subscribed)
41
+ # #=> false
42
+ def default(name, context = nil)
43
+ given = @defaults[name]
44
+ return (context || self).instance_exec(&given) if given.is_a?(Proc)
45
+
46
+ given
47
+ end
48
+
49
+ private
50
+
51
+ def attribute(name, type, **options)
52
+ name = name.to_sym
53
+ type = ActiveModel::Type.lookup(type, **options.except(:default)) if type.is_a?(Symbol)
54
+
55
+ @attributes[name] = type
56
+ @defaults[name] = options[:default] if options.key?(:default)
57
+
58
+ @model_class.module_eval <<~RUBY, __FILE__, __LINE__ + 1
59
+ def #{name} # def user_name
60
+ store = public_send(:#{@column_name}) # store = public_send(:settings)
61
+ if store.key?("#{name}") # if store.key?("user_name")
62
+ store["#{name}"] # store["user_name"]
63
+ else # else
64
+ self.class # self.class
65
+ .serialized_attributes_store(:#{@column_name}) # .serialized_attributes_store(:settings)
66
+ .default(:#{name}, self) # .default(:user_name, self)
67
+ end # end
68
+ end # end
69
+
70
+ def #{name}=(value) # def user_name=(value)
71
+ cast_value = self.class # cast_value = self.class
72
+ .serialized_attributes_store(:#{@column_name}) # .serialized_attributes_store(:settings)
73
+ .cast(:#{name}, value) # .cast(:user_name, value)
74
+ store = public_send(:#{@column_name}) # store = public_send(:settings)
75
+ self.public_send( # self.public_send(
76
+ :#{@column_name}=, # :settings=,
77
+ store.merge("#{name}" => cast_value) # store.merge("user_name" => cast_value)
78
+ ) # )
79
+ end # end
80
+ RUBY
81
+ end
82
+
83
+ class StoreColumnWrapper < SimpleDelegator # :nodoc:
84
+ def initialize(original, store) # rubocop:disable Lint/MissingSuper
85
+ __setobj__(original)
86
+ @store = store
87
+ end
88
+
89
+ def deserialize(...)
90
+ result = __getobj__.deserialize(...)
91
+ return result unless @store && result.respond_to?(:each)
92
+
93
+ result.each_with_object({}) do |(attribute_name, serialized_value), out|
94
+ out[attribute_name] = @store.deserialize(attribute_name, serialized_value)
95
+ end
96
+ end
97
+ end
98
+
99
+ # This method wraps the original store column and catches the `deserialize` call -
100
+ # this gives us a chance to convert the data in the database back into our types.
101
+ def wrap_store_column
102
+ return unless @model_class.respond_to?(:attribute_types)
103
+
104
+ original_store_column_type = @model_class.attribute_types.fetch(@column_name.to_s)
105
+ @model_class.attribute(@column_name, StoreColumnWrapper.new(
106
+ original_store_column_type,
107
+ self
108
+ ))
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SerializeAttributes
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "serialize_attributes/version"
4
+ require "serialize_attributes/store"
5
+
6
+ # Serialize ActiveModel attributes in JSON using type casting
7
+ module SerializeAttributes
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Configure a SerializeAttributes::Store, using the given column to store each
12
+ # attribute.
13
+ #
14
+ # class Person
15
+ # serialize_attributes :settings do
16
+ # attribute :user_name, :string, default: "Christian"
17
+ # end
18
+ # end
19
+ #
20
+ # Person.new(user_name: "Nick")
21
+ def serialize_attributes(column_name, &block)
22
+ column_name = column_name.to_sym
23
+
24
+ @serialized_attribute_stores ||= {}
25
+ @serialized_attribute_stores[column_name] = Store.new(self, column_name, &block)
26
+ end
27
+
28
+ # Retrieve a SerializeAttributes registered against the given column
29
+ #
30
+ # Person.serialized_attributes_store(:settings)
31
+ def serialized_attributes_store(column_name)
32
+ @serialized_attribute_stores.fetch(column_name.to_sym)
33
+ end
34
+
35
+ # Get a list of the attributes registered in a given store
36
+ #
37
+ # Person.serialized_attribute_names(:settings)
38
+ def serialized_attribute_names(column_name)
39
+ serialized_attributes_store(column_name).attribute_names
40
+ end
41
+ end
42
+
43
+ # Retrieve all of the SerializeAttributes attributes, including their default values
44
+ #
45
+ # person = Person.new
46
+ # person.serialized_attributes_on(:settings)
47
+ # #=> { "user_name" => "Christian" }
48
+ def serialized_attributes_on(column_name)
49
+ store = self.class.serialized_attributes_store(column_name)
50
+
51
+ store.attribute_names.index_with do |attribute_name|
52
+ public_send(attribute_name)
53
+ end
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: serialize_attributes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Zaikio
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
27
+ description:
28
+ email:
29
+ - support@zaikio.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/serialize_attributes.rb
38
+ - lib/serialize_attributes/store.rb
39
+ - lib/serialize_attributes/version.rb
40
+ homepage: https://github.com/zaikio/serialize_attributes
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/zaikio/serialize_attributes
45
+ source_code_uri: https://github.com/zaikio/serialize_attributes
46
+ changelog_uri: https://github.com/zaikio/serialize_attributes/blob/main/CHANGELOG.md
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.2.32
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Serialize ActiveModel attributes in JSON using type casting
66
+ test_files: []