cached_serializer 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: 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: []