cached_serializer 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: 164738a352a3d55ad7ad7efe859c9b4f5840d54f0e21d1ebe5ecbfccbdf16cb8
4
+ data.tar.gz: b0b6bdbe968c74de5cd8ad85d772190fa465f5b5c971706c0eb00d409e90ef26
5
+ SHA512:
6
+ metadata.gz: c86836b5aa5f676a4f39564f95565c4c3570fcc8063d7f85d0c22e28f78cf5f4acc3bfcd3f0c0594a38a5de5d78fbc774ce3caccb90130185c04f3b2173e5844
7
+ data.tar.gz: 275c53b05c3e82d78cdd4aa716b61c6c0465e480c96c22a3e1ca4b99875d2cb7705a6e78951973af07af5ebf463e55132ec7f7a1f53fcb6bc614fb3b503f15ad
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3
+
4
+ # Declare your gem's dependencies in cached_serializer.gemspec.
5
+ # Bundler will treat runtime dependencies like base dependencies, and
6
+ # development dependencies will be added by default to the :development group.
7
+ gemspec
8
+
9
+ # Declare any dependencies that are still in development here instead of in
10
+ # your gemspec. These might include edge Rails or gems from your path or
11
+ # Git. Remember to move these dependencies to your gemspec before releasing
12
+ # your gem to rubygems.org.
13
+
14
+ # To use a debugger
15
+ # gem 'byebug', group: [:development, :test]
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Keegan Leitz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Keegan Leitz
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,121 @@
1
+ # CachedSerializer
2
+
3
+ A serializer for Rails models that prevents unnecessary lookups.
4
+
5
+ ## Usage
6
+
7
+ The following example serializes a `User` model. By using certain macros, the
8
+ author can specify what values should be cached or recomputed when serializing
9
+ a model record. Serialized data is cached via `Rails.cache` (by the User's `id`
10
+ and specified attribute keys) so the serializer doesn't have to hit the DB for
11
+ every attribute. This can be desirable when some of the serialized data involves
12
+ long-running queries, relationship-heavy calculations, etc.
13
+
14
+ ```rb
15
+ class UserSerializer < CachedSerializer::Base
16
+ # Properties specified by `::columns` are called as-is on the model, and
17
+ # invalidated automatically when the model is saved with new values for these
18
+ # properties, by checking Rails' built-in `#email_changed?`/`#phone_changed?`
19
+ # /etc. dynamic methods on save. Allows you to serialize most simple straight-
20
+ # from-the-DB values without any extra effort.
21
+ columns :email, :phone
22
+
23
+ # Properties specified by `::constant` are are attributes that should never
24
+ # need to be recomputed. They will be cached ad infinitum (until the cache key
25
+ # is cleared, manually or otherwise).
26
+ #
27
+ # This will call `some_user.id` for the `:id` attribute, and
28
+ # `some_user.created_at` for the `:created_at` attribute. Consider the values
29
+ # for each to be cached FOREVER, and never recomputed.
30
+ constant :id, :created_at
31
+
32
+ # Alternatively, you can pass a block to `::constant`. This will use the
33
+ # result of the block as the value for the `:name` attribute. It will cache
34
+ # the value FOREVER, and never recompute it.
35
+ constant :name do |user|
36
+ "#{user.first_name} #{user.last_name}"
37
+ end
38
+
39
+ # Properties specified by `::volatile` are attributes that should always be
40
+ # recomputed every time a user is serialized, and will never be cached.
41
+ #
42
+ # This will call `some_user.token` for the `:token` attribute, and
43
+ # `some_user.updated_at` for the `:updated_at` attribute. It will ALWAYS
44
+ # recompute the values, EVERY time it serializes a user.
45
+ volatile :token, :updated_at
46
+
47
+ # Alternatively, you can pass a block to `::volatile`. This will use the
48
+ # result of the block as the value for the `:time_on_platform` attribute. It
49
+ # will ALWAYS recompute the value, EVERY time it serializes a user.
50
+ volatile :time_on_platform do |user|
51
+ Time.zone.now.to_i - user.created_at.to_i
52
+ end
53
+
54
+ # Properties specified by `::computed` are attributes that should either be
55
+ # recomputed by the given block or drawn from the cache based on rules that
56
+ # you specify.
57
+ #
58
+ # You can specify columns that it depends on, such that when any of the
59
+ # columns change it will invalidate the cache for this serialized property. In
60
+ # this case, `:address` will only be recomputed when any of the supplied
61
+ # attributes (`:address_1`, `:address_2`, `:city`, etc.) change on the user
62
+ # record.
63
+ computed :address, columns: [:address_1, :address_2, :city, :state, :zip] do |user|
64
+ "#{user.address_1} #{user.address_2}, #{user.city}, #{user.state} #{user.zip}"
65
+ end
66
+
67
+ # You can also specify a `:recompute_if` proc/lambda. It will run
68
+ # `:recompute_if` every time a model is being recomputed, and if it returns
69
+ # `true` then it will recompute. Otherwise, it will use the cached result.
70
+ computed :active, recompute_if: ->(u) { u.last_logged_in_at > 1.week.ago } do |user|
71
+ user.purchases.where(created_at: 1.week.ago..Time.zone.now).present?
72
+ end
73
+
74
+ # Additionally, you can specify an `:expires_in` expiration duration. This
75
+ # will cache the result for one day after the last time the
76
+ # `:revenue_generated` attribute was recomputed.
77
+ computed :revenue_generated, expires_in: 1.week do |user|
78
+ user.offers.accepted.reduce(0) { |total, offer| total + offer.total_price }
79
+ end
80
+
81
+ # You can use any number of these cache conditions with `:compute`. For
82
+ # example, this will cache the result until `:foo` changes on the user, `:bar`
83
+ # changes on the user, `u.silly?` is `true` at the time of serialization, or
84
+ # it has been more than 10 seconds since the last time the `:silly` attribute
85
+ # was recomputed.
86
+ computed :silly, columns: [:foo, :bar], recompute_if: ->(u) { u.silly? }, expires_in: 10.seconds do |user|
87
+ rand(100)
88
+ end
89
+ end
90
+ ```
91
+
92
+ ## Installation
93
+
94
+ Add this line to your application's `Gemfile`:
95
+
96
+ ```ruby
97
+ gem 'cached_serializer'
98
+ ```
99
+
100
+ Then `cd` into your project's directory and run:
101
+
102
+ ```bash
103
+ bundle install
104
+ ```
105
+
106
+ ## Bug reports
107
+
108
+ If you encounter a bug, you can report it [here](https://github.com/kjleitz/cached_serializer/issues). Please include the following information, if possible:
109
+
110
+ - your Ruby version (`ruby --version`; if using multiple Rubies via `rvm` or similar, make sure you're in your project's directory when you run `ruby --version`)
111
+ - your Rails version (`cd` into your project's directory, then `bundle exec rails --version`)
112
+ - example code (which demonstrates the issue)
113
+ - your soul (for eating)
114
+
115
+ ## Contributing
116
+
117
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/kjleitz/cached_serializer](https://github.com/kjleitz/cached_serializer).
118
+
119
+ ## License
120
+
121
+ 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,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'CachedSerializer'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,5 @@
1
+ require "cached_serializer/railtie"
2
+ require "cached_serializer/base"
3
+
4
+ module CachedSerializer
5
+ end
@@ -0,0 +1,32 @@
1
+ module CachedSerializer
2
+ class AttrSerializer
3
+ attr_accessor :attr_name, :recompute_ifs, :expires_in, :recompute
4
+
5
+ class << self
6
+ def cache_key(subject, attr_name)
7
+ "cached_serializer:#{subject.class.model_name.name.underscore}:#{subject.id}:#{attr_name}"
8
+ end
9
+ end
10
+
11
+ def initialize(attr_name, recompute_ifs = nil, :expires_in = nil, &recompute)
12
+ self.attr_name = attr_name
13
+ self.recompute_ifs = [recompute_ifs].flatten.compact
14
+ self.expires_in = expires_in
15
+ self.recompute = recompute || proc { |subj| subj.send(attr_name.to_sym) }
16
+ end
17
+
18
+ def serialize_for(subject)
19
+ { attr_name.to_sym => serialized_value_for(subject) }
20
+ end
21
+
22
+ private
23
+
24
+ def serialized_value_for(subject)
25
+ should_recompute = recompute_ifs.any? { |recompute_if| recompute_if.call(subject) }
26
+ cache_key = self.class.cache_key(subject, attr_name)
27
+ Rails.cache.fetch(cache_key, expires_in: expires_in, force: should_recompute) do
28
+ attr_serializer.recompute.call(subject)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ module CachedSerializer
2
+ class AttrSerializerCollection
3
+ attr_accessor :collection
4
+
5
+ def initialize(collection = [])
6
+ self.collection = collection
7
+ end
8
+
9
+ def add(*attr_serializers)
10
+ attr_serializers.each do |attr_serializer|
11
+ existing = collection.find { |serializer| serializer.attr_name == attr_serializer.attr_name }
12
+ if existing
13
+ existing.recompute_ifs.concat(attr_serializer.recompute_ifs)
14
+ existing.recompute = attr_serializer.recompute # shadowed attrs override the blocks of previously-declared ones
15
+ existing.expires_in = attr_serializer.expires_in # shadowed attrs override the expires_in of previously-declared ones
16
+ else
17
+ existing << attr_serializer
18
+ end
19
+ end
20
+ end
21
+
22
+ alias_method :push, :add
23
+
24
+ def <<(item)
25
+ add(item)
26
+ end
27
+
28
+ def concat(items)
29
+ add(*items)
30
+ end
31
+
32
+ def method_missing(method, *args, &block)
33
+ collection.send(method, *args, &block)
34
+ end
35
+
36
+ def respond_to?(method, *args)
37
+ collection.respond_to?(method, *args)
38
+ end
39
+
40
+ def serialize_for(subject)
41
+ collection.reduce({}) do |memo, attr_serializer|
42
+ memo.merge(attr_serializer.serialize_for(subject))
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,160 @@
1
+ require "json"
2
+ require_relative "./attr_serializer"
3
+ require_relative "./attr_serializer_collection"
4
+
5
+ module CachedSerializer
6
+ class Base
7
+ attr_accessor :subject
8
+
9
+ @serializers = AttrSerializerCollection.new
10
+
11
+ class << self
12
+ def serializers
13
+ @serializers
14
+ end
15
+
16
+ # Example (in a UserSerializer):
17
+ #
18
+ # columns :email, :phone
19
+ #
20
+ # This will call `some_user.email` for the `:email` attribute, and
21
+ # `some_user.phone` for the `:phone` attribute. It will cache the value
22
+ # for each, until the attribute changes on the record.
23
+ #
24
+ def columns(*column_names)
25
+ column_names.each do |column_name|
26
+ serializers << AttrSerializer.new(column_name)
27
+ add_column_changed_cache_invalidator_callback(column_name, column_name)
28
+ end
29
+ end
30
+
31
+ # Example (in a UserSerializer):
32
+ #
33
+ # constant :email, :phone
34
+ #
35
+ # This will call `some_user.email` for the `:email` attribute, and
36
+ # `some_user.phone` for the `:phone` attribute. It will cache the value
37
+ # for each FOREVER, and never recompute it.
38
+ #
39
+ # constant :name do |user|
40
+ # "#{user.first_name} #{user.last_name}"
41
+ # end
42
+ #
43
+ # This will use the result of the block as the value for the `:name`
44
+ # attribute. It will cache the value FOREVER, and never recompute it.
45
+ #
46
+ def constant(*attr_names, &recompute)
47
+ attr_names.each do |attr_name|
48
+ serializers << AttrSerializer.new(attr_name, &recompute)
49
+ end
50
+ end
51
+
52
+ # Example (in a UserSerializer):
53
+ #
54
+ # volatile :email, :phone
55
+ #
56
+ # This will call `some_user.email` for the `:email` attribute, and
57
+ # `some_user.phone` for the `:phone` attribute. It will ALWAYS recompute
58
+ # the values, EVERY time it serializes a user.
59
+ #
60
+ # volatile :name do |user|
61
+ # "#{user.first_name} #{user.last_name}"
62
+ # end
63
+ #
64
+ # This will use the result of the block as the value for the `:name`
65
+ # attribute. It will ALWAYS recompute the value, EVERY time it serializes
66
+ # a user.
67
+ #
68
+ def volatile(*attr_names, &recompute)
69
+ attr_names.each do |attr_name|
70
+ always_recompute = proc { |_subj| true }
71
+ serializers << AttrSerializer.new(attr_name, always_recompute, &recompute)
72
+ end
73
+ end
74
+
75
+ # Example (in a UserSerializer):
76
+ #
77
+ # computed :name, columns: [:first_name, :last_name] do |user|
78
+ # "#{user.first_name} #{user.last_name}"
79
+ # end
80
+ #
81
+ # This will use the result of the block as the value for the `:name`
82
+ # attribute. It will cache the result until either `:first_name` or
83
+ # `:last_name` changes on the user record.
84
+ #
85
+ # computed :active, recompute_if: ->(u) { u.last_logged_in_at > 1.week.ago } do |user|
86
+ # user.purchases.where(created_at: 1.week.ago..Time.zone.now).present?
87
+ # end
88
+ #
89
+ # This will use the result of the block as the value for the `:active`
90
+ # attribute. It will cache the result until the `recompute_if` proc/lambda
91
+ # returns `true`.
92
+ #
93
+ # computed :purchase_count, expires_in: 1.day do |user|
94
+ # user.purchases.count
95
+ # end
96
+ #
97
+ # This will use the result of the block as the value for the
98
+ # `:purchase_count` attribute. It will cache the result for one day after
99
+ # the last time the `:purchase_count` attribute was recomputed.
100
+ #
101
+ # computed :silly, columns: [:foo, :bar], recompute_if: ->(u) { u.silly? }, expires_in: 10.seconds do |user|
102
+ # rand(100)
103
+ # end
104
+ #
105
+ # This will use the result of the block as the value for the `:silly`
106
+ # attribute. It will cache the result until `:foo` changes on the user,
107
+ # `:bar` changes on the user, `u.silly?` at the time of serialization,
108
+ # or it has been more than 10 seconds since the last time the `:silly`
109
+ # attribute was recomputed.
110
+ #
111
+ def computed(attr_name, columns: [], recompute_if: nil, expires_in: nil, &recompute)
112
+ if (columns.empty? && !recompute_if && !expires_in)
113
+ raise ArgumentError, "Must provide :columns, :recompute_if, or :expires_in to a computed attribute setter"
114
+ end
115
+ serializers << AttrSerializer.new(attr_name, recompute_if, expires_in, &recompute)
116
+ columns.each do |column_name|
117
+ add_column_changed_cache_invalidator_callback(attr_name, column_name)
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def subject_class
124
+ @subject_class ||= self.class.to_s.gsub(/[Ss]erializer\z/, '').constantize
125
+ end
126
+
127
+ def add_column_changed_cache_invalidator_callback(attr_name, dependent_attr_name)
128
+ @already_added_callback ||= {}
129
+ @already_added_callback[attr_name.to_sym] ||= {}
130
+ return if @already_added_callback[attr_name.to_sym][dependent_attr_name.to_sym]
131
+
132
+ subject_class.class_eval do
133
+ after_commit(on: :save) do
134
+ if changes[dependent_attr_name.to_s]
135
+ Rails.cache.delete(CachedSerializer::AttrSerializer.cache_key(subject, attr_name))
136
+ end
137
+ end
138
+ end
139
+
140
+ @already_added_callback[attr_name.to_sym][dependent_attr_name.to_sym] = true
141
+ end
142
+ end
143
+
144
+ def initialize(subject)
145
+ self.subject = subject
146
+ end
147
+
148
+ def to_h
149
+ self.class.serializers.serialize_for(subject)
150
+ end
151
+
152
+ def as_json
153
+ to_h
154
+ end
155
+
156
+ def to_json
157
+ JSON.generate(as_json)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,4 @@
1
+ module CachedSerializer
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module CachedSerializer
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :cached_serializer do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cached_serializer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Keegan Leitz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-10-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A serializer for Rails models that prevents unnecessary lookups
42
+ email:
43
+ - keegan@openbay.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - Gemfile
49
+ - LICENSE
50
+ - MIT-LICENSE
51
+ - README.md
52
+ - Rakefile
53
+ - lib/cached_serializer.rb
54
+ - lib/cached_serializer/attr_serializer.rb
55
+ - lib/cached_serializer/attr_serializer_collection.rb
56
+ - lib/cached_serializer/base.rb
57
+ - lib/cached_serializer/railtie.rb
58
+ - lib/cached_serializer/version.rb
59
+ - lib/tasks/cached_serializer_tasks.rake
60
+ homepage: https://github.com/kjleitz/cached_serializer
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.0.6
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: A serializer for Rails models that prevents unnecessary lookups
83
+ test_files: []