Skip to content

Commit 86fa7a9

Browse files
committed
Merge pull request #1260 from bf4/serializer_cache_digest
Serialization and Cache Documentation
2 parents 9b0c865 + 274cb66 commit 86fa7a9

File tree

2 files changed

+106
-11
lines changed

2 files changed

+106
-11
lines changed

lib/active_model/serializer.rb

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
require 'active_model/serializer/fieldset'
88
require 'active_model/serializer/lint'
99

10+
# ActiveModel::Serializer is an abstract class that is
11+
# reified when subclassed to decorate a resource.
1012
module ActiveModel
1113
class Serializer
1214
include Configuration
@@ -44,19 +46,28 @@ def self.digest_caller_file(caller_line)
4446

4547
with_options instance_writer: false, instance_reader: false do |serializer|
4648
class_attribute :_type, instance_reader: true
47-
class_attribute :_attributes
49+
class_attribute :_attributes # @api private : names of attribute methods, @see Serializer#attribute
4850
self._attributes ||= []
49-
class_attribute :_attributes_keys
51+
class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute
5052
self._attributes_keys ||= {}
51-
serializer.class_attribute :_cache
52-
serializer.class_attribute :_fragmented
53-
serializer.class_attribute :_cache_key
54-
serializer.class_attribute :_cache_only
55-
serializer.class_attribute :_cache_except
56-
serializer.class_attribute :_cache_options
57-
serializer.class_attribute :_cache_digest
53+
serializer.class_attribute :_cache # @api private : the cache object
54+
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
55+
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
56+
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
57+
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
58+
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
59+
# _cache_options include:
60+
# expires_in
61+
# compress
62+
# force
63+
# race_condition_ttl
64+
# Passed to ::_cache as
65+
# serializer._cache.fetch(cache_key, @klass._cache_options)
66+
serializer.class_attribute :_cache_digest # @api private : Generated
5867
end
5968

69+
# Serializers inherit _attributes and _attributes_keys.
70+
# Generates a unique digest for each serializer at load.
6071
def self.inherited(base)
6172
caller_line = caller.first
6273
base._attributes = _attributes.dup
@@ -65,10 +76,16 @@ def self.inherited(base)
6576
super
6677
end
6778

79+
# @example
80+
# class AdminAuthorSerializer < ActiveModel::Serializer
81+
# type 'authors'
6882
def self.type(type)
6983
self._type = type
7084
end
7185

86+
# @example
87+
# class AdminAuthorSerializer < ActiveModel::Serializer
88+
# attributes :id, :name, :recent_edits
7289
def self.attributes(*attrs)
7390
attrs = attrs.first if attrs.first.class == Array
7491

@@ -77,6 +94,14 @@ def self.attributes(*attrs)
7794
end
7895
end
7996

97+
# @example
98+
# class AdminAuthorSerializer < ActiveModel::Serializer
99+
# attributes :id, :recent_edits
100+
# attribute :name, key: :title
101+
#
102+
# def recent_edits
103+
# object.edits.last(5)
104+
# enr
80105
def self.attribute(attr, options = {})
81106
key = options.fetch(:key, attr)
82107
_attributes_keys[attr] = { key: key } if key != attr
@@ -89,11 +114,35 @@ def self.attribute(attr, options = {})
89114
end
90115
end
91116

117+
# @api private
118+
# Used by FragmentCache on the CachedSerializer
119+
# to call attribute methods on the fragmented cached serializer.
92120
def self.fragmented(serializer)
93121
self._fragmented = serializer
94122
end
95123

96124
# Enables a serializer to be automatically cached
125+
#
126+
# Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
127+
# when Rails.configuration.action_controller.perform_caching
128+
#
129+
# @params options [Hash] with valid keys:
130+
# key : @see ::_cache_key
131+
# only : @see ::_cache_only
132+
# except : @see ::_cache_except
133+
# skip_digest : does not include digest in cache_key
134+
# all else : @see ::_cache_options
135+
#
136+
# @example
137+
# class PostSerializer < ActiveModel::Serializer
138+
# cache key: 'post', expires_in: 3.hours
139+
# attributes :title, :body
140+
#
141+
# has_many :comments
142+
# end
143+
#
144+
# @todo require less code comments. See
145+
# https://github.com/rails-api/active_model_serializers/pull/1249#issuecomment-146567837
97146
def self.cache(options = {})
98147
self._cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
99148
self._cache_key = options.delete(:key)
@@ -102,6 +151,13 @@ def self.cache(options = {})
102151
self._cache_options = (options.empty?) ? nil : options
103152
end
104153

154+
# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
155+
# @return [ActiveModel::Serializer]
156+
# Preferentially returns
157+
# 1. resource.serializer
158+
# 2. ArraySerializer when resource is a collection
159+
# 3. options[:serializer]
160+
# 4. lookup serializer when resource is a Class
105161
def self.serializer_for(resource, options = {})
106162
if resource.respond_to?(:serializer_class)
107163
resource.serializer_class
@@ -117,6 +173,8 @@ def self.adapter
117173
ActiveModel::Serializer::Adapter.lookup(config.adapter)
118174
end
119175

176+
# Used to cache serializer name => serializer class
177+
# when looked up by Serializer.get_serializer_for.
120178
def self.serializers_cache
121179
@serializers_cache ||= ThreadSafe::Cache.new
122180
end
@@ -136,6 +194,11 @@ def self.serializer_lookup_chain_for(klass)
136194
end
137195

138196
# @api private
197+
# Find a serializer from a class and caches the lookup.
198+
# Preferentially retuns:
199+
# 1. class name appended with "Serializer"
200+
# 2. try again with superclass, if present
201+
# 3. nil
139202
def self.get_serializer_for(klass)
140203
serializers_cache.fetch_or_store(klass) do
141204
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
@@ -151,6 +214,9 @@ def self.get_serializer_for(klass)
151214

152215
attr_accessor :object, :root, :scope
153216

217+
# `scope_name` is set as :current_user by default in the controller.
218+
# If the instance does not have a method named `scope_name`, it
219+
# defines the method so that it calls the +scope+.
154220
def initialize(object, options = {})
155221
self.object = object
156222
self.instance_options = options
@@ -165,10 +231,13 @@ def initialize(object, options = {})
165231
end
166232
end
167233

234+
# Used by adapter as resource root.
168235
def json_key
169236
root || object.class.model_name.to_s.underscore
170237
end
171238

239+
# Return the +attributes+ of +object+ as presented
240+
# by the serializer.
172241
def attributes
173242
attributes = self.class._attributes.dup
174243

lib/active_model/serializer/adapter/fragment_cache.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ def initialize(adapter, serializer, options)
1010
@serializer = serializer
1111
end
1212

13+
# TODO: Use Serializable::Resource
14+
# TODO: call +constantize+ less
15+
# 1. Create a CachedSerializer and NonCachedSerializer from the serializer class
16+
# 2. Serialize the above two with the given adapter
17+
# 3. Pass their serializations to the adapter +::fragment_cache+
1318
def fetch
1419
klass = serializer.class
15-
# It will split the serializer into two, one that will be cached and other wont
20+
# It will split the serializer into two, one that will be cached and one that will not
1621
serializers = fragment_serializer(serializer.object.class.name, klass)
1722

18-
# Instanciate both serializers
23+
# Instantiate both serializers
1924
cached_serializer = serializers[:cached].constantize.new(serializer.object)
2025
non_cached_serializer = serializers[:non_cached].constantize.new(serializer.object)
2126

@@ -36,6 +41,10 @@ def fetch
3641

3742
private
3843

44+
# Given a serializer class and a hash of its cached and non-cached serializers
45+
# 1. Determine cached attributes from serializer class options
46+
# 2. Add cached attributes to cached Serializer
47+
# 3. Add non-cached attributes to non-cached Serializer
3948
def cached_attributes(klass, serializers)
4049
attributes = serializer.class._attributes
4150
cached_attributes = (klass._cache_only) ? klass._cache_only : attributes.reject { |attr| klass._cache_except.include?(attr) }
@@ -56,6 +65,23 @@ def cached_attributes(klass, serializers)
5665
end
5766
end
5867

68+
# Given a resource name and its serializer's class
69+
# 1. Dyanmically creates a CachedSerializer and NonCachedSerializer
70+
# for a given class 'name'
71+
# 2. Call
72+
# CachedSerializer.cache(serializer._cache_options)
73+
# CachedSerializer.fragmented(serializer)
74+
# NontCachedSerializer.cache(serializer._cache_options)
75+
# 3. Build a hash keyed to the +cached+ and +non_cached+ serializers
76+
# 4. Call +cached_attributes+ on the serializer class and the above hash
77+
# 5. Return the hash
78+
#
79+
# @example
80+
# When +name+ is <tt>User::Admin</tt>
81+
# creates the Serializer classes (if they don't exist).
82+
# User_AdminCachedSerializer
83+
# User_AdminNOnCachedSerializer
84+
#
5985
def fragment_serializer(name, klass)
6086
cached = "#{to_valid_const_name(name)}CachedSerializer"
6187
non_cached = "#{to_valid_const_name(name)}NonCachedSerializer"

0 commit comments

Comments
 (0)