Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialization and Cache Documentation #1260

Merged
merged 3 commits into from
Oct 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 78 additions & 9 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
require 'active_model/serializer/fieldset'
require 'active_model/serializer/lint'

# ActiveModel::Serializer is an abstract class that is
# reified when subclassed to decorate a resource.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo here with reified. Do you mean redefined?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BenMorganIO No no, "to reify" is a verb that means "to make something abstract concrete" (well, technically, "to look at something abstract as a concrete thing"). Here he just means that this is an abstract class, the use of which will be to serve as a base for actual serializers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, i mean reified. Is just fancy for 'made real'. Intention is like instantiated but for object not instance

B mobile phone

On Oct 12, 2015, at 4:19 AM, Ben A. Morgan notifications@github.com wrote:

In lib/active_model/serializer.rb:

@@ -6,6 +6,8 @@
require 'active_model/serializer/fieldset'
require 'active_model/serializer/lint'

+# ActiveModel::Serializer is an abstract class that is
+# reified when subclassed to decorate a resource.
Typo here with reified. Do you mean redefined?


Reply to this email directly or view it on GitHub.

module ActiveModel
class Serializer
include Configuration
Expand Down Expand Up @@ -44,19 +46,28 @@ def self.digest_caller_file(caller_line)

with_options instance_writer: false, instance_reader: false do |serializer|
class_attribute :_type, instance_reader: true
class_attribute :_attributes
class_attribute :_attributes # @api private : names of attribute methods, @see Serializer#attribute
self._attributes ||= []
class_attribute :_attributes_keys
class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute
self._attributes_keys ||= {}
serializer.class_attribute :_cache
serializer.class_attribute :_fragmented
serializer.class_attribute :_cache_key
serializer.class_attribute :_cache_only
serializer.class_attribute :_cache_except
serializer.class_attribute :_cache_options
serializer.class_attribute :_cache_digest
serializer.class_attribute :_cache # @api private : the cache object
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
# _cache_options include:
# expires_in
# compress
# force
# race_condition_ttl
# Passed to ::_cache as
# serializer._cache.fetch(cache_key, @klass._cache_options)
serializer.class_attribute :_cache_digest # @api private : Generated
end

# Serializers inherit _attributes and _attributes_keys.
# Generates a unique digest for each serializer at load.
def self.inherited(base)
caller_line = caller.first
base._attributes = _attributes.dup
Expand All @@ -65,10 +76,16 @@ def self.inherited(base)
super
end

# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# type 'authors'
def self.type(type)
self._type = type
end

# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# attributes :id, :name, :recent_edits
def self.attributes(*attrs)
attrs = attrs.first if attrs.first.class == Array

Expand All @@ -77,6 +94,14 @@ def self.attributes(*attrs)
end
end

# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# attributes :id, :recent_edits
# attribute :name, key: :title
#
# def recent_edits
# object.edits.last(5)
# enr
def self.attribute(attr, options = {})
key = options.fetch(:key, attr)
_attributes_keys[attr] = { key: key } if key != attr
Expand All @@ -89,11 +114,35 @@ def self.attribute(attr, options = {})
end
end

# @api private
# Used by FragmentCache on the CachedSerializer
# to call attribute methods on the fragmented cached serializer.
def self.fragmented(serializer)
self._fragmented = serializer
end

# Enables a serializer to be automatically cached
#
# Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
# when Rails.configuration.action_controller.perform_caching
#
# @params options [Hash] with valid keys:
# key : @see ::_cache_key
# only : @see ::_cache_only
# except : @see ::_cache_except
# skip_digest : does not include digest in cache_key
# all else : @see ::_cache_options
#
# @example
# class PostSerializer < ActiveModel::Serializer
# cache key: 'post', expires_in: 3.hours
# attributes :title, :body
#
# has_many :comments
# end
#
# @todo require less code comments. See
# https://github.com/rails-api/active_model_serializers/pull/1249#issuecomment-146567837
def self.cache(options = {})
self._cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
self._cache_key = options.delete(:key)
Expand All @@ -102,6 +151,13 @@ def self.cache(options = {})
self._cache_options = (options.empty?) ? nil : options
end

# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
# @return [ActiveModel::Serializer]
# Preferentially returns
# 1. resource.serializer
# 2. ArraySerializer when resource is a collection
# 3. options[:serializer]
# 4. lookup serializer when resource is a Class
def self.serializer_for(resource, options = {})
if resource.respond_to?(:serializer_class)
resource.serializer_class
Expand All @@ -117,6 +173,8 @@ def self.adapter
ActiveModel::Serializer::Adapter.lookup(config.adapter)
end

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

# @api private
# Find a serializer from a class and caches the lookup.
# Preferentially retuns:
# 1. class name appended with "Serializer"
# 2. try again with superclass, if present
# 3. nil
def self.get_serializer_for(klass)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly out of the scope of this PR but how about renaming this method to something like lookup_serializer_for? It really makes my eyes bleed to have serializer_for calling get_serializer_for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely out of scope normally.. but this is already a 3 in 1. I could still break this down into smaller ones.. (when I get back)

Also, needs to be updated now that #1225 has been merged

serializers_cache.fetch_or_store(klass) do
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
Expand All @@ -151,6 +214,9 @@ def self.get_serializer_for(klass)

attr_accessor :object, :root, :scope

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

# Used by adapter as resource root.
def json_key
root || object.class.model_name.to_s.underscore
end

# Return the +attributes+ of +object+ as presented
# by the serializer.
def attributes
attributes = self.class._attributes.dup

Expand Down
30 changes: 28 additions & 2 deletions lib/active_model/serializer/adapter/fragment_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ def initialize(adapter, serializer, options)
@serializer = serializer
end

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

# Instanciate both serializers
# Instantiate both serializers
cached_serializer = serializers[:cached].constantize.new(serializer.object)
non_cached_serializer = serializers[:non_cached].constantize.new(serializer.object)

Expand All @@ -36,6 +41,10 @@ def fetch

private

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

# Given a resource name and its serializer's class
# 1. Dyanmically creates a CachedSerializer and NonCachedSerializer
# for a given class 'name'
# 2. Call
# CachedSerializer.cache(serializer._cache_options)
# CachedSerializer.fragmented(serializer)
# NontCachedSerializer.cache(serializer._cache_options)
# 3. Build a hash keyed to the +cached+ and +non_cached+ serializers
# 4. Call +cached_attributes+ on the serializer class and the above hash
# 5. Return the hash
#
# @example
# When +name+ is <tt>User::Admin</tt>
# creates the Serializer classes (if they don't exist).
# User_AdminCachedSerializer
# User_AdminNOnCachedSerializer
#
def fragment_serializer(name, klass)
cached = "#{to_valid_const_name(name)}CachedSerializer"
non_cached = "#{to_valid_const_name(name)}NonCachedSerializer"
Expand Down