active_model_serializers/lib/active_model/serializer/caching.rb
2016-06-01 01:02:13 -05:00

343 lines
14 KiB
Ruby

# TODO(BF): refactor file to be smaller
# rubocop:disable Metrics/ModuleLength
module ActiveModel
class Serializer
UndefinedCacheKey = Class.new(StandardError)
module Caching
extend ActiveSupport::Concern
included do
with_options instance_writer: false, instance_reader: false do |serializer|
serializer.class_attribute :_cache # @api private : the cache store
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #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_store.fetch(cache_key, @klass._cache_options)
# Passed as second argument to serializer.cache_store.fetch(cache_key, self.class._cache_options)
serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance
end
end
# Matches
# "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
# AND
# "/c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
# AS
# c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb
CALLER_FILE = /
\A # start of string
.+ # file path (one or more characters)
(?= # stop previous match when
:\d+ # a colon is followed by one or more digits
:in # followed by a colon followed by in
)
/x
module ClassMethods
def inherited(base)
super
caller_line = caller[1]
base._cache_digest_file_path = caller_line
end
def _cache_digest
return @_cache_digest if defined?(@_cache_digest)
@_cache_digest = digest_caller_file(_cache_digest_file_path)
end
# Hashes contents of file for +_cache_digest+
def digest_caller_file(caller_line)
serializer_file_path = caller_line[CALLER_FILE]
serializer_file_contents = IO.read(serializer_file_path)
Digest::MD5.hexdigest(serializer_file_contents)
rescue TypeError, Errno::ENOENT
warn <<-EOF.strip_heredoc
Cannot digest non-existent file: '#{caller_line}'.
Please set `::_cache_digest` of the serializer
if you'd like to cache it.
EOF
''.freeze
end
def _skip_digest?
_cache_options && _cache_options[:skip_digest]
end
def cached_attributes
_cache_only ? _cache_only : _attributes - _cache_except
end
def non_cached_attributes
_attributes - cached_attributes
end
# @api private
# Used by FragmentCache on the CachedSerializer
# to call attribute methods on the fragmented cached serializer.
def 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
#
# @param options [Hash] with valid keys:
# cache_store : @see ::_cache
# 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 cache(options = {})
self._cache =
options.delete(:cache_store) ||
ActiveModelSerializers.config.cache_store ||
ActiveSupport::Cache.lookup_store(:null_store)
self._cache_key = options.delete(:key)
self._cache_only = options.delete(:only)
self._cache_except = options.delete(:except)
self._cache_options = options.empty? ? nil : options
end
# Value is from ActiveModelSerializers.config.perform_caching. Is used to
# globally enable or disable all serializer caching, just like
# Rails.configuration.action_controller.perform_caching, which is its
# default value in a Rails application.
# @return [true, false]
# Memoizes value of config first time it is called with a non-nil value.
# rubocop:disable Style/ClassVars
def perform_caching
return @@perform_caching if defined?(@@perform_caching) && !@@perform_caching.nil?
@@perform_caching = ActiveModelSerializers.config.perform_caching
end
alias perform_caching? perform_caching
# rubocop:enable Style/ClassVars
# The canonical method for getting the cache store for the serializer.
#
# @return [nil] when _cache is not set (i.e. when `cache` has not been called)
# @return [._cache] when _cache is not the NullStore
# @return [ActiveModelSerializers.config.cache_store] when _cache is the NullStore.
# This is so we can use `cache` being called to mean the serializer should be cached
# even if ActiveModelSerializers.config.cache_store has not yet been set.
# That means that when _cache is the NullStore and ActiveModelSerializers.config.cache_store
# is configured, `cache_store` becomes `ActiveModelSerializers.config.cache_store`.
# @return [nil] when _cache is the NullStore and ActiveModelSerializers.config.cache_store is nil.
def cache_store
return nil if _cache.nil?
return _cache if _cache.class != ActiveSupport::Cache::NullStore
if ActiveModelSerializers.config.cache_store
self._cache = ActiveModelSerializers.config.cache_store
else
nil
end
end
def cache_enabled?
perform_caching? && cache_store && !_cache_only && !_cache_except
end
def fragment_cache_enabled?
perform_caching? && cache_store &&
(_cache_only && !_cache_except || !_cache_only && _cache_except)
end
# Read cache from cache_store
# @return [Hash]
def cache_read_multi(collection_serializer, adapter_instance, include_directive)
return {} if ActiveModelSerializers.config.cache_store.blank?
keys = object_cache_keys(collection_serializer, adapter_instance, include_directive)
return {} if keys.blank?
ActiveModelSerializers.config.cache_store.read_multi(*keys)
end
# Find all cache_key for the collection_serializer
# @param serializers [ActiveModel::Serializer::CollectionSerializer]
# @param adapter_instance [ActiveModelSerializers::Adapter::Base]
# @param include_directive [JSONAPI::IncludeDirective]
# @return [Array] all cache_key of collection_serializer
def object_cache_keys(collection_serializer, adapter_instance, include_directive)
cache_keys = []
collection_serializer.each do |serializer|
cache_keys << object_cache_key(serializer, adapter_instance)
serializer.associations(include_directive).each do |association|
if association.serializer.respond_to?(:each)
association.serializer.each do |sub_serializer|
cache_keys << object_cache_key(sub_serializer, adapter_instance)
end
else
cache_keys << object_cache_key(association.serializer, adapter_instance)
end
end
end
cache_keys.compact.uniq
end
# @return [String, nil] the cache_key of the serializer or nil
def object_cache_key(serializer, adapter_instance)
return unless serializer.present? && serializer.object.present?
serializer.class.cache_enabled? ? serializer.cache_key(adapter_instance) : nil
end
end
def cached_attributes(options, cached_attributes, adapter_instance)
if self.class.cache_enabled?
key = cache_key(adapter_instance)
cached_attributes.fetch(key) do
cache_check(adapter_instance) do
attributes(options[:fields])
end
end
else
cache_check(adapter_instance) do
attributes(options[:fields])
end
end
end
def cache_check(adapter_instance)
if self.class.cache_enabled?
self.class.cache_store.fetch(cache_key(adapter_instance), self.class._cache_options) do
yield
end
elsif self.class.fragment_cache_enabled?
fetch_fragment_cache(adapter_instance)
else
yield
end
end
# 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+
#
# It will split the serializer into two, one that will be cached and one that will not
#
# Given a resource name
# 1. Dynamically creates a CachedSerializer and NonCachedSerializer
# for a given class 'name'
# 2. Call
# CachedSerializer.cache(serializer._cache_options)
# CachedSerializer.fragmented(serializer)
# NonCachedSerializer.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).
# CachedUser_AdminSerializer
# NonCachedUser_AdminSerializer
#
# Given 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 fetch_fragment_cache(adapter_instance)
serializer_class_name = self.class.name.gsub('::'.freeze, '_'.freeze)
self.class._cache_options ||= {}
self.class._cache_options[:key] = self.class._cache_key if self.class._cache_key
cached_serializer = _get_or_create_fragment_cached_serializer(serializer_class_name)
cached_hash = ActiveModelSerializers::SerializableResource.new(
object,
serializer: cached_serializer,
adapter: adapter_instance.class
).serializable_hash
non_cached_serializer = _get_or_create_fragment_non_cached_serializer(serializer_class_name)
non_cached_hash = ActiveModelSerializers::SerializableResource.new(
object,
serializer: non_cached_serializer,
adapter: adapter_instance.class
).serializable_hash
# Merge both results
adapter_instance.fragment_cache(cached_hash, non_cached_hash)
end
def _get_or_create_fragment_cached_serializer(serializer_class_name)
cached_serializer = _get_or_create_fragment_serializer "Cached#{serializer_class_name}"
cached_serializer.cache(self.class._cache_options)
cached_serializer.type(self.class._type)
cached_serializer.fragmented(self)
self.class.cached_attributes.each do |attribute|
options = self.class._attributes_keys[attribute] || {}
cached_serializer.attribute(attribute, options)
end
cached_serializer
end
def _get_or_create_fragment_non_cached_serializer(serializer_class_name)
non_cached_serializer = _get_or_create_fragment_serializer "NonCached#{serializer_class_name}"
non_cached_serializer.type(self.class._type)
non_cached_serializer.fragmented(self)
self.class.non_cached_attributes.each do |attribute|
options = self.class._attributes_keys[attribute] || {}
non_cached_serializer.attribute(attribute, options)
end
non_cached_serializer
end
def _get_or_create_fragment_serializer(name)
return Object.const_get(name) if Object.const_defined?(name)
Object.const_set(name, Class.new(ActiveModel::Serializer))
end
def cache_key(adapter_instance)
return @cache_key if defined?(@cache_key)
parts = []
parts << object_cache_key
parts << adapter_instance.cached_name
parts << self.class._cache_digest unless self.class._skip_digest?
@cache_key = parts.join('/')
end
# Use object's cache_key if available, else derive a key from the object
# Pass the `key` option to the `cache` declaration or override this method to customize the cache key
def object_cache_key
if object.respond_to?(:cache_key)
object.cache_key
elsif (serializer_cache_key = (self.class._cache_key || self.class._cache_options[:key]))
object_time_safe = object.updated_at
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
"#{serializer_cache_key}/#{object.id}-#{object_time_safe}"
else
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{self.class}.cache'"
end
end
end
end
end
# rubocop:enable Metrics/ModuleLength