From 4ea1b9887970214f09ae71cc04ae1ad3122de196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Tue, 3 Feb 2015 21:57:02 -0200 Subject: [PATCH] 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. --- README.md | 24 ++++ lib/active_model/serializer.rb | 12 +- lib/active_model/serializer/adapter.rb | 46 +++++-- .../serializer/adapter/fragment_cache.rb | 74 +++++++++++ lib/active_model/serializer/adapter/json.rb | 42 ++++-- .../serializer/adapter/json/fragment_cache.rb | 15 +++ .../serializer/adapter/json_api.rb | 31 +++-- .../adapter/json_api/fragment_cache.rb | 22 ++++ .../action_controller/json_api_linked_test.rb | 2 + test/action_controller/serialization_test.rb | 122 +++++++++++++++++- test/adapter/fragment_cache_test.rb | 26 ++++ test/adapter/json/has_many_test.rb | 1 + test/adapter/json_api/has_one_test.rb | 2 +- test/adapter/json_api/linked_test.rb | 3 +- test/adapter/json_test.rb | 1 + test/fixtures/poro.rb | 16 ++- test/serializers/cache_test.rb | 82 +++++++++--- test/serializers/meta_test.rb | 1 + 18 files changed, 451 insertions(+), 71 deletions(-) create mode 100644 lib/active_model/serializer/adapter/fragment_cache.rb create mode 100644 lib/active_model/serializer/adapter/json/fragment_cache.rb create mode 100644 lib/active_model/serializer/adapter/json_api/fragment_cache.rb create mode 100644 test/adapter/fragment_cache_test.rb diff --git a/README.md b/README.md index 4f75004a3..8776451e0 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index b61934005..a9a38a268 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -10,20 +10,25 @@ class Serializer class << self attr_accessor :_attributes + attr_accessor :_attributes_keys attr_accessor :_associations attr_accessor :_urls attr_accessor :_cache 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| @@ -35,6 +40,7 @@ def self.attributes(*attrs) 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) @@ -43,8 +49,10 @@ def self.attribute(attr, options = {}) # 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 diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 85b014639..74a6eab0c 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -1,3 +1,5 @@ +require 'active_model/serializer/adapter/fragment_cache' + module ActiveModel class Serializer class Adapter @@ -32,8 +34,38 @@ def self.adapter_class(adapter) "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) + @serializer = serializer + @klass = serializer.class + if is_cached? + @klass._cache.fetch(cache_key, @klass._cache_options) do + yield + end + elsif is_fragment_cached? + FragmentCache.new(self, @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}/#{@serializer.object.id}-#{@serializer.object.updated_at}" : @serializer.object.cache_key + end + def meta serializer.meta if serializer.respond_to?(:meta) end @@ -50,20 +82,6 @@ def include_meta(json) 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 diff --git a/lib/active_model/serializer/adapter/fragment_cache.rb b/lib/active_model/serializer/adapter/fragment_cache.rb new file mode 100644 index 000000000..fdf4abc66 --- /dev/null +++ b/lib/active_model/serializer/adapter/fragment_cache.rb @@ -0,0 +1,74 @@ +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_and_association(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) + + serializers = {cached: cached, non_cached: non_cached} + cached_attributes_and_association(klass, serializers) + serializers + end + end + end + end +end \ No newline at end of file diff --git a/lib/active_model/serializer/adapter/json.rb b/lib/active_model/serializer/adapter/json.rb index 8848f8fbf..617a79522 100644 --- a/lib/active_model/serializer/adapter/json.rb +++ b/lib/active_model/serializer/adapter/json.rb @@ -1,3 +1,5 @@ +require 'active_model/serializer/adapter/json/fragment_cache' + module ActiveModel class Serializer class Adapter @@ -6,31 +8,43 @@ def serializable_hash(options = {}) 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 + 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 +end \ No newline at end of file diff --git a/lib/active_model/serializer/adapter/json/fragment_cache.rb b/lib/active_model/serializer/adapter/json/fragment_cache.rb new file mode 100644 index 000000000..761a6e548 --- /dev/null +++ b/lib/active_model/serializer/adapter/json/fragment_cache.rb @@ -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 \ No newline at end of file diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index f604b67fa..ab9dfe317 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -1,3 +1,5 @@ +require 'active_model/serializer/adapter/json_api/fragment_cache' + module ActiveModel class Serializer class Adapter @@ -23,15 +25,17 @@ def serializable_hash(options = {}) self.class.new(s, @options.merge(top: @top, fieldset: @fieldset)).serializable_hash[@root] end else - @hash = cached_object do - @hash[@root] = attributes_for_serializer(serializer, @options) - add_resource_links(@hash[@root], serializer) - @hash - end + @hash[@root] = attributes_for_serializer(serializer, @options) + add_resource_links(@hash[@root], 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) @@ -91,22 +95,25 @@ def add_linked(resource_name, serializers, parent = nil) 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) - attributes = object.attributes(options) - attributes[:id] = attributes[:id].to_s if attributes[:id] - result << attributes + result << cache_check(object) do + attributes = object.attributes(options) + attributes[:id] = attributes[:id].to_s if attributes[:id] + result << attributes + end end else options[:fields] = @fieldset && @fieldset.fields_for(serializer) - result = serializer.attributes(options) - result[:id] = result[:id].to_s if result[:id] + result = cache_check(serializer) do + result = serializer.attributes(options) + result[:id] = result[:id].to_s if result[:id] + result + end end - result end diff --git a/lib/active_model/serializer/adapter/json_api/fragment_cache.rb b/lib/active_model/serializer/adapter/json_api/fragment_cache.rb new file mode 100644 index 000000000..75630b619 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/fragment_cache.rb @@ -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 \ No newline at end of file diff --git a/test/action_controller/json_api_linked_test.rb b/test/action_controller/json_api_linked_test.rb index 7d4dcbf85..e1c1d9f62 100644 --- a/test/action_controller/json_api_linked_test.rb +++ b/test/action_controller/json_api_linked_test.rb @@ -110,12 +110,14 @@ def test_render_resource_with_nested_has_many_include "roles"=>[{ "id" => "1", "name" => "admin", + "description" => nil, "links" => { "author" => "1" } }, { "id" => "2", "name" => "colab", + "description" => nil, "links" => { "author" => "1" } diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index a10cb039b..e20561b36 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -51,14 +51,21 @@ def render_array_using_implicit_serializer_and_meta 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', blog:nil, 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 @@ -69,7 +76,7 @@ def render_object_expired_with_cache_enabled generate_cached_serializer(post) post.title = 'ZOMG a New Post' - sleep 0.05 + sleep 0.1 render json: post end @@ -81,6 +88,42 @@ def render_changed_object_with_cache_enabled 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', blog:nil, body: 'Body', comments: [comment], author: author }) + post2 = Post.new({ id: 1, title: 'New Post2', blog:nil, 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) @@ -206,6 +249,71 @@ def test_render_with_cache_enable_and_expired 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: nil, + 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 diff --git a/test/adapter/fragment_cache_test.rb b/test/adapter/fragment_cache_test.rb new file mode 100644 index 000000000..951f5230a --- /dev/null +++ b/test/adapter/fragment_cache_test.rb @@ -0,0 +1,26 @@ +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) + @fragment_cache = FragmentCache.new(RoleSerializer.adapter.new(@role_serializer), @role_serializer, {}, nil) + end + + def test_fragment_fetch + expected_result = { + id: @role.id, + description: @role.description, + name: @role.name + } + assert_equal(@fragment_cache.fetch, expected_result) + end + end + end + end +end + diff --git a/test/adapter/json/has_many_test.rb b/test/adapter/json/has_many_test.rb index c5679e681..b73af9f53 100644 --- a/test/adapter/json/has_many_test.rb +++ b/test/adapter/json/has_many_test.rb @@ -6,6 +6,7 @@ class Adapter 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') diff --git a/test/adapter/json_api/has_one_test.rb b/test/adapter/json_api/has_one_test.rb index 247bb2f96..3cd0940fd 100644 --- a/test/adapter/json_api/has_one_test.rb +++ b/test/adapter/json_api/has_one_test.rb @@ -35,7 +35,7 @@ def test_includes_bio_id def test_includes_linked_bio @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'bio') - assert_equal([{id: "43", :content=>"AMS Contributor", :links=>{:author=>"1"}}], @adapter.serializable_hash[:linked][:bios]) + assert_equal([{id: "43", :rating=>nil, :content=>"AMS Contributor", :links=>{:author=>"1"}}], @adapter.serializable_hash[:linked][:bios]) end end end diff --git a/test/adapter/json_api/linked_test.rb b/test/adapter/json_api/linked_test.rb index 728972397..3ad8d012d 100644 --- a/test/adapter/json_api/linked_test.rb +++ b/test/adapter/json_api/linked_test.rb @@ -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') @@ -176,6 +176,7 @@ def test_include_multiple_posts_and_linked } ] } + assert_equal expected, adapter.serializable_hash[:linked] assert_equal expected, alt_adapter.serializable_hash[:linked] end diff --git a/test/adapter/json_test.rb b/test/adapter/json_test.rb index 5795174eb..52c9d8fb4 100644 --- a/test/adapter/json_test.rb +++ b/test/adapter/json_test.rb @@ -5,6 +5,7 @@ class Serializer 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') diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index d04d79dd5..a9d2dc850 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -58,6 +58,7 @@ class ProfilePreviewSerializer < ActiveModel::Serializer end Post = Class.new(Model) +Like = Class.new(Model) Comment = Class.new(Model) Author = Class.new(Model) Bio = Class.new(Model) @@ -67,7 +68,7 @@ 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 @@ -115,13 +116,22 @@ def custom_options end RoleSerializer = Class.new(ActiveModel::Serializer) do - attributes :id, :name + cache only: [:name] + attributes :id, :name, :description belongs_to :author end +LikeSerializer = Class.new(ActiveModel::Serializer) do + cache only: [:post] + attributes :id, :time + + belongs_to :post +end + BioSerializer = Class.new(ActiveModel::Serializer) do - attributes :id, :content + cache except: [:content] + attributes :id, :content, :rating belongs_to :author end diff --git a/test/serializers/cache_test.rb b/test/serializers/cache_test.rb index 6377fa950..92980c6b8 100644 --- a/test/serializers/cache_test.rb +++ b/test/serializers/cache_test.rb @@ -3,18 +3,26 @@ 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') + @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 + @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) @@ -33,24 +41,64 @@ def test_cache_key_definition 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 + 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) diff --git a/test/serializers/meta_test.rb b/test/serializers/meta_test.rb index b226c13a8..4494d70f8 100644 --- a/test/serializers/meta_test.rb +++ b/test/serializers/meta_test.rb @@ -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"),