serialize_attributes 0.1.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 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: []