From 80e75202ea1d6ba1173e78b6852cd4d868c65cba Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 9 Oct 2015 00:49:47 -0500 Subject: [PATCH 1/3] Document Serializer and FragmentCache --- lib/active_model/serializer.rb | 87 +++++++++++++++++-- .../serializer/adapter/fragment_cache.rb | 28 +++++- 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index fd3576301..318e986ee 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -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. module ActiveModel class Serializer include Configuration @@ -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 : + 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 @@ -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 @@ -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 @@ -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 ActionController::Base.cache_store + # 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) @@ -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 retuns + # 1. resource.serializer + # 2. ArraySerializer for a collection + # 3. options[:serializer] + # 4. lookup serializer by class (i.e. resource is a class) def self.serializer_for(resource, options = {}) if resource.respond_to?(:serializer_class) resource.serializer_class @@ -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 @@ -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) serializers_cache.fetch_or_store(klass) do # 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) 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 nameed `scope_name`, it + # defines the method so that calls the +scope+. def initialize(object, options = {}) self.object = object self.instance_options = options @@ -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 diff --git a/lib/active_model/serializer/adapter/fragment_cache.rb b/lib/active_model/serializer/adapter/fragment_cache.rb index cf54a3307..c6c0aaecc 100644 --- a/lib/active_model/serializer/adapter/fragment_cache.rb +++ b/lib/active_model/serializer/adapter/fragment_cache.rb @@ -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 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) @@ -36,6 +41,10 @@ def fetch private + # Given a serializer class and a hash of its cached and non0cached 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) } @@ -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 User::Admin + # 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" From 63317699f3d151813cf304d6aa1685f50e2bb085 Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Sat, 10 Oct 2015 15:40:38 +0200 Subject: [PATCH 2/3] Correct minor typos --- lib/active_model/serializer.rb | 10 +++++----- .../serializer/adapter/fragment_cache.rb | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 318e986ee..6b9749175 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -116,7 +116,7 @@ def self.attribute(attr, options = {}) # @api private # Used by FragmentCache on the CachedSerializer - # to call attribute methods on the fragmented cached serializer + # to call attribute methods on the fragmented cached serializer. def self.fragmented(serializer) self._fragmented = serializer end @@ -174,7 +174,7 @@ def self.adapter end # Used to cache serializer name => serializer class - # when looked up by Serializer.get_serializer_for + # when looked up by Serializer.get_serializer_for. def self.serializers_cache @serializers_cache ||= ThreadSafe::Cache.new end @@ -215,8 +215,8 @@ 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 nameed `scope_name`, it - # defines the method so that calls the +scope+. + # 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 @@ -231,7 +231,7 @@ def initialize(object, options = {}) end end - # Used by adapter as resource root + # Used by adapter as resource root. def json_key root || object.class.model_name.to_s.underscore end diff --git a/lib/active_model/serializer/adapter/fragment_cache.rb b/lib/active_model/serializer/adapter/fragment_cache.rb index c6c0aaecc..5c97a64a4 100644 --- a/lib/active_model/serializer/adapter/fragment_cache.rb +++ b/lib/active_model/serializer/adapter/fragment_cache.rb @@ -12,12 +12,12 @@ def initialize(adapter, serializer, options) # 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+. + # 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) # Instantiate both serializers @@ -41,9 +41,9 @@ def fetch private - # Given a serializer class and a hash of its cached and non0cached serializers - # 1. Determine cached attributes from serializer class options. - # 2. Add cached attributes to cached Serializer. + # 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 @@ -66,8 +66,8 @@ def cached_attributes(klass, serializers) end # Given a resource name and its serializer's class - # 1. Dyanmically creates a CachedSerializer and NonCachedSerializer - # for a given class 'name'. + # 1. Dyanmically creates a CachedSerializer and NonCachedSerializer + # for a given class 'name' # 2. Call # CachedSerializer.cache(serializer._cache_options) # CachedSerializer.fragmented(serializer) From 274cb66d34f64d9b1de423279f0012e1f889b1cb Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Wed, 21 Oct 2015 16:24:21 -0500 Subject: [PATCH 3/3] Edits per beauby [ci skip] --- lib/active_model/serializer.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 6b9749175..f61284105 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -51,11 +51,11 @@ def self.digest_caller_file(caller_line) class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute self._attributes_keys ||= {} serializer.class_attribute :_cache # @api private : the cache object - serializer.class_attribute :_fragmented # @api private : + 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 + serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch # _cache_options include: # expires_in # compress @@ -153,11 +153,11 @@ def self.cache(options = {}) # @param resource [ActiveRecord::Base, ActiveModelSerializers::Model] # @return [ActiveModel::Serializer] - # Preferentially retuns + # Preferentially returns # 1. resource.serializer - # 2. ArraySerializer for a collection + # 2. ArraySerializer when resource is a collection # 3. options[:serializer] - # 4. lookup serializer by class (i.e. resource is a class) + # 4. lookup serializer when resource is a Class def self.serializer_for(resource, options = {}) if resource.respond_to?(:serializer_class) resource.serializer_class