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 `'" # AND # "/c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `'" # 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 ActionController::Base.cache_store # 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_tree) return {} if ActiveModelSerializers.config.cache_store.blank? keys = object_cache_keys(collection_serializer, adapter_instance, include_tree) 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_tree [ActiveModel::Serializer::IncludeTree] # @return [Array] all cache_key of collection_serializer def object_cache_keys(collection_serializer, adapter_instance, include_tree) cache_keys = [] collection_serializer.each do |serializer| cache_keys << object_cache_key(serializer, adapter_instance) serializer.associations(include_tree).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 # Get attributes from @cached_attributes # @return [Hash] cached attributes # def cached_attributes(fields, adapter_instance) def cached_fields(fields, adapter_instance) cache_check(adapter_instance) do attributes(fields) 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 User::Admin # 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