From a2023b95170fffff40c0ddb91083c9353fa2eb39 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. --- CHANGELOG.md | 2 + README.md | 24 ++++ lib/active_model/serializer.rb | 52 ++++--- lib/active_model/serializer/adapter.rb | 46 +++++-- .../serializer/adapter/fragment_cache.rb | 78 +++++++++++ lib/active_model/serializer/adapter/json.rb | 44 ++++-- .../serializer/adapter/json/fragment_cache.rb | 15 ++ .../serializer/adapter/json_api.rb | 41 ++++-- .../adapter/json_api/fragment_cache.rb | 22 +++ .../action_controller/json_api_linked_test.rb | 4 + test/action_controller/serialization_test.rb | 129 ++++++++++++++++-- test/adapter/fragment_cache_test.rb | 27 ++++ test/adapter/json/has_many_test.rb | 1 + test/adapter/json_api/has_one_test.rb | 1 + test/adapter/json_api/linked_test.rb | 8 +- test/adapter/json_test.rb | 1 + test/fixtures/poro.rb | 51 +++++-- test/serializers/cache_test.rb | 110 ++++++++++++--- test/serializers/meta_test.rb | 1 + 19 files changed, 555 insertions(+), 102 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/CHANGELOG.md b/CHANGELOG.md index b2079e3ab..e07aa12ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] \ No newline at end of file diff --git a/README.md b/README.md index dae1b7e02..9808a6e2f 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 588abc99f..2abeebce7 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -10,41 +10,54 @@ class Serializer 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 @@ def self.root_name 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 @@ def attributes(options = {}) 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]) 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..93d02f65b --- /dev/null +++ b/lib/active_model/serializer/adapter/fragment_cache.rb @@ -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 \ 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..8a78ffbb9 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,45 @@ 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 + 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 +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 0f0ad59ae..cd8de8e2b 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 @@ -26,15 +28,17 @@ def serializable_hash(options = {}) 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 @@ def add_links(resource, name, serializers) 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 @@ def add_included(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) - 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 @@ def add_resource_links(attrs, serializer, options = {}) 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] 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 321974e39..d3a1f8447 100644 --- a/test/action_controller/json_api_linked_test.rb +++ b/test/action_controller/json_api_linked_test.rb @@ -111,6 +111,8 @@ def test_render_resource_with_nested_has_many_include "id" => "1", "type" => "roles", "name" => "admin", + "description" => nil, + "slug" => "admin-1", "links" => { "author" => { "linkage" => { "type" =>"authors", "id" => "1" } } } @@ -118,6 +120,8 @@ def test_render_resource_with_nested_has_many_include "id" => "2", "type" => "roles", "name" => "colab", + "description" => nil, + "slug" => "colab-2", "links" => { "author" => { "linkage" => { "type" =>"authors", "id" => "1" } } } diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index b47db5218..361a65a43 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -48,36 +48,79 @@ 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', 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 @@ 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: { + 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 diff --git a/test/adapter/fragment_cache_test.rb b/test/adapter/fragment_cache_test.rb new file mode 100644 index 000000000..693035576 --- /dev/null +++ b/test/adapter/fragment_cache_test.rb @@ -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 + 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 8847e0203..170caf84f 100644 --- a/test/adapter/json_api/has_one_test.rb +++ b/test/adapter/json_api/has_one_test.rb @@ -41,6 +41,7 @@ def test_includes_linked_bio expected = [ { id: "43", + rating: nil, type: "bios", content:"AMS Contributor", links: { diff --git a/test/adapter/json_api/linked_test.rb b/test/adapter/json_api/linked_test.rb index be228c470..b5b372e3b 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') @@ -103,8 +103,9 @@ def test_include_multiple_posts_and_linked_array } }, { id: "1", - content: "AMS Contributor", + rating: nil, type: "bios", + content: "AMS Contributor", links: { author: { linkage: { type: "authors", id: "1" } } } @@ -119,8 +120,9 @@ def test_include_multiple_posts_and_linked_array } }, { id: "2", - content: "Rails Contributor", + rating: nil, type: "bios", + content: "Rails Contributor", links: { author: { linkage: { type: "authors", id: "2" } } } 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 6ce52e443..b66395c37 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -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 @@ def custom_options 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 diff --git a/test/serializers/cache_test.rb b/test/serializers/cache_test.rb index 6377fa950..89c62a212 100644 --- a/test/serializers/cache_test.rb +++ b/test/serializers/cache_test.rb @@ -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 - - @post_serializer = PostSerializer.new(@post) - @author_serializer = AuthorSerializer.new(@author) - @comment_serializer = CommentSerializer.new(@comment) + 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 + + @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 @@ 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 + + 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 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"),