Adding Fragment Cache to AMS

It's an upgrade based on the new Cache implementation #693.
It allows to use the Rails conventions to cache
specific attributes or associations.
It's based on the Cache Composition implementation.
This commit is contained in:
João Moura 2015-02-03 21:57:02 -02:00
parent 48ed7cf9ba
commit 792fb8a905
19 changed files with 554 additions and 101 deletions

View File

@ -4,3 +4,5 @@
* adds method to override association [adcb99e, @kurko]
* adds `has_one` attribute for backwards compatibility [@ggordon]
* updates JSON API support to RC3 [@mateomurphy]
* adds fragment cache support [@joaomdmoura]
* adds cache support to attributes and associations [@joaomdmoura]

View File

@ -271,7 +271,10 @@ The options are the same options of ```ActiveSupport::Cache::Store```, plus
a ```key``` option that will be the prefix of the object cache
on a pattern ```"#{key}/#{object.id}-#{object.updated_at}"```.
The cache support is optimized to use the cached object in multiple request. An object cached on an ```show``` request will be reused at the ```index```. If there is a relationship with another cached serializer it will also be created and reused automatically.
**[NOTE] Every object is individually cached.**
**[NOTE] The cache is automatically expired after update an object but it's not deleted.**
```ruby
@ -295,6 +298,27 @@ On this example every ```Post``` object will be cached with
the key ```"post/#{post.id}-#{post.updated_at}"```. You can use this key to expire it as you want,
but in this case it will be automatically expired after 3 hours.
### Fragmenting Caching
If there is some API endpoint that shouldn't be fully cached, you can still optmise it, using Fragment Cache on the attributes and relationships that you want to cache.
You can define the attribute by using ```only``` or ```except``` option on cache method.
**[NOTE] Cache serializers will be used at their relationships**
Example:
```ruby
class PostSerializer < ActiveModel::Serializer
cache key: 'post', expires_in: 3.hours, only: [:title]
attributes :title, :body
has_many :comments
url :post
end
```
## Getting Help
If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new).

View File

@ -10,41 +10,54 @@ module ActiveModel
class << self
attr_accessor :_attributes
attr_accessor :_attributes_keys
attr_accessor :_associations
attr_accessor :_urls
attr_accessor :_cache
attr_accessor :_fragmented
attr_accessor :_cache_key
attr_accessor :_cache_only
attr_accessor :_cache_except
attr_accessor :_cache_options
end
def self.inherited(base)
base._attributes = []
base._attributes_keys = {}
base._associations = {}
base._urls = []
end
def self.attributes(*attrs)
attrs = attrs.first if attrs.first.class == Array
@_attributes.concat attrs
attrs.each do |attr|
define_method attr do
object && object.read_attribute_for_serialization(attr)
end unless method_defined?(attr)
end unless method_defined?(attr) || _fragmented.respond_to?(attr)
end
end
def self.attribute(attr, options = {})
key = options.fetch(:key, attr)
@_attributes_keys[attr] = {key: key} if key != attr
@_attributes.concat [key]
define_method key do
object.read_attribute_for_serialization(attr)
end unless method_defined?(key)
end unless method_defined?(key) || _fragmented.respond_to?(attr)
end
def self.fragmented(serializer)
@_fragmented = serializer
end
# Enables a serializer to be automatically cached
def self.cache(options = {})
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
@_cache_key = options.delete(:key)
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
@_cache_key = options.delete(:key)
@_cache_only = options.delete(:only)
@_cache_except = options.delete(:except)
@_cache_options = (options.empty?) ? nil : options
end
@ -141,12 +154,12 @@ module ActiveModel
attr_accessor :object, :root, :meta, :meta_key, :scope
def initialize(object, options = {})
@object = object
@options = options
@root = options[:root] || (self.class._root ? self.class.root_name : false)
@meta = options[:meta]
@meta_key = options[:meta_key]
@scope = options[:scope]
@object = object
@options = options
@root = options[:root] || (self.class._root ? self.class.root_name : false)
@meta = options[:meta]
@meta_key = options[:meta_key]
@scope = options[:scope]
scope_name = options[:scope_name]
if scope_name && !respond_to?(scope_name)
@ -183,22 +196,29 @@ module ActiveModel
attributes += options[:required_fields] if options[:required_fields]
attributes.each_with_object({}) do |name, hash|
hash[name] = send(name)
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
def each_association(&block)
self.class._associations.dup.each do |name, association_options|
next unless object
association_value = send(name)
serializer_class = ActiveModel::Serializer.serializer_for(association_value, association_options)
serializer = serializer_class.new(
association_value,
options.merge(serializer_from_options(association_options))
) if serializer_class
if serializer_class
serializer = serializer_class.new(
association_value,
options.merge(serializer_from_options(association_options))
)
elsif !association_value.nil? && !association_value.instance_of?(Object)
association_options[:association_options][:virtual_value] = association_value
end
if block_given?
block.call(name, serializer, association_options[:association_options])

View File

@ -1,3 +1,5 @@
require 'active_model/serializer/adapter/fragment_cache'
module ActiveModel
class Serializer
class Adapter
@ -32,8 +34,38 @@ module ActiveModel
"ActiveModel::Serializer::Adapter::#{adapter.to_s.classify}".safe_constantize
end
def fragment_cache(*args)
raise NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
end
private
def cache_check(serializer)
@cached_serializer = serializer
@klass = @cached_serializer.class
if is_cached?
@klass._cache.fetch(cache_key, @klass._cache_options) do
yield
end
elsif is_fragment_cached?
FragmentCache.new(self, @cached_serializer, @options, @root).fetch
else
yield
end
end
def is_cached?
@klass._cache && !@klass._cache_only && !@klass._cache_except
end
def is_fragment_cached?
@klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except
end
def cache_key
(@klass._cache_key) ? "#{@klass._cache_key}/#{@cached_serializer.object.id}-#{@cached_serializer.object.updated_at}" : @cached_serializer.object.cache_key
end
def meta
serializer.meta if serializer.respond_to?(:meta)
end
@ -50,20 +82,6 @@ module ActiveModel
json[meta_key] = meta if meta && root
json
end
private
def cached_object
klass = serializer.class
if klass._cache
_cache_key = (klass._cache_key) ? "#{klass._cache_key}/#{serializer.object.id}-#{serializer.object.updated_at}" : serializer.object.cache_key
klass._cache.fetch(_cache_key, klass._cache_options) do
yield
end
else
yield
end
end
end
end
end

View File

@ -0,0 +1,78 @@
module ActiveModel
class Serializer
class Adapter
class FragmentCache
attr_reader :serializer
def initialize(adapter, serializer, options, root)
@root = root
@options = options
@adapter = adapter
@serializer = serializer
end
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
cached_serializer = serializers[:cached].constantize.new(serializer.object)
non_cached_serializer = serializers[:non_cached].constantize.new(serializer.object)
cached_adapter = @adapter.class.new(cached_serializer, @options)
non_cached_adapter = @adapter.class.new(non_cached_serializer, @options)
# Get serializable hash from both
cached_hash = cached_adapter.serializable_hash
non_cached_hash = non_cached_adapter.serializable_hash
# Merge both results
@adapter.fragment_cache(cached_hash, non_cached_hash)
end
private
def cached_attributes(klass, serializers)
cached_attributes = (klass._cache_only) ? klass._cache_only : serializer.attributes.keys.delete_if {|attr| klass._cache_except.include?(attr) }
non_cached_attributes = serializer.attributes.keys.delete_if {|attr| cached_attributes.include?(attr) }
cached_attributes.each do |attribute|
options = serializer.class._attributes_keys[attribute]
options ||= {}
# Add cached attributes to cached Serializer
serializers[:cached].constantize.attribute(attribute, options)
end
non_cached_attributes.each do |attribute|
options = serializer.class._attributes_keys[attribute]
options ||= {}
# Add non-cached attributes to non-cached Serializer
serializers[:non_cached].constantize.attribute(attribute, options)
end
end
def fragment_serializer(name, klass)
cached = "#{name.capitalize}CachedSerializer"
non_cached = "#{name.capitalize}NonCachedSerializer"
Object.const_set cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(cached)
Object.const_set non_cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(non_cached)
klass._cache_options ||= {}
klass._cache_options[:key] = klass._cache_key if klass._cache_key
cached.constantize.cache(klass._cache_options)
cached.constantize.fragmented(serializer)
non_cached.constantize.fragmented(serializer)
serializers = {cached: cached, non_cached: non_cached}
cached_attributes(klass, serializers)
serializers
end
end
end
end
end

View File

@ -1,3 +1,5 @@
require 'active_model/serializer/adapter/json/fragment_cache'
module ActiveModel
class Serializer
class Adapter
@ -6,31 +8,45 @@ module ActiveModel
if serializer.respond_to?(:each)
@result = serializer.map{|s| self.class.new(s).serializable_hash }
else
@result = cached_object do
@hash = serializer.attributes(options)
serializer.each_association do |name, association, opts|
if association.respond_to?(:each)
array_serializer = association
@hash[name] = array_serializer.map { |item| item.attributes(opts) }
else
if association
@hash[name] = association.attributes(options)
else
@hash[name] = nil
@hash = {}
@core = cache_check(serializer) do
serializer.attributes(options)
end
serializer.each_association do |name, association, opts|
if association.respond_to?(:each)
array_serializer = association
@hash[name] = array_serializer.map do |item|
cache_check(item) do
item.attributes(opts)
end
end
else
if association
@hash[name] = cache_check(association) do
association.attributes(options)
end
elsif opts[:virtual_value]
@hash[name] = opts[:virtual_value]
else
@hash[name] = nil
end
end
@hash
end
@result = @core.merge @hash
end
if root = options.fetch(:root, serializer.json_key)
@result = { root => @result }
end
@result
end
end
def fragment_cache(cached_hash, non_cached_hash)
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
end
end
end
end

View File

@ -0,0 +1,15 @@
module ActiveModel
class Serializer
class Adapter
class Json < Adapter
class FragmentCache
def fragment_cache(cached_hash, non_cached_hash)
non_cached_hash.merge cached_hash
end
end
end
end
end
end

View File

@ -1,3 +1,5 @@
require 'active_model/serializer/adapter/json_api/fragment_cache'
module ActiveModel
class Serializer
class Adapter
@ -26,15 +28,17 @@ module ActiveModel
end
end
else
@hash = cached_object do
@hash[:data] = attributes_for_serializer(serializer, @options)
add_resource_links(@hash[:data], serializer)
@hash
end
@hash[:data] = attributes_for_serializer(serializer, @options)
add_resource_links(@hash[:data], serializer)
end
@hash
end
def fragment_cache(cached_hash, non_cached_hash)
root = false if @options.include?(:include)
JsonApi::FragmentCache.new().fragment_cache(root, cached_hash, non_cached_hash)
end
private
def add_links(resource, name, serializers)
@ -43,7 +47,7 @@ module ActiveModel
resource[:links][name][:linkage] += serializers.map { |serializer| { type: serializer.type, id: serializer.id.to_s } }
end
def add_link(resource, name, serializer)
def add_link(resource, name, serializer, val=nil)
resource[:links] ||= {}
resource[:links][name] = { linkage: nil }
@ -76,24 +80,27 @@ module ActiveModel
end
end
def attributes_for_serializer(serializer, options)
if serializer.respond_to?(:each)
result = []
serializer.each do |object|
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
options[:required_fields] = [:id, :type]
attributes = object.attributes(options)
attributes[:id] = attributes[:id].to_s
result << attributes
result << cache_check(object) do
options[:required_fields] = [:id, :type]
attributes = object.attributes(options)
attributes[:id] = attributes[:id].to_s
result << attributes
end
end
else
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
options[:required_fields] = [:id, :type]
result = serializer.attributes(options)
result[:id] = result[:id].to_s
result = cache_check(serializer) do
result = serializer.attributes(options)
result[:id] = result[:id].to_s
result
end
end
result
end
@ -124,7 +131,11 @@ module ActiveModel
if association.respond_to?(:each)
add_links(attrs, name, association)
else
add_link(attrs, name, association)
if opts[:virtual_value]
add_link(attrs, name, nil, opts[:virtual_value])
else
add_link(attrs, name, association)
end
end
if options[:add_included]

View File

@ -0,0 +1,22 @@
module ActiveModel
class Serializer
class Adapter
class JsonApi < Adapter
class FragmentCache
def fragment_cache(root, cached_hash, non_cached_hash)
hash = {}
core_cached = cached_hash.first
core_non_cached = non_cached_hash.first
no_root_cache = cached_hash.delete_if {|key, value| key == core_cached[0] }
no_root_non_cache = non_cached_hash.delete_if {|key, value| key == core_non_cached[0] }
cached_resource = (core_cached[1]) ? core_cached[1].merge(core_non_cached[1]) : core_non_cached[1]
hash = (root) ? { root => cached_resource } : cached_resource
hash.merge no_root_non_cache.merge no_root_cache
end
end
end
end
end
end

View File

@ -111,6 +111,8 @@ module ActionController
"id" => "1",
"type" => "roles",
"name" => "admin",
"description" => nil,
"slug" => "admin-1",
"links" => {
"author" => { "linkage" => { "type" =>"authors", "id" => "1" } }
}
@ -118,6 +120,8 @@ module ActionController
"id" => "2",
"type" => "roles",
"name" => "colab",
"description" => nil,
"slug" => "colab-2",
"links" => {
"author" => { "linkage" => { "type" =>"authors", "id" => "1" } }
}

View File

@ -48,36 +48,79 @@ module ActionController
end
def render_object_with_cache_enabled
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'New Post', blog:nil, body: 'Body', comments: [comment], author: author })
@comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
@author = Author.new(id: 1, name: 'Joao Moura.')
@post = Post.new({ id: 1, title: 'New Post', body: 'Body', comments: [@comment], author: @author })
generate_cached_serializer(post)
generate_cached_serializer(@post)
post.title = 'ZOMG a New Post'
render json: post
@post.title = 'ZOMG a New Post'
render json: @post
end
def update_and_render_object_with_cache_enabled
@post.updated_at = DateTime.now
generate_cached_serializer(@post)
render json: @post
end
def render_object_expired_with_cache_enabled
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'New Post', blog:nil, body: 'Body', comments: [comment], author: author })
post = Post.new({ id: 1, title: 'New Post', body: 'Body', comments: [comment], author: author })
generate_cached_serializer(post)
post.title = 'ZOMG a New Post'
sleep 0.05
sleep 0.1
render json: post
end
def render_changed_object_with_cache_enabled
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'ZOMG a New Post', blog:nil, body: 'Body', comments: [comment], author: author })
post = Post.new({ id: 1, title: 'ZOMG a New Post', body: 'Body', comments: [comment], author: author })
render json: post
end
def render_fragment_changed_object_with_only_cache_enabled
author = Author.new(id: 1, name: 'Joao Moura.')
role = Role.new({ id: 42, name: 'ZOMG A ROLE', description: 'DESCRIPTION HERE', author: author })
generate_cached_serializer(role)
role.name = 'lol'
role.description = 'HUEHUEBRBR'
render json: role
end
def render_fragment_changed_object_with_except_cache_enabled
author = Author.new(id: 1, name: 'Joao Moura.')
bio = Bio.new({ id: 42, content: 'ZOMG A ROLE', rating: 5, author: author })
generate_cached_serializer(bio)
bio.content = 'lol'
bio.rating = 0
render json: bio
end
def render_fragment_changed_object_with_relationship
comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
author = Author.new(id: 1, name: 'Joao Moura.')
post = Post.new({ id: 1, title: 'New Post', body: 'Body', comments: [comment], author: author })
post2 = Post.new({ id: 1, title: 'New Post2', body: 'Body2', comments: [comment], author: author })
like = Like.new({ id: 1, post: post, time: 3.days.ago })
generate_cached_serializer(like)
like.post = post2
like.time = DateTime.now.to_s
render json: like
end
private
def generate_cached_serializer(obj)
serializer_class = ActiveModel::Serializer.serializer_for(obj)
@ -249,6 +292,74 @@ module ActionController
assert_equal 'application/json', @response.content_type
assert_equal expected.to_json, @response.body
end
def test_render_with_fragment_only_cache_enable
ActionController::Base.cache_store.clear
get :render_fragment_changed_object_with_only_cache_enabled
response = JSON.parse(@response.body)
assert_equal 'application/json', @response.content_type
assert_equal 'ZOMG A ROLE', response["name"]
assert_equal 'HUEHUEBRBR', response["description"]
end
def test_render_with_fragment_except_cache_enable
ActionController::Base.cache_store.clear
get :render_fragment_changed_object_with_except_cache_enabled
response = JSON.parse(@response.body)
assert_equal 'application/json', @response.content_type
assert_equal 5, response["rating"]
assert_equal 'lol', response["content"]
end
def test_render_fragment_changed_object_with_relationship
ActionController::Base.cache_store.clear
get :render_fragment_changed_object_with_relationship
response = JSON.parse(@response.body)
expected_return = {
"post" => {
"id"=>1,
"title"=>"New Post",
"body"=>"Body"
},
"id"=>1,
"time"=>DateTime.now.to_s
}
assert_equal 'application/json', @response.content_type
assert_equal expected_return, response
end
def test_cache_expiration_on_update
ActionController::Base.cache_store.clear
get :render_object_with_cache_enabled
expected = {
id: 1,
title: 'ZOMG a New Post',
body: 'Body',
comments: [
{
id: 1,
body: 'ZOMG A COMMENT' }
],
blog: {
id:999,
name: "Custom blog"
},
author: {
id: 1,
name: 'Joao Moura.'
}
}
get :update_and_render_object_with_cache_enabled
assert_equal 'application/json', @response.content_type
assert_equal expected.to_json, @response.body
end
end
end
end

View File

@ -0,0 +1,27 @@
require 'test_helper'
module ActiveModel
class Serializer
class Adapter
class FragmentCacheTest < Minitest::Test
def setup
@author = Author.new(name: 'Joao M. D. Moura')
@role = Role.new(name: 'Great Author', description:nil)
@role.author = [@author]
@role_serializer = RoleSerializer.new(@role)
@role_hash = FragmentCache.new(RoleSerializer.adapter.new(@role_serializer), @role_serializer, {}, nil)
end
def test_fragment_fetch_with_virtual_attributes
expected_result = {
id: @role.id,
description: @role.description,
slug: "#{@role.name}-#{@role.id}",
name: @role.name
}
assert_equal(@role_hash.fetch, expected_result)
end
end
end
end
end

View File

@ -6,6 +6,7 @@ module ActiveModel
class Json
class HasManyTestTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@author = Author.new(id: 1, name: 'Steve K.')
@post = Post.new(title: 'New Post', body: 'Body')
@first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')

View File

@ -41,6 +41,7 @@ module ActiveModel
expected = [
{
id: "43",
rating: nil,
type: "bios",
content:"AMS Contributor",
links: {

View File

@ -1,11 +1,11 @@
require 'test_helper'
module ActiveModel
class Serializer
class Adapter
class JsonApi
class LinkedTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@author1 = Author.new(id: 1, name: 'Steve K.')
@author2 = Author.new(id: 2, name: 'Tenderlove')
@bio1 = Bio.new(id: 1, content: 'AMS Contributor')
@ -103,8 +103,9 @@ module ActiveModel
}
}, {
id: "1",
content: "AMS Contributor",
rating: nil,
type: "bios",
content: "AMS Contributor",
links: {
author: { linkage: { type: "authors", id: "1" } }
}
@ -119,8 +120,9 @@ module ActiveModel
}
}, {
id: "2",
content: "Rails Contributor",
rating: nil,
type: "bios",
content: "Rails Contributor",
links: {
author: { linkage: { type: "authors", id: "2" } }
}

View File

@ -5,6 +5,7 @@ module ActiveModel
class Adapter
class JsonTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@author = Author.new(id: 1, name: 'Steve K.')
@post = Post.new(title: 'New Post', body: 'Body')
@first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')

51
test/fixtures/poro.rb vendored
View File

@ -57,18 +57,22 @@ class ProfilePreviewSerializer < ActiveModel::Serializer
urls :posts, :comments
end
Post = Class.new(Model)
Comment = Class.new(Model)
Author = Class.new(Model)
Bio = Class.new(Model)
Blog = Class.new(Model)
Role = Class.new(Model)
Post = Class.new(Model)
Like = Class.new(Model)
Comment = Class.new(Model)
Author = Class.new(Model)
Bio = Class.new(Model)
Blog = Class.new(Model)
Role = Class.new(Model)
User = Class.new(Model)
Location = Class.new(Model)
Place = Class.new(Model)
module Spam; end
Spam::UnrelatedLink = Class.new(Model)
PostSerializer = Class.new(ActiveModel::Serializer) do
cache key:'post', expires_in: 0.05
cache key:'post', expires_in: 0.1
attributes :id, :title, :body
has_many :comments
@ -116,13 +120,42 @@ AuthorSerializer = Class.new(ActiveModel::Serializer) do
end
RoleSerializer = Class.new(ActiveModel::Serializer) do
attributes :id, :name
cache only: [:name]
attributes :id, :name, :description, :slug
def slug
"#{name}-#{id}"
end
belongs_to :author
end
LikeSerializer = Class.new(ActiveModel::Serializer) do
attributes :id, :time
belongs_to :post
end
LocationSerializer = Class.new(ActiveModel::Serializer) do
cache only: [:place]
attributes :id, :lat, :lng
belongs_to :place
def place
'Nowhere'
end
end
PlaceSerializer = Class.new(ActiveModel::Serializer) do
attributes :id, :name
has_many :locations
end
BioSerializer = Class.new(ActiveModel::Serializer) do
attributes :id, :content
cache except: [:content]
attributes :id, :content, :rating
belongs_to :author
end

View File

@ -3,21 +3,33 @@ module ActiveModel
class Serializer
class CacheTest < Minitest::Test
def setup
@post = Post.new({ title: 'New Post', body: 'Body' })
@comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
@author = Author.new(name: 'Joao M. D. Moura')
@role = Role.new(name: 'Great Author')
@author.posts = [@post]
@author.roles = [@role]
@author.bio = nil
@post.comments = [@comment]
@post.author = @author
@comment.post = @post
@comment.author = @author
ActionController::Base.cache_store.clear
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
@blog = Blog.new(id: 999, name: "Custom blog")
@post = Post.new(title: 'New Post', body: 'Body')
@bio = Bio.new(id: 1, content: 'AMS Contributor')
@author = Author.new(name: 'Joao M. D. Moura')
@role = Role.new(name: 'Great Author')
@location = Location.new(lat: '-23.550520', lng: '-46.633309')
@place = Place.new(name: 'Amazing Place')
@author.posts = [@post]
@author.roles = [@role]
@role.author = @author
@author.bio = @bio
@bio.author = @author
@post.comments = [@comment]
@post.author = @author
@comment.post = @post
@comment.author = @author
@post.blog = @blog
@location.place = @place
@post_serializer = PostSerializer.new(@post)
@author_serializer = AuthorSerializer.new(@author)
@comment_serializer = CommentSerializer.new(@comment)
@location_serializer = LocationSerializer.new(@location)
@bio_serializer = BioSerializer.new(@bio)
@role_serializer = RoleSerializer.new(@role)
@post_serializer = PostSerializer.new(@post)
@author_serializer = AuthorSerializer.new(@author)
@comment_serializer = CommentSerializer.new(@comment)
end
def test_cache_definition
@ -33,28 +45,82 @@ module ActiveModel
end
def test_cache_key_interpolation_with_updated_at
author = render_object_with_cache_without_cache_key(@author)
author = render_object_with_cache(@author)
assert_equal(nil, ActionController::Base.cache_store.fetch(@author.cache_key))
assert_equal(author, ActionController::Base.cache_store.fetch("#{@author_serializer.class._cache_key}/#{@author_serializer.object.id}-#{@author_serializer.object.updated_at}").to_json)
assert_equal(@author_serializer.attributes.to_json, ActionController::Base.cache_store.fetch("#{@author_serializer.class._cache_key}/#{@author_serializer.object.id}-#{@author_serializer.object.updated_at}").to_json)
end
def test_default_cache_key_fallback
comment = render_object_with_cache_without_cache_key(@comment)
assert_equal(comment, ActionController::Base.cache_store.fetch(@comment.cache_key).to_json)
comment = render_object_with_cache(@comment)
assert_equal(@comment_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(@comment.cache_key).to_json)
end
def test_cache_options_definition
assert_equal({expires_in: 0.05}, @post_serializer.class._cache_options)
assert_equal({expires_in: 0.1}, @post_serializer.class._cache_options)
assert_equal(nil, @author_serializer.class._cache_options)
assert_equal({expires_in: 1.day}, @comment_serializer.class._cache_options)
end
def test_fragment_cache_definition
assert_equal([:name], @role_serializer.class._cache_only)
assert_equal([:content], @bio_serializer.class._cache_except)
end
def test_associations_separately_cache
ActionController::Base.cache_store.clear
assert_equal(nil, ActionController::Base.cache_store.fetch(@post.cache_key))
assert_equal(nil, ActionController::Base.cache_store.fetch(@comment.cache_key))
post = render_object_with_cache(@post)
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key))
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(@comment.cache_key))
end
def test_associations_cache_when_updated
# Clean the Cache
ActionController::Base.cache_store.clear
# Generate a new Cache of Post object and each objects related to it.
render_object_with_cache(@post)
# Check if if cache the objects separately
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key))
assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(@comment.cache_key))
# Simulating update on comments relationship with Post
new_comment = Comment.new(id: 2, body: 'ZOMG A NEW COMMENT')
new_comment_serializer = CommentSerializer.new(new_comment)
@post.comments = [new_comment]
# Ask for the serialized object
render_object_with_cache(@post)
# Check if the the new comment was cached
assert_equal(new_comment_serializer.attributes, ActionController::Base.cache_store.fetch(new_comment.cache_key))
assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key))
end
def test_fragment_fetch_with_virtual_associations
expected_result = {
id: @location.id,
lat: @location.lat,
lng: @location.lng,
place: 'Nowhere'
}
hash = render_object_with_cache(@location)
assert_equal(hash, expected_result)
assert_equal({place: 'Nowhere'}, ActionController::Base.cache_store.fetch(@location.cache_key))
end
private
def render_object_with_cache_without_cache_key(obj)
def render_object_with_cache(obj)
serializer_class = ActiveModel::Serializer.serializer_for(obj)
serializer = serializer_class.new(obj)
adapter = ActiveModel::Serializer.adapter.new(serializer)
adapter.to_json
adapter.serializable_hash
end
end
end

View File

@ -4,6 +4,7 @@ module ActiveModel
class Serializer
class MetaTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@blog = Blog.new(id: 1,
name: 'AMS Hints',
writer: Author.new(id: 2, name: "Steve"),