From f7c77c1256fa07b79fbe977147ed629be29be613 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Sun, 2 Aug 2015 18:02:56 -0300 Subject: [PATCH 01/20] add feature to include pagination links in response --- .gitignore | 1 + README.md | 12 ++++ lib/active_model/serializer/adapter.rb | 21 ++++++- .../serializer/array_serializer.rb | 4 +- lib/active_model/serializer/pagination.rb | 63 +++++++++++++++++++ lib/active_model_serializers.rb | 1 + 6 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 lib/active_model/serializer/pagination.rb diff --git a/.gitignore b/.gitignore index 0374e060e..ad7f37d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ test/version_tmp tmp *.swp .ruby-version +.ruby-gemset diff --git a/README.md b/README.md index 15e869472..ce21fa8e7 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,18 @@ And you can change the JSON key that the serializer should use for a particular The `url` declaration describes which named routes to use while generating URLs for your JSON. Not every adapter will require URLs. +## Pagination + +If you want pagination links in your response, specify it in the `render` + +```ruby + render json: @posts, pagination: true +``` + +AMS relies on either Kaminari or WillPaginate. Please install either dependency by adding one of those to your Gemfile. + +Pagination links will only be included in your response if you are using an Adapter that supports `root`, as JsonAPI and Json adapters, the default adapter (FlattenJson) doesn't have `root`. + ## Caching To cache a serializer, call ```cache``` and pass its options. diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 0b8118d60..79256a588 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -21,9 +21,9 @@ def serializable_hash(options = nil) end def as_json(options = nil) - hash = serializable_hash(options) - include_meta(hash) - hash + serializable_hash(options).tap do |hash| + include_meta(hash) + end end def self.create(resource, options = {}) @@ -94,6 +94,21 @@ def include_meta(json) json[meta_key] = meta if meta json end + + def include_pagination_links(json) + return unless page_links + + links?(json) ? json.merge!(page_links) : json['links'] = page_links + json + end + + def page_links + @links ||= serializer.page_links + end + + def links?(json) + !json['links'].nil? + end end end end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index f2f916e57..7a8d14ac2 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -4,8 +4,9 @@ class ArraySerializer NoSerializerError = Class.new(StandardError) include Enumerable delegate :each, to: :@objects + delegate :page_links, to: :pagination - attr_reader :root, :meta, :meta_key + attr_reader :root, :meta, :meta_key, :pagination def initialize(objects, options = {}) @root = options[:root] @@ -24,6 +25,7 @@ def initialize(objects, options = {}) end @meta = options[:meta] @meta_key = options[:meta_key] + @pagination = ActiveModel::Serializer::Pagination.new(objects) if options[:pagination] end def json_key diff --git a/lib/active_model/serializer/pagination.rb b/lib/active_model/serializer/pagination.rb new file mode 100644 index 000000000..a9bbba592 --- /dev/null +++ b/lib/active_model/serializer/pagination.rb @@ -0,0 +1,63 @@ +module ActiveModel + class Serializer + class Pagination + attr_reader :collection + + def initialize(collection) + @collection = collection + end + + def page_links + send(default_adapter) + end + + private + + def kaminari + build_links collection.size + end + + def will_paginate + setup_will_paginate + build_links collection.per_page + end + + def build_links(per_page) + pages = pages_from.each_with_object({}) do |(key, value), hash| + hash[key] = "?page=#{value}&per_page=#{per_page}" + end + { pages: pages } unless pages.empty? + end + + def pages_from + return {} if collection.total_pages == 1 + + {}.tap do |pages| + unless collection.first_page? + pages[:first] = 1 + pages[:prev] = collection.current_page - 1 + end + + unless collection.last_page? + pages[:next] = collection.current_page + 1 + pages[:last] = collection.total_pages + end + end + end + + def default_adapter + return :kaminari if defined?(Kaminari) + return :will_paginate if defined?(WillPaginate::CollectionMethods) + raise "AMS relies on either Kaminari or WillPaginate." + + "Please install either dependency by adding one of those to your Gemfile" + end + + def setup_will_paginate + WillPaginate::CollectionMethods.module_eval do + def first_page?() !previous_page end + def last_page?() !next_page end + end + end + end + end +end diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index 211a2870e..bf5f5d566 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -2,6 +2,7 @@ require 'active_model/serializer/version' require 'active_model/serializer' require 'active_model/serializer/fieldset' +require 'active_model/serializer/pagination' require 'active_model/serializable_resource' begin From b864302695c17358fb88ecbb52fff7c55c2fdecd Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Thu, 6 Aug 2015 14:19:52 -0300 Subject: [PATCH 02/20] remove 'page object' on paginations links' --- lib/active_model/serializer/pagination.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/active_model/serializer/pagination.rb b/lib/active_model/serializer/pagination.rb index a9bbba592..639eaeedb 100644 --- a/lib/active_model/serializer/pagination.rb +++ b/lib/active_model/serializer/pagination.rb @@ -23,10 +23,9 @@ def will_paginate end def build_links(per_page) - pages = pages_from.each_with_object({}) do |(key, value), hash| + pages_from.each_with_object({}) do |(key, value), hash| hash[key] = "?page=#{value}&per_page=#{per_page}" end - { pages: pages } unless pages.empty? end def pages_from From 1fe8b069863b9601a238fcc4fb387ee307566ae6 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Fri, 7 Aug 2015 16:59:44 -0300 Subject: [PATCH 03/20] exchange pagination class to inside json_api scope --- README.md | 2 +- lib/active_model/serializer/adapter.rb | 15 ----- .../serializer/adapter/json_api.rb | 17 +++++ .../adapter/json_api/pagination_links.rb | 52 ++++++++++++++++ .../serializer/array_serializer.rb | 5 +- lib/active_model/serializer/pagination.rb | 62 ------------------- lib/active_model_serializers.rb | 1 - test/array_serializer_test.rb | 9 +++ 8 files changed, 81 insertions(+), 82 deletions(-) create mode 100644 lib/active_model/serializer/adapter/json_api/pagination_links.rb delete mode 100644 lib/active_model/serializer/pagination.rb diff --git a/README.md b/README.md index ce21fa8e7..45361ea86 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ If you want pagination links in your response, specify it in the `render` AMS relies on either Kaminari or WillPaginate. Please install either dependency by adding one of those to your Gemfile. -Pagination links will only be included in your response if you are using an Adapter that supports `root`, as JsonAPI and Json adapters, the default adapter (FlattenJson) doesn't have `root`. +Pagination links will only be included in your response if you are using a JsonAPI adapter, the others adapters doesn't have this feature. ## Caching diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 79256a588..7a48942d0 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -94,21 +94,6 @@ def include_meta(json) json[meta_key] = meta if meta json end - - def include_pagination_links(json) - return unless page_links - - links?(json) ? json.merge!(page_links) : json['links'] = page_links - json - end - - def page_links - @links ||= serializer.page_links - end - - def links?(json) - !json['links'].nil? - end end end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index a7e0dedee..54de24f08 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -1,4 +1,5 @@ require 'active_model/serializer/adapter/json_api/fragment_cache' +require 'active_model/serializer/adapter/json_api/pagination_links' module ActiveModel class Serializer @@ -27,6 +28,8 @@ def serializable_hash(options = nil) @hash[:included] |= result[:included] end end + + include_pagination_links if serializer.pagination else @hash[:data] = attributes_for_serializer(serializer, options) add_resource_relationships(@hash[:data], serializer) @@ -157,6 +160,20 @@ def add_resource_relationships(attrs, serializer, options = {}) end end end + + def include_pagination_links + return if page_links.empty? + + links? ? @hash[:links].merge!(page_links) : @hash[:links] = page_links + end + + def page_links + @links ||= JsonApi::PaginationLinks.new(serializer.resource).page_links + end + + def links? + !@hash[:links].nil? + end end end end diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb new file mode 100644 index 000000000..3ebc1c51e --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -0,0 +1,52 @@ +module ActiveModel + class Serializer + class Adapter + class JsonApi < Adapter + class PaginationLinks + FIRST_PAGE = 1 + + attr_reader :collection + + def initialize(collection) + raise_unless_any_gem_installed + @collection = collection + end + + def page_links + build_links + end + + private + + def build_links + pages_from.each_with_object({}) do |(key, value), hash| + hash[key] = "?page=#{value}&per_page=#{collection.size}" + end + end + + def pages_from + return {} if collection.total_pages == FIRST_PAGE + + {}.tap do |pages| + unless collection.current_page == FIRST_PAGE + pages[:first] = FIRST_PAGE + pages[:prev] = collection.current_page - FIRST_PAGE + end + + unless collection.current_page == collection.total_pages + pages[:next] = collection.current_page + FIRST_PAGE + pages[:last] = collection.total_pages + end + end + end + + def raise_unless_any_gem_installed + return if defined?(WillPaginate) || defined?(Kaminari) + raise "AMS relies on either Kaminari or WillPaginate." + + "Please install either dependency by adding one of those to your Gemfile" + end + end + end + end + end +end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index 7a8d14ac2..8417d9e27 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -4,9 +4,8 @@ class ArraySerializer NoSerializerError = Class.new(StandardError) include Enumerable delegate :each, to: :@objects - delegate :page_links, to: :pagination - attr_reader :root, :meta, :meta_key, :pagination + attr_reader :root, :meta, :meta_key, :pagination, :resource def initialize(objects, options = {}) @root = options[:root] @@ -25,7 +24,7 @@ def initialize(objects, options = {}) end @meta = options[:meta] @meta_key = options[:meta_key] - @pagination = ActiveModel::Serializer::Pagination.new(objects) if options[:pagination] + @pagination = options[:pagination] end def json_key diff --git a/lib/active_model/serializer/pagination.rb b/lib/active_model/serializer/pagination.rb deleted file mode 100644 index 639eaeedb..000000000 --- a/lib/active_model/serializer/pagination.rb +++ /dev/null @@ -1,62 +0,0 @@ -module ActiveModel - class Serializer - class Pagination - attr_reader :collection - - def initialize(collection) - @collection = collection - end - - def page_links - send(default_adapter) - end - - private - - def kaminari - build_links collection.size - end - - def will_paginate - setup_will_paginate - build_links collection.per_page - end - - def build_links(per_page) - pages_from.each_with_object({}) do |(key, value), hash| - hash[key] = "?page=#{value}&per_page=#{per_page}" - end - end - - def pages_from - return {} if collection.total_pages == 1 - - {}.tap do |pages| - unless collection.first_page? - pages[:first] = 1 - pages[:prev] = collection.current_page - 1 - end - - unless collection.last_page? - pages[:next] = collection.current_page + 1 - pages[:last] = collection.total_pages - end - end - end - - def default_adapter - return :kaminari if defined?(Kaminari) - return :will_paginate if defined?(WillPaginate::CollectionMethods) - raise "AMS relies on either Kaminari or WillPaginate." + - "Please install either dependency by adding one of those to your Gemfile" - end - - def setup_will_paginate - WillPaginate::CollectionMethods.module_eval do - def first_page?() !previous_page end - def last_page?() !next_page end - end - end - end - end -end diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index bf5f5d566..211a2870e 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -2,7 +2,6 @@ require 'active_model/serializer/version' require 'active_model/serializer' require 'active_model/serializer/fieldset' -require 'active_model/serializer/pagination' require 'active_model/serializable_resource' begin diff --git a/test/array_serializer_test.rb b/test/array_serializer_test.rb index 3eff3ef8a..60caf9bc2 100644 --- a/test/array_serializer_test.rb +++ b/test/array_serializer_test.rb @@ -92,6 +92,15 @@ def test_json_key_with_root_and_no_serializers serializer = ArraySerializer.new(build_named_collection, root: 'custom_root') assert_equal serializer.json_key, 'custom_roots' end + + def test_pagination_attr_readers + serializer = ArraySerializer.new(@resource, pagination: true) + assert_equal serializer.pagination, true + end + + def test_resource + assert_equal @serializer.resource, @resource + end end end end From e040d6fcce4f9feac59b68534a67b32bbf3a1bbd Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Sat, 8 Aug 2015 11:26:20 -0300 Subject: [PATCH 04/20] add action test to pagination links --- active_model_serializers.gemspec | 2 + .../action_controller/json_api/linked_test.rb | 180 ++++++++++++++++++ .../json_api/pagination_test.rb | 92 +++++++++ .../action_controller/json_api_linked_test.rb | 179 ----------------- 4 files changed, 274 insertions(+), 179 deletions(-) create mode 100644 test/action_controller/json_api/linked_test.rb create mode 100644 test/action_controller/json_api/pagination_test.rb delete mode 100644 test/action_controller/json_api_linked_test.rb diff --git a/active_model_serializers.gemspec b/active_model_serializers.gemspec index a0279e3d6..eff27ca92 100644 --- a/active_model_serializers.gemspec +++ b/active_model_serializers.gemspec @@ -24,4 +24,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.6" spec.add_development_dependency "timecop", ">= 0.7" spec.add_development_dependency "rake" + spec.add_development_dependency "kaminari" + spec.add_development_dependency "will_paginate" end diff --git a/test/action_controller/json_api/linked_test.rb b/test/action_controller/json_api/linked_test.rb new file mode 100644 index 000000000..a3422fa58 --- /dev/null +++ b/test/action_controller/json_api/linked_test.rb @@ -0,0 +1,180 @@ +require 'test_helper' + +module ActionController + module Serialization + class JsonApi + class LinkedTest < ActionController::TestCase + class LinkedTestController < ActionController::Base + def setup_post + ActionController::Base.cache_store.clear + @role1 = Role.new(id: 1, name: 'admin') + @role2 = Role.new(id: 2, name: 'colab') + @author = Author.new(id: 1, name: 'Steve K.') + @author.posts = [] + @author.bio = nil + @author.roles = [@role1, @role2] + @role1.author = @author + @role2.author = @author + @author2 = Author.new(id: 2, name: 'Anonymous') + @author2.posts = [] + @author2.bio = nil + @author2.roles = [] + @post = Post.new(id: 1, title: 'New Post', body: 'Body') + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @post.author = @author + @first_comment.post = @post + @first_comment.author = @author2 + @second_comment.post = @post + @second_comment.author = nil + @post2 = Post.new(id: 2, title: "Another Post", body: "Body") + @post2.author = @author + @post2.comments = [] + @blog = Blog.new(id: 1, name: "My Blog!!") + @post.blog = @blog + @post2.blog = @blog + end + + def render_resource_without_include + setup_post + render json: @post, adapter: :json_api + end + + def render_resource_with_include + setup_post + render json: @post, include: 'author', adapter: :json_api + end + + def render_resource_with_nested_include + setup_post + render json: @post, include: 'comments.author', adapter: :json_api + end + + def render_resource_with_nested_has_many_include + setup_post + render json: @post, include: ['author', 'author.roles'], adapter: :json_api + end + + def render_resource_with_missing_nested_has_many_include + setup_post + @post.author = @author2 # author2 has no roles. + render json: @post, include: 'author,author.roles', adapter: :json_api + end + + def render_collection_with_missing_nested_has_many_include + setup_post + @post.author = @author2 + render json: [@post, @post2], include: 'author,author.roles', adapter: :json_api + end + + def render_collection_without_include + setup_post + render json: [@post], adapter: :json_api + end + + def render_collection_with_include + setup_post + render json: [@post], include: ['author', 'comments'], adapter: :json_api + end + end + + tests LinkedTestController + + def test_render_resource_without_include + get :render_resource_without_include + response = JSON.parse(@response.body) + refute response.key? 'included' + end + + def test_render_resource_with_include + get :render_resource_with_include + response = JSON.parse(@response.body) + assert response.key? 'included' + assert_equal 1, response['included'].size + assert_equal 'Steve K.', response['included'].first['attributes']['name'] + end + + def test_render_resource_with_nested_has_many_include + get :render_resource_with_nested_has_many_include + response = JSON.parse(@response.body) + expected_linked = [ + { + "id" => "1", + "type" => "authors", + "attributes" => { + "name" => "Steve K." + }, + "relationships" => { + "posts" => { "data" => [] }, + "roles" => { "data" => [{ "type" =>"roles", "id" => "1" }, { "type" =>"roles", "id" => "2" }] }, + "bio" => { "data" => nil } + } + }, { + "id" => "1", + "type" => "roles", + "attributes" => { + "name" => "admin", + "description" => nil, + "slug" => "admin-1" + }, + "relationships" => { + "author" => { "data" => { "type" =>"authors", "id" => "1" } } + } + }, { + "id" => "2", + "type" => "roles", + "attributes" => { + "name" => "colab", + "description" => nil, + "slug" => "colab-2" + }, + "relationships" => { + "author" => { "data" => { "type" =>"authors", "id" => "1" } } + } + } + ] + assert_equal expected_linked, response['included'] + end + + def test_render_resource_with_nested_include + get :render_resource_with_nested_include + response = JSON.parse(@response.body) + assert response.key? 'included' + assert_equal 1, response['included'].size + assert_equal 'Anonymous', response['included'].first['attributes']['name'] + end + + def test_render_collection_without_include + get :render_collection_without_include + response = JSON.parse(@response.body) + refute response.key? 'included' + end + + def test_render_collection_with_include + get :render_collection_with_include + response = JSON.parse(@response.body) + assert response.key? 'included' + end + + def test_render_resource_with_nested_attributes_even_when_missing_associations + get :render_resource_with_missing_nested_has_many_include + response = JSON.parse(@response.body) + assert response.key? 'included' + refute has_type?(response['included'], 'roles') + end + + def test_render_collection_with_missing_nested_has_many_include + get :render_collection_with_missing_nested_has_many_include + response = JSON.parse(@response.body) + assert response.key? 'included' + assert has_type?(response['included'], 'roles') + end + + def has_type?(collection, value) + collection.detect { |i| i['type'] == value} + end + end + end + end +end diff --git a/test/action_controller/json_api/pagination_test.rb b/test/action_controller/json_api/pagination_test.rb new file mode 100644 index 000000000..1db50fe9c --- /dev/null +++ b/test/action_controller/json_api/pagination_test.rb @@ -0,0 +1,92 @@ +require 'test_helper' +require 'will_paginate/array' +require 'kaminari' +require 'kaminari/hooks' +::Kaminari::Hooks.init + +module ActionController + module Serialization + class JsonApi + class PaginationTest < ActionController::TestCase + class PaginationTestController < ActionController::Base + def setup + @array = [ + Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }), + Profile.new({ name: 'Name 2', description: 'Description 2', comments: 'Comments 2' }), + Profile.new({ name: 'Name 3', description: 'Description 3', comments: 'Comments 3' }) + ] + end + + def using_kaminari + setup + Kaminari.paginate_array(@array).page(params[:page]).per(params[:per_page]) + end + + def using_will_paginate + setup + @array.paginate(page: params[:page], per_page: params[:per_page]) + end + + def render_pagination_using_kaminari + render json: using_kaminari, adapter: :json_api, pagination: true + end + + def render_pagination_using_will_paginate + render json: using_will_paginate, adapter: :json_api, pagination: true + end + + def render_array_without_pagination_links + render json: using_will_paginate, adapter: :json_api, pagination: false + end + + def render_array_omitting_pagination_options + render json: using_kaminari, adapter: :json_api + end + end + + tests PaginationTestController + + def test_render_pagination_links_with_will_paginate + expected_links = {"first"=>"?page=1&per_page=1", "prev"=>"?page=1&per_page=1", "next"=>"?page=3&per_page=1", "last"=>"?page=3&per_page=1"} + + get :render_pagination_using_will_paginate, page: 2, per_page: 1 + response = JSON.parse(@response.body) + assert_equal expected_links, response['links'] + end + + def test_render_only_last_and_next_pagination_links + expected_links = {"next"=>"?page=2&per_page=2", "last"=>"?page=2&per_page=2"} + get :render_pagination_using_will_paginate, page: 1, per_page: 2 + response = JSON.parse(@response.body) + assert_equal expected_links, response['links'] + end + + def test_render_pagination_links_with_kaminari + expected_links = {"first"=>"?page=1&per_page=1", "prev"=>"?page=1&per_page=1", "next"=>"?page=3&per_page=1", "last"=>"?page=3&per_page=1"} + get :render_pagination_using_kaminari, page: 2, per_page: 1 + response = JSON.parse(@response.body) + assert_equal expected_links, response['links'] + end + + def test_render_only_prev_and_first_pagination_links + expected_links = {"first"=>"?page=1&per_page=1", "prev"=>"?page=2&per_page=1"} + get :render_pagination_using_kaminari, page: 3, per_page: 1 + response = JSON.parse(@response.body) + assert_equal expected_links, response['links'] + end + + def test_array_without_pagination_links + get :render_array_without_pagination_links + response = JSON.parse(@response.body) + refute response.key? 'links' + end + + def test_array_omitting_pagination_options + get :render_array_omitting_pagination_options + response = JSON.parse(@response.body) + refute response.key? 'links' + end + end + end + end +end diff --git a/test/action_controller/json_api_linked_test.rb b/test/action_controller/json_api_linked_test.rb deleted file mode 100644 index dab35c560..000000000 --- a/test/action_controller/json_api_linked_test.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'test_helper' - -module ActionController - module Serialization - class JsonApiLinkedTest < ActionController::TestCase - class JsonApiLinkedTestController < ActionController::Base - def setup_post - ActionController::Base.cache_store.clear - @role1 = Role.new(id: 1, name: 'admin') - @role2 = Role.new(id: 2, name: 'colab') - @author = Author.new(id: 1, name: 'Steve K.') - @author.posts = [] - @author.bio = nil - @author.roles = [@role1, @role2] - @role1.author = @author - @role2.author = @author - @author2 = Author.new(id: 2, name: 'Anonymous') - @author2.posts = [] - @author2.bio = nil - @author2.roles = [] - @post = Post.new(id: 1, title: 'New Post', body: 'Body') - @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') - @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') - @post.comments = [@first_comment, @second_comment] - @post.author = @author - @first_comment.post = @post - @first_comment.author = @author2 - @second_comment.post = @post - @second_comment.author = nil - @post2 = Post.new(id: 2, title: "Another Post", body: "Body") - @post2.author = @author - @post2.comments = [] - @blog = Blog.new(id: 1, name: "My Blog!!") - @post.blog = @blog - @post2.blog = @blog - end - - def render_resource_without_include - setup_post - render json: @post, adapter: :json_api - end - - def render_resource_with_include - setup_post - render json: @post, include: 'author', adapter: :json_api - end - - def render_resource_with_nested_include - setup_post - render json: @post, include: 'comments.author', adapter: :json_api - end - - def render_resource_with_nested_has_many_include - setup_post - render json: @post, include: ['author', 'author.roles'], adapter: :json_api - end - - def render_resource_with_missing_nested_has_many_include - setup_post - @post.author = @author2 # author2 has no roles. - render json: @post, include: 'author,author.roles', adapter: :json_api - end - - def render_collection_with_missing_nested_has_many_include - setup_post - @post.author = @author2 - render json: [@post, @post2], include: 'author,author.roles', adapter: :json_api - end - - def render_collection_without_include - setup_post - render json: [@post], adapter: :json_api - end - - def render_collection_with_include - setup_post - render json: [@post], include: ['author', 'comments'], adapter: :json_api - end - end - - tests JsonApiLinkedTestController - - def test_render_resource_without_include - get :render_resource_without_include - response = JSON.parse(@response.body) - refute response.key? 'included' - end - - def test_render_resource_with_include - get :render_resource_with_include - response = JSON.parse(@response.body) - assert response.key? 'included' - assert_equal 1, response['included'].size - assert_equal 'Steve K.', response['included'].first['attributes']['name'] - end - - def test_render_resource_with_nested_has_many_include - get :render_resource_with_nested_has_many_include - response = JSON.parse(@response.body) - expected_linked = [ - { - "id" => "1", - "type" => "authors", - "attributes" => { - "name" => "Steve K." - }, - "relationships" => { - "posts" => { "data" => [] }, - "roles" => { "data" => [{ "type" =>"roles", "id" => "1" }, { "type" =>"roles", "id" => "2" }] }, - "bio" => { "data" => nil } - } - }, { - "id" => "1", - "type" => "roles", - "attributes" => { - "name" => "admin", - "description" => nil, - "slug" => "admin-1" - }, - "relationships" => { - "author" => { "data" => { "type" =>"authors", "id" => "1" } } - } - }, { - "id" => "2", - "type" => "roles", - "attributes" => { - "name" => "colab", - "description" => nil, - "slug" => "colab-2" - }, - "relationships" => { - "author" => { "data" => { "type" =>"authors", "id" => "1" } } - } - } - ] - assert_equal expected_linked, response['included'] - end - - def test_render_resource_with_nested_include - get :render_resource_with_nested_include - response = JSON.parse(@response.body) - assert response.key? 'included' - assert_equal 1, response['included'].size - assert_equal 'Anonymous', response['included'].first['attributes']['name'] - end - - def test_render_collection_without_include - get :render_collection_without_include - response = JSON.parse(@response.body) - refute response.key? 'included' - end - - def test_render_collection_with_include - get :render_collection_with_include - response = JSON.parse(@response.body) - assert response.key? 'included' - end - - def test_render_resource_with_nested_attributes_even_when_missing_associations - get :render_resource_with_missing_nested_has_many_include - response = JSON.parse(@response.body) - assert response.key? 'included' - refute has_type?(response['included'], 'roles') - end - - def test_render_collection_with_missing_nested_has_many_include - get :render_collection_with_missing_nested_has_many_include - response = JSON.parse(@response.body) - assert response.key? 'included' - assert has_type?(response['included'], 'roles') - end - - def has_type?(collection, value) - collection.detect { |i| i['type'] == value} - end - - end - end -end From 331218d1c39619f5272c661cdc19367ab083e426 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Sat, 8 Aug 2015 12:10:12 -0300 Subject: [PATCH 05/20] add test to class of pagination links --- .../adapter/json_api/pagination_links_test.rb | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 test/adapter/json_api/pagination_links_test.rb diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb new file mode 100644 index 000000000..94f4632d9 --- /dev/null +++ b/test/adapter/json_api/pagination_links_test.rb @@ -0,0 +1,85 @@ +require 'test_helper' +require 'will_paginate/array' +require 'kaminari' +require 'kaminari/hooks' +::Kaminari::Hooks.init + +module ActiveModel + class Serializer + class Adapter + class JsonApi + class PaginationLinksTest < Minitest::Test + def setup + ActionController::Base.cache_store.clear + @array = [ + Profile.new({ id: 1, name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }), + Profile.new({ id: 2, name: 'Name 2', description: 'Description 2', comments: 'Comments 2' }), + Profile.new({ id: 3, name: 'Name 3', description: 'Description 3', comments: 'Comments 3' }) + ] + end + + def using_kaminari + Kaminari.paginate_array(@array).page(2).per(1) + end + + def using_will_paginate + @array.paginate(page: 2, per_page: 1) + end + + def expected_response_without_pagination_links + { + data: [{ + id:"2", + type:"profiles", + attributes:{ + name:"Name 2", + description:"Description 2" + } + }] + } + end + + def expected_response_with_pagination_links + { + data: [{ + id:"2", + type:"profiles", + attributes:{ + name:"Name 2", + description:"Description 2" + } + }], + links:{ + first:"?page=1&per_page=1", + prev:"?page=1&per_page=1", + next:"?page=3&per_page=1", + last:"?page=3&per_page=1" + } + } + end + + def test_pagination_links_using_kaminari + serializer = ArraySerializer.new(using_kaminari, pagination: true) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + + assert_equal expected_response_with_pagination_links, adapter.serializable_hash + end + + def test_pagination_links_using_will_paginate + serializer = ArraySerializer.new(using_will_paginate, pagination: true) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + + assert_equal expected_response_with_pagination_links, adapter.serializable_hash + end + + def test_not_showing_pagination_links + serializer = ArraySerializer.new(using_will_paginate, pagination: false) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + + assert_equal expected_response_without_pagination_links, adapter.serializable_hash + end + end + end + end + end +end From acb6545c508b931d90df2382bf13595692b401a0 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Sat, 8 Aug 2015 16:25:22 -0300 Subject: [PATCH 06/20] add documentation to pagination feature --- README.md | 12 --------- docs/README.md | 1 + docs/howto/add_pagination_links.md | 39 ++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 docs/howto/add_pagination_links.md diff --git a/README.md b/README.md index 45361ea86..15e869472 100644 --- a/README.md +++ b/README.md @@ -273,18 +273,6 @@ And you can change the JSON key that the serializer should use for a particular The `url` declaration describes which named routes to use while generating URLs for your JSON. Not every adapter will require URLs. -## Pagination - -If you want pagination links in your response, specify it in the `render` - -```ruby - render json: @posts, pagination: true -``` - -AMS relies on either Kaminari or WillPaginate. Please install either dependency by adding one of those to your Gemfile. - -Pagination links will only be included in your response if you are using a JsonAPI adapter, the others adapters doesn't have this feature. - ## Caching To cache a serializer, call ```cache``` and pass its options. diff --git a/docs/README.md b/docs/README.md index ddecb3e45..2398528b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ This is the documentation of AMS, it's focused on the **0.10.x version.** ## How to - [How to add root key](howto/add_root_key.md) +- [How to add pagination links](howto/add_pagination_links.md) (```JSON-API``` only) ## Getting Help diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md new file mode 100644 index 000000000..0445903df --- /dev/null +++ b/docs/howto/add_pagination_links.md @@ -0,0 +1,39 @@ +# How to add pagination links + +If you want pagination links in your response, specify it in the `render` + +```ruby + render json: @posts, pagination: true +``` + +AMS relies on either `Kaminari` or `WillPaginate`. Please install either dependency by adding one of those to your Gemfile. + +Pagination links will only be included in your response if you are using a ```JSON-API``` adapter, the others adapters doesn't have this feature. + +```ruby +ActiveModel::Serializer.config.adapter = :json_api +``` + +ex: +```json +{ + "data": [ + { + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON API paints my bikeshed!", + "body": "The shortest article. Ever.", + "created": "2015-05-22T14:56:29.000Z", + "updated": "2015-05-22T14:56:28.000Z" + } + } + ], + "links": { + "first": "?page=1&per_page=1", + "prev": "?page=2&per_page=1", + "next": "?page=4&per_page=1", + "last": "?page=13&per_page=1" + } +} +``` From 36c452e60bae38d40ed6328e71c08b38a54b616f Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Sat, 8 Aug 2015 17:30:48 -0300 Subject: [PATCH 07/20] add pagination feature to changelog file --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f1e822e..b70c2e9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,5 @@ * adds cache support to attributes and associations [@joaomdmoura] * uses model name to determine the type [@lsylvester] * remove root key option and split JSON adapter [@joaomdmoura] - * adds FlattenJSON as default adapter [@joaomdmoura] \ No newline at end of file + * adds FlattenJSON as default adapter [@joaomdmoura] + * adds support for `pagination links` at top level of JsonApi adapter [@bacarini] From e62a7d6f34e83d2334c9871ac2648eb440647d6e Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Mon, 10 Aug 2015 11:04:48 -0300 Subject: [PATCH 08/20] return complete URIs on pagination links --- docs/howto/add_pagination_links.md | 8 ++++---- lib/action_controller/serialization.rb | 5 +++++ lib/active_model/serializer/adapter/json_api.rb | 4 ++-- .../adapter/json_api/pagination_links.rb | 16 +++++++++++++--- lib/active_model/serializer/array_serializer.rb | 4 ++-- .../json_api/pagination_test.rb | 16 ++++++++++++---- test/action_controller/serialization_test.rb | 6 ++++++ test/adapter/json_api/pagination_links_test.rb | 12 ++++++------ test/array_serializer_test.rb | 2 +- 9 files changed, 51 insertions(+), 22 deletions(-) diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index 0445903df..960723c4f 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -30,10 +30,10 @@ ex: } ], "links": { - "first": "?page=1&per_page=1", - "prev": "?page=2&per_page=1", - "next": "?page=4&per_page=1", - "last": "?page=13&per_page=1" + "first": "http://example.com/articles?page=1&per_page=1", + "prev": "http://example.com/articles?page=2&per_page=1", + "next": "http://example.com/articles?page=4&per_page=1", + "last": "http://example.com/articles?page=13&per_page=1" } } ``` diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index b216f0681..b3b843229 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -25,6 +25,7 @@ def get_serializer(resource, options = {}) "Please pass 'adapter: false' or see ActiveSupport::SerializableResource#serialize" options[:adapter] = false end + options[:original_url] = original_url ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource| if serializable_resource.serializer? serializable_resource.serialization_scope ||= serialization_scope @@ -57,5 +58,9 @@ def serialization_scope(scope) self._serialization_scope = scope end end + + def original_url + request.original_url.sub(/\?.*$/, "") + end end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 54de24f08..e30c9bf04 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -29,7 +29,7 @@ def serializable_hash(options = nil) end end - include_pagination_links if serializer.pagination + include_pagination_links if serializer.options[:pagination] else @hash[:data] = attributes_for_serializer(serializer, options) add_resource_relationships(@hash[:data], serializer) @@ -168,7 +168,7 @@ def include_pagination_links end def page_links - @links ||= JsonApi::PaginationLinks.new(serializer.resource).page_links + @links ||= JsonApi::PaginationLinks.new(serializer.resource, serializer.options).page_links end def links? diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index 3ebc1c51e..dd4c90c10 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -5,11 +5,12 @@ class JsonApi < Adapter class PaginationLinks FIRST_PAGE = 1 - attr_reader :collection + attr_reader :collection, :options - def initialize(collection) + def initialize(collection, options={}) raise_unless_any_gem_installed @collection = collection + @options = options end def page_links @@ -20,7 +21,7 @@ def page_links def build_links pages_from.each_with_object({}) do |(key, value), hash| - hash[key] = "?page=#{value}&per_page=#{collection.size}" + hash[key] = "#{url}?page=#{value}&per_page=#{collection.size}" end end @@ -45,6 +46,15 @@ def raise_unless_any_gem_installed raise "AMS relies on either Kaminari or WillPaginate." + "Please install either dependency by adding one of those to your Gemfile" end + + def url + return default_url unless options && options[:links] && options[:links][:self] + options[:links][:self] + end + + def default_url + options[:original_url] + end end end end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index 8417d9e27..b97dcd8c5 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -5,7 +5,7 @@ class ArraySerializer include Enumerable delegate :each, to: :@objects - attr_reader :root, :meta, :meta_key, :pagination, :resource + attr_reader :root, :meta, :meta_key, :options, :resource def initialize(objects, options = {}) @root = options[:root] @@ -24,7 +24,7 @@ def initialize(objects, options = {}) end @meta = options[:meta] @meta_key = options[:meta_key] - @pagination = options[:pagination] + @options = options end def json_key diff --git a/test/action_controller/json_api/pagination_test.rb b/test/action_controller/json_api/pagination_test.rb index 1db50fe9c..c8a3647d8 100644 --- a/test/action_controller/json_api/pagination_test.rb +++ b/test/action_controller/json_api/pagination_test.rb @@ -47,7 +47,10 @@ def render_array_omitting_pagination_options tests PaginationTestController def test_render_pagination_links_with_will_paginate - expected_links = {"first"=>"?page=1&per_page=1", "prev"=>"?page=1&per_page=1", "next"=>"?page=3&per_page=1", "last"=>"?page=3&per_page=1"} + expected_links = {"first"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=1&per_page=1", + "prev"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=1&per_page=1", + "next"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=3&per_page=1", + "last"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=3&per_page=1"} get :render_pagination_using_will_paginate, page: 2, per_page: 1 response = JSON.parse(@response.body) @@ -55,21 +58,26 @@ def test_render_pagination_links_with_will_paginate end def test_render_only_last_and_next_pagination_links - expected_links = {"next"=>"?page=2&per_page=2", "last"=>"?page=2&per_page=2"} + expected_links = {"next"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=2&per_page=2", + "last"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=2&per_page=2"} get :render_pagination_using_will_paginate, page: 1, per_page: 2 response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_pagination_links_with_kaminari - expected_links = {"first"=>"?page=1&per_page=1", "prev"=>"?page=1&per_page=1", "next"=>"?page=3&per_page=1", "last"=>"?page=3&per_page=1"} + expected_links = {"first"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=1&per_page=1", + "prev"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=1&per_page=1", + "next"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=3&per_page=1", + "last"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=3&per_page=1"} get :render_pagination_using_kaminari, page: 2, per_page: 1 response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_only_prev_and_first_pagination_links - expected_links = {"first"=>"?page=1&per_page=1", "prev"=>"?page=2&per_page=1"} + expected_links = {"first"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=1&per_page=1", + "prev"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=2&per_page=1"} get :render_pagination_using_kaminari, page: 3, per_page: 1 response = JSON.parse(@response.body) assert_equal expected_links, response['links'] diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index 8c89ceac0..d60cdf55f 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -404,6 +404,9 @@ def test_warn_overridding_use_adapter_as_falsy_on_controller_instance def use_adapter? false end + def original_url + "http://example.com/" + end }.new assert_match /adapter: false/, (capture(:stderr) { controller.get_serializer(Profile.new) @@ -415,6 +418,9 @@ def test_dont_warn_overridding_use_adapter_as_truthy_on_controller_instance def use_adapter? true end + def original_url + "http://example.com/" + end }.new assert_equal "", (capture(:stderr) { controller.get_serializer(Profile.new) diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 94f4632d9..7c26c192b 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -50,23 +50,23 @@ def expected_response_with_pagination_links } }], links:{ - first:"?page=1&per_page=1", - prev:"?page=1&per_page=1", - next:"?page=3&per_page=1", - last:"?page=3&per_page=1" + first: "http://example.com?page=1&per_page=1", + prev: "http://example.com?page=1&per_page=1", + next: "http://example.com?page=3&per_page=1", + last: "http://example.com?page=3&per_page=1" } } end def test_pagination_links_using_kaminari - serializer = ArraySerializer.new(using_kaminari, pagination: true) + serializer = ArraySerializer.new(using_kaminari, pagination: true, original_url: "http://example.com") adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) assert_equal expected_response_with_pagination_links, adapter.serializable_hash end def test_pagination_links_using_will_paginate - serializer = ArraySerializer.new(using_will_paginate, pagination: true) + serializer = ArraySerializer.new(using_will_paginate, pagination: true, original_url: "http://example.com") adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) assert_equal expected_response_with_pagination_links, adapter.serializable_hash diff --git a/test/array_serializer_test.rb b/test/array_serializer_test.rb index 60caf9bc2..b5eb9384b 100644 --- a/test/array_serializer_test.rb +++ b/test/array_serializer_test.rb @@ -95,7 +95,7 @@ def test_json_key_with_root_and_no_serializers def test_pagination_attr_readers serializer = ArraySerializer.new(@resource, pagination: true) - assert_equal serializer.pagination, true + assert_equal serializer.options[:pagination], true end def test_resource From 7be25fef14f04054a8728acee3dae2086c621e0e Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Mon, 10 Aug 2015 12:26:16 -0300 Subject: [PATCH 09/20] include query_parameters on pagination links as well --- docs/howto/add_pagination_links.md | 8 ++-- lib/action_controller/serialization.rb | 9 +++- .../adapter/json_api/pagination_links.rb | 15 +++---- .../json_api/pagination_test.rb | 35 ++++++++++------ test/action_controller/serialization_test.rb | 6 --- .../adapter/json_api/pagination_links_test.rb | 42 ++++++++++++------- 6 files changed, 71 insertions(+), 44 deletions(-) diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index 960723c4f..971e3ee83 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -30,10 +30,10 @@ ex: } ], "links": { - "first": "http://example.com/articles?page=1&per_page=1", - "prev": "http://example.com/articles?page=2&per_page=1", - "next": "http://example.com/articles?page=4&per_page=1", - "last": "http://example.com/articles?page=13&per_page=1" + "first": "http://example.com/articles?page[number]=1&page[size]=1", + "prev": "http://example.com/articles?page[number]=2&page[size]=1", + "next": "http://example.com/articles?page[number]=4&page[size]=1", + "last": "http://example.com/articles?page[number]=13&page[size]=1" } } ``` diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index b3b843229..21aaad48f 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -25,7 +25,10 @@ def get_serializer(resource, options = {}) "Please pass 'adapter: false' or see ActiveSupport::SerializableResource#serialize" options[:adapter] = false end - options[:original_url] = original_url + if options[:pagination] + options[:original_url] = original_url + options[:query_parameters] = query_parameters + end ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource| if serializable_resource.serializer? serializable_resource.serialization_scope ||= serialization_scope @@ -62,5 +65,9 @@ def serialization_scope(scope) def original_url request.original_url.sub(/\?.*$/, "") end + + def query_parameters + request.query_parameters + end end end diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index dd4c90c10..bbae19eaf 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -14,17 +14,14 @@ def initialize(collection, options={}) end def page_links - build_links - end - - private - - def build_links pages_from.each_with_object({}) do |(key, value), hash| - hash[key] = "#{url}?page=#{value}&per_page=#{collection.size}" + params = query_parameters.merge(page: { number: value, size: collection.size }).to_query + hash[key] = "#{url}?#{params}" end end + private + def pages_from return {} if collection.total_pages == FIRST_PAGE @@ -55,6 +52,10 @@ def url def default_url options[:original_url] end + + def query_parameters + options[:query_parameters] ? options[:query_parameters] : {} + end end end end diff --git a/test/action_controller/json_api/pagination_test.rb b/test/action_controller/json_api/pagination_test.rb index c8a3647d8..41d3ed6fa 100644 --- a/test/action_controller/json_api/pagination_test.rb +++ b/test/action_controller/json_api/pagination_test.rb @@ -8,6 +8,9 @@ module ActionController module Serialization class JsonApi class PaginationTest < ActionController::TestCase + KAMINARI_URI = 'http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari' + WILL_PAGINATE_URI = 'http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate' + class PaginationTestController < ActionController::Base def setup @array = [ @@ -47,10 +50,10 @@ def render_array_omitting_pagination_options tests PaginationTestController def test_render_pagination_links_with_will_paginate - expected_links = {"first"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=1&per_page=1", - "prev"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=1&per_page=1", - "next"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=3&per_page=1", - "last"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=3&per_page=1"} + expected_links = {"first"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", + "prev"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", + "next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1", + "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1"} get :render_pagination_using_will_paginate, page: 2, per_page: 1 response = JSON.parse(@response.body) @@ -58,31 +61,39 @@ def test_render_pagination_links_with_will_paginate end def test_render_only_last_and_next_pagination_links - expected_links = {"next"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=2&per_page=2", - "last"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_will_paginate?page=2&per_page=2"} + expected_links = {"next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2", + "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2"} get :render_pagination_using_will_paginate, page: 1, per_page: 2 response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_pagination_links_with_kaminari - expected_links = {"first"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=1&per_page=1", - "prev"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=1&per_page=1", - "next"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=3&per_page=1", - "last"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=3&per_page=1"} + expected_links = {"first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", + "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", + "next"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1", + "last"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1"} get :render_pagination_using_kaminari, page: 2, per_page: 1 response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_only_prev_and_first_pagination_links - expected_links = {"first"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=1&per_page=1", - "prev"=>"http://test.host/action_controller/serialization/json_api/pagination_test/pagination_test/render_pagination_using_kaminari?page=2&per_page=1"} + expected_links = {"first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", + "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1&per_page=1"} get :render_pagination_using_kaminari, page: 3, per_page: 1 response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end + def test_render_only_last_and_next_pagination_links_with_additional_params + expected_links = {"next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2&teste=additional", + "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2&teste=additional"} + get :render_pagination_using_will_paginate, page: 1, per_page: 2, teste: "additional" + response = JSON.parse(@response.body) + assert_equal expected_links, response['links'] + end + def test_array_without_pagination_links get :render_array_without_pagination_links response = JSON.parse(@response.body) diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index d60cdf55f..8c89ceac0 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -404,9 +404,6 @@ def test_warn_overridding_use_adapter_as_falsy_on_controller_instance def use_adapter? false end - def original_url - "http://example.com/" - end }.new assert_match /adapter: false/, (capture(:stderr) { controller.get_serializer(Profile.new) @@ -418,9 +415,6 @@ def test_dont_warn_overridding_use_adapter_as_truthy_on_controller_instance def use_adapter? true end - def original_url - "http://example.com/" - end }.new assert_equal "", (capture(:stderr) { controller.get_serializer(Profile.new) diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 7c26c192b..1ccfd8a66 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -26,7 +26,7 @@ def using_will_paginate @array.paginate(page: 2, per_page: 1) end - def expected_response_without_pagination_links + def data { data: [{ id:"2", @@ -39,25 +39,30 @@ def expected_response_without_pagination_links } end - def expected_response_with_pagination_links + def links { - data: [{ - id:"2", - type:"profiles", - attributes:{ - name:"Name 2", - description:"Description 2" - } - }], links:{ - first: "http://example.com?page=1&per_page=1", - prev: "http://example.com?page=1&per_page=1", - next: "http://example.com?page=3&per_page=1", - last: "http://example.com?page=3&per_page=1" + first: "http://example.com?page%5Bnumber%5D=1&page%5Bsize%5D=1", + prev: "http://example.com?page%5Bnumber%5D=1&page%5Bsize%5D=1", + next: "http://example.com?page%5Bnumber%5D=3&page%5Bsize%5D=1", + last: "http://example.com?page%5Bnumber%5D=3&page%5Bsize%5D=1" } } end + def expected_response_without_pagination_links + data + end + + def expected_response_with_pagination_links + data.merge links + end + + def expected_response_with_pagination_links_and_additional_params + new_links = links[:links].each_with_object({}) {|(key, value), hash| hash[key] = "#{value}&teste=teste" } + data.merge links: new_links + end + def test_pagination_links_using_kaminari serializer = ArraySerializer.new(using_kaminari, pagination: true, original_url: "http://example.com") adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) @@ -72,6 +77,15 @@ def test_pagination_links_using_will_paginate assert_equal expected_response_with_pagination_links, adapter.serializable_hash end + def test_pagination_links_with_additional_params + serializer = ArraySerializer.new(using_will_paginate, pagination: true, + original_url: "http://example.com", + query_parameters: { teste: "teste"}) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + + assert_equal expected_response_with_pagination_links_and_additional_params, adapter.serializable_hash + end + def test_not_showing_pagination_links serializer = ArraySerializer.new(using_will_paginate, pagination: false) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) From e0d050d2afbddbed5f6672c8d53e6e7e380ebb55 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Tue, 11 Aug 2015 14:10:50 -0300 Subject: [PATCH 10/20] remove resource and options attr_reader from array_serialize --- lib/active_model/serializable_resource.rb | 2 +- .../serializer/adapter/json_api.rb | 22 ++++++--------- .../adapter/json_api/pagination_links.rb | 25 ++++++----------- .../serializer/array_serializer.rb | 3 +- .../adapter/json_api/pagination_links_test.rb | 28 ++++++++++--------- test/array_serializer_test.rb | 9 ------ 6 files changed, 35 insertions(+), 54 deletions(-) diff --git a/lib/active_model/serializable_resource.rb b/lib/active_model/serializable_resource.rb index fa3fbe035..9115b3967 100644 --- a/lib/active_model/serializable_resource.rb +++ b/lib/active_model/serializable_resource.rb @@ -2,7 +2,7 @@ module ActiveModel class SerializableResource - ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter]) + ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :pagination]) def initialize(resource, options = {}) @resource = resource diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index e30c9bf04..02a0ff8f1 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -20,7 +20,7 @@ def serializable_hash(options = nil) options ||= {} if serializer.respond_to?(:each) serializer.each do |s| - result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash(options) + result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash({}) @hash[:data] << result[:data] if result[:included] @@ -29,9 +29,9 @@ def serializable_hash(options = nil) end end - include_pagination_links if serializer.options[:pagination] + add_links(options) else - @hash[:data] = attributes_for_serializer(serializer, options) + @hash[:data] = attributes_for_serializer(serializer, {}) add_resource_relationships(@hash[:data], serializer) end @hash @@ -161,18 +161,14 @@ def add_resource_relationships(attrs, serializer, options = {}) end end - def include_pagination_links - return if page_links.empty? - - links? ? @hash[:links].merge!(page_links) : @hash[:links] = page_links - end - - def page_links - @links ||= JsonApi::PaginationLinks.new(serializer.resource, serializer.options).page_links + def add_links(options) + links = @hash.fetch(:links) { {} } + resources = serializer.instance_variable_get(:@resource) + @hash[:links] = add_pagination_links(links, resources, options) if @options[:pagination] end - def links? - !@hash[:links].nil? + def add_pagination_links(links, resources, options) + links.update(JsonApi::PaginationLinks.new(resources).serializable_hash(options)) end end end diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index bbae19eaf..add1a8bd2 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -5,18 +5,19 @@ class JsonApi < Adapter class PaginationLinks FIRST_PAGE = 1 - attr_reader :collection, :options + attr_reader :collection - def initialize(collection, options={}) + def initialize(collection) raise_unless_any_gem_installed @collection = collection - @options = options end - def page_links + def serializable_hash(options = {}) pages_from.each_with_object({}) do |(key, value), hash| + query_parameters = options.fetch(:query_parameters) { {} } params = query_parameters.merge(page: { number: value, size: collection.size }).to_query - hash[key] = "#{url}?#{params}" + + hash[key] = "#{url(options)}?#{params}" end end @@ -44,17 +45,9 @@ def raise_unless_any_gem_installed "Please install either dependency by adding one of those to your Gemfile" end - def url - return default_url unless options && options[:links] && options[:links][:self] - options[:links][:self] - end - - def default_url - options[:original_url] - end - - def query_parameters - options[:query_parameters] ? options[:query_parameters] : {} + def url(options) + self_link = options.fetch(:links) {{}} + self_link.fetch(:self) {} ? options[:links][:self] : options[:original_url] end end end diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index b97dcd8c5..f2f916e57 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -5,7 +5,7 @@ class ArraySerializer include Enumerable delegate :each, to: :@objects - attr_reader :root, :meta, :meta_key, :options, :resource + attr_reader :root, :meta, :meta_key def initialize(objects, options = {}) @root = options[:root] @@ -24,7 +24,6 @@ def initialize(objects, options = {}) end @meta = options[:meta] @meta_key = options[:meta_key] - @options = options end def json_key diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 1ccfd8a66..cd394a7a0 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -64,31 +64,33 @@ def expected_response_with_pagination_links_and_additional_params end def test_pagination_links_using_kaminari - serializer = ArraySerializer.new(using_kaminari, pagination: true, original_url: "http://example.com") - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + serializer = ArraySerializer.new(using_kaminari) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) - assert_equal expected_response_with_pagination_links, adapter.serializable_hash + assert_equal expected_response_with_pagination_links, + adapter.serializable_hash(original_url: "http://example.com") end def test_pagination_links_using_will_paginate - serializer = ArraySerializer.new(using_will_paginate, pagination: true, original_url: "http://example.com") - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + serializer = ArraySerializer.new(using_will_paginate) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) - assert_equal expected_response_with_pagination_links, adapter.serializable_hash + assert_equal expected_response_with_pagination_links, + adapter.serializable_hash(original_url: "http://example.com") end def test_pagination_links_with_additional_params - serializer = ArraySerializer.new(using_will_paginate, pagination: true, - original_url: "http://example.com", - query_parameters: { teste: "teste"}) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + serializer = ArraySerializer.new(using_will_paginate) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) + assert_equal expected_response_with_pagination_links_and_additional_params, + adapter.serializable_hash(original_url: "http://example.com", + query_parameters: { teste: "teste"}) - assert_equal expected_response_with_pagination_links_and_additional_params, adapter.serializable_hash end def test_not_showing_pagination_links - serializer = ArraySerializer.new(using_will_paginate, pagination: false) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + serializer = ArraySerializer.new(using_will_paginate) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: false) assert_equal expected_response_without_pagination_links, adapter.serializable_hash end diff --git a/test/array_serializer_test.rb b/test/array_serializer_test.rb index b5eb9384b..3eff3ef8a 100644 --- a/test/array_serializer_test.rb +++ b/test/array_serializer_test.rb @@ -92,15 +92,6 @@ def test_json_key_with_root_and_no_serializers serializer = ArraySerializer.new(build_named_collection, root: 'custom_root') assert_equal serializer.json_key, 'custom_roots' end - - def test_pagination_attr_readers - serializer = ArraySerializer.new(@resource, pagination: true) - assert_equal serializer.options[:pagination], true - end - - def test_resource - assert_equal @serializer.resource, @resource - end end end end From 77a8f66ad849ee986cfdf218aa18d1d3cc555f5e Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Tue, 11 Aug 2015 16:58:36 -0300 Subject: [PATCH 11/20] fix message on raise of pagination links class --- .../serializer/adapter/json_api/pagination_links.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index add1a8bd2..38f882d9f 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -41,8 +41,10 @@ def pages_from def raise_unless_any_gem_installed return if defined?(WillPaginate) || defined?(Kaminari) - raise "AMS relies on either Kaminari or WillPaginate." + - "Please install either dependency by adding one of those to your Gemfile" + raise <<-EOF + AMS relies on either Kaminari or WillPaginate for pagination. + Please install either dependency by adding one of those to your Gemfile. + EOF end def url(options) From 59ae84baba32c9f8de3c3dfa027da3aa2710345f Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Wed, 12 Aug 2015 11:51:04 -0300 Subject: [PATCH 12/20] exchange to a faster regex to get origina_url --- lib/action_controller/serialization.rb | 2 +- lib/active_model/serializer/adapter/json_api.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index 21aaad48f..99f860f57 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -63,7 +63,7 @@ def serialization_scope(scope) end def original_url - request.original_url.sub(/\?.*$/, "") + request.original_url[/\A[^?]+/] end def query_parameters diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 02a0ff8f1..ceb69487f 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -20,7 +20,7 @@ def serializable_hash(options = nil) options ||= {} if serializer.respond_to?(:each) serializer.each do |s| - result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash({}) + result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash(options) @hash[:data] << result[:data] if result[:included] @@ -31,7 +31,7 @@ def serializable_hash(options = nil) add_links(options) else - @hash[:data] = attributes_for_serializer(serializer, {}) + @hash[:data] = attributes_for_serializer(serializer, options) add_resource_relationships(@hash[:data], serializer) end @hash From a41d90cce46c426712417bc4821fc9dfab7c6133 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Fri, 14 Aug 2015 14:03:08 -0300 Subject: [PATCH 13/20] add self to pagination links --- docs/howto/add_pagination_links.md | 3 +- .../adapter/json_api/pagination_links.rb | 2 + .../json_api/pagination_test.rb | 51 ++++++++++--------- .../adapter/json_api/pagination_links_test.rb | 1 + 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index 971e3ee83..eca749294 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -20,7 +20,7 @@ ex: "data": [ { "type": "articles", - "id": "1", + "id": "3", "attributes": { "title": "JSON API paints my bikeshed!", "body": "The shortest article. Ever.", @@ -30,6 +30,7 @@ ex: } ], "links": { + "self": "http://example.com/articles?page[number]=3&page[size]=1", "first": "http://example.com/articles?page[number]=1&page[size]=1", "prev": "http://example.com/articles?page[number]=2&page[size]=1", "next": "http://example.com/articles?page[number]=4&page[size]=1", diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index 38f882d9f..4244721e2 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -27,6 +27,8 @@ def pages_from return {} if collection.total_pages == FIRST_PAGE {}.tap do |pages| + pages[:self] = collection.current_page + unless collection.current_page == FIRST_PAGE pages[:first] = FIRST_PAGE pages[:prev] = collection.current_page - FIRST_PAGE diff --git a/test/action_controller/json_api/pagination_test.rb b/test/action_controller/json_api/pagination_test.rb index 41d3ed6fa..cf7832a85 100644 --- a/test/action_controller/json_api/pagination_test.rb +++ b/test/action_controller/json_api/pagination_test.rb @@ -22,12 +22,12 @@ def setup def using_kaminari setup - Kaminari.paginate_array(@array).page(params[:page]).per(params[:per_page]) + Kaminari.paginate_array(@array).page(params[:page][:number]).per(params[:page][:size]) end def using_will_paginate setup - @array.paginate(page: params[:page], per_page: params[:per_page]) + @array.paginate(page: params[:page][:number], per_page: params[:page][:size]) end def render_pagination_using_kaminari @@ -50,58 +50,63 @@ def render_array_omitting_pagination_options tests PaginationTestController def test_render_pagination_links_with_will_paginate - expected_links = {"first"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", - "prev"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", - "next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1", - "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1"} + expected_links = { "self"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1", + "first"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + "prev"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + "next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1", + "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1"} - get :render_pagination_using_will_paginate, page: 2, per_page: 1 + get :render_pagination_using_will_paginate, page: { number: 2, size: 1 } response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_only_last_and_next_pagination_links - expected_links = {"next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2", - "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2"} - get :render_pagination_using_will_paginate, page: 1, per_page: 2 + expected_links = { "self"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=2", + "next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2", + "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2"} + get :render_pagination_using_will_paginate, page: { number: 1, size: 2 } response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_pagination_links_with_kaminari - expected_links = {"first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", - "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", - "next"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1", - "last"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&per_page=1"} - get :render_pagination_using_kaminari, page: 2, per_page: 1 + expected_links = { "self"=>"#{KAMINARI_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1", + "first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + "next"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1", + "last"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1"} + get :render_pagination_using_kaminari, page: { number: 2, size: 1 } response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_only_prev_and_first_pagination_links - expected_links = {"first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&per_page=1", - "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1&per_page=1"} - get :render_pagination_using_kaminari, page: 3, per_page: 1 + expected_links = { "self"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1", + "first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1"} + get :render_pagination_using_kaminari, page: { number: 3, size: 1 } response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_render_only_last_and_next_pagination_links_with_additional_params - expected_links = {"next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2&teste=additional", - "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&per_page=2&teste=additional"} - get :render_pagination_using_will_paginate, page: 1, per_page: 2, teste: "additional" + expected_links = { "self"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=2&teste=additional", + "next"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&teste=additional", + "last"=>"#{WILL_PAGINATE_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=2&teste=additional"} + get :render_pagination_using_will_paginate, page: { number: 1, size: 2 }, teste: "additional" response = JSON.parse(@response.body) assert_equal expected_links, response['links'] end def test_array_without_pagination_links - get :render_array_without_pagination_links + get :render_array_without_pagination_links, page: { number: 2, size: 1 } response = JSON.parse(@response.body) refute response.key? 'links' end def test_array_omitting_pagination_options - get :render_array_omitting_pagination_options + get :render_array_omitting_pagination_options, page: { number: 2, size: 1 } response = JSON.parse(@response.body) refute response.key? 'links' end diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index cd394a7a0..198662fdb 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -42,6 +42,7 @@ def data def links { links:{ + self: "http://example.com?page%5Bnumber%5D=2&page%5Bsize%5D=1", first: "http://example.com?page%5Bnumber%5D=1&page%5Bsize%5D=1", prev: "http://example.com?page%5Bnumber%5D=1&page%5Bsize%5D=1", next: "http://example.com?page%5Bnumber%5D=3&page%5Bsize%5D=1", From 2c2f948fa0ea5e2fb05940c636c9f078a09a33b6 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Mon, 17 Aug 2015 14:25:59 -0300 Subject: [PATCH 14/20] Add pagination links automatically Pagination links will be included in your response automatically as long as the resource is paginated using Kaminari or WillPaginate and if you are using a JSON-API adapter. The others adapters does not have this feature. --- README.md | 7 +++- docs/howto/add_pagination_links.md | 18 +++++--- lib/action_controller/serialization.rb | 7 ++-- lib/active_model/serializable_resource.rb | 2 +- .../serializer/adapter/json_api.rb | 2 +- .../adapter/json_api/pagination_links.rb | 4 +- .../json_api/pagination_test.rb | 17 ++------ .../adapter/json_api/pagination_links_test.rb | 42 ++++++++++--------- 8 files changed, 52 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 15e869472..ef103ba16 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ If you wish to use a serializer other than the default, you can explicitly pass render json: @posts, each_serializer: PostPreviewSerializer # Or, you can explicitly provide the collection serializer as well -render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer +render json: @posts, serializer: CollectionSerializer, each_serializer: PostPreviewSerializer ``` ### Meta @@ -272,6 +272,11 @@ And you can change the JSON key that the serializer should use for a particular The `url` declaration describes which named routes to use while generating URLs for your JSON. Not every adapter will require URLs. +## Pagination + +Pagination links will be included in your response automatically as long as the resource is paginated using [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate) and if you are using a ```JSON-API``` adapter. The others adapters does not have this feature. + +For more information about it, please see in our docs [How to add pagination links](https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md) ## Caching diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index eca749294..d49095b8c 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -1,14 +1,18 @@ # How to add pagination links -If you want pagination links in your response, specify it in the `render` +Pagination links will be included in your response automatically as long as the resource is paginated and if you are using a ```JSON-API``` adapter. The others adapters does not have this feature. -```ruby - render json: @posts, pagination: true -``` +If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). -AMS relies on either `Kaminari` or `WillPaginate`. Please install either dependency by adding one of those to your Gemfile. +```ruby +#kaminari example +@posts = Kaminari.paginate_array(Post.all).page(3).per(1) +render json: @posts -Pagination links will only be included in your response if you are using a ```JSON-API``` adapter, the others adapters doesn't have this feature. +#will_paginate example +@posts = Post.all.paginate(page: 3, per_page: 1) +render json: @posts +``` ```ruby ActiveModel::Serializer.config.adapter = :json_api @@ -38,3 +42,5 @@ ex: } } ``` + +AMS relies on either [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). Please install either dependency by adding one of those to your Gemfile. diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index 99f860f57..927160f3b 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -25,9 +25,10 @@ def get_serializer(resource, options = {}) "Please pass 'adapter: false' or see ActiveSupport::SerializableResource#serialize" options[:adapter] = false end - if options[:pagination] - options[:original_url] = original_url - options[:query_parameters] = query_parameters + if resource.respond_to?(:current_page) && resource.respond_to?(:total_pages) + options[:pagination] = {} + options[:pagination][:original_url] = original_url + options[:pagination][:query_parameters] = query_parameters end ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource| if serializable_resource.serializer? diff --git a/lib/active_model/serializable_resource.rb b/lib/active_model/serializable_resource.rb index 9115b3967..fa3fbe035 100644 --- a/lib/active_model/serializable_resource.rb +++ b/lib/active_model/serializable_resource.rb @@ -2,7 +2,7 @@ module ActiveModel class SerializableResource - ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :pagination]) + ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter]) def initialize(resource, options = {}) @resource = resource diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index ceb69487f..b48e1d20c 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -164,7 +164,7 @@ def add_resource_relationships(attrs, serializer, options = {}) def add_links(options) links = @hash.fetch(:links) { {} } resources = serializer.instance_variable_get(:@resource) - @hash[:links] = add_pagination_links(links, resources, options) if @options[:pagination] + @hash[:links] = add_pagination_links(links, resources, options) if options[:pagination] end def add_pagination_links(links, resources, options) diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index 4244721e2..7b608e611 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -14,7 +14,7 @@ def initialize(collection) def serializable_hash(options = {}) pages_from.each_with_object({}) do |(key, value), hash| - query_parameters = options.fetch(:query_parameters) { {} } + query_parameters = options[:pagination].fetch(:query_parameters) { {} } params = query_parameters.merge(page: { number: value, size: collection.size }).to_query hash[key] = "#{url(options)}?#{params}" @@ -51,7 +51,7 @@ def raise_unless_any_gem_installed def url(options) self_link = options.fetch(:links) {{}} - self_link.fetch(:self) {} ? options[:links][:self] : options[:original_url] + self_link.fetch(:self) {} ? options[:links][:self] : options[:pagination][:original_url] end end end diff --git a/test/action_controller/json_api/pagination_test.rb b/test/action_controller/json_api/pagination_test.rb index cf7832a85..bc044c3f8 100644 --- a/test/action_controller/json_api/pagination_test.rb +++ b/test/action_controller/json_api/pagination_test.rb @@ -31,19 +31,16 @@ def using_will_paginate end def render_pagination_using_kaminari - render json: using_kaminari, adapter: :json_api, pagination: true + render json: using_kaminari, adapter: :json_api end def render_pagination_using_will_paginate - render json: using_will_paginate, adapter: :json_api, pagination: true + render json: using_will_paginate, adapter: :json_api end def render_array_without_pagination_links - render json: using_will_paginate, adapter: :json_api, pagination: false - end - - def render_array_omitting_pagination_options - render json: using_kaminari, adapter: :json_api + setup + render json: @array, adapter: :json_api end end @@ -104,12 +101,6 @@ def test_array_without_pagination_links response = JSON.parse(@response.body) refute response.key? 'links' end - - def test_array_omitting_pagination_options - get :render_array_omitting_pagination_options, page: { number: 2, size: 1 } - response = JSON.parse(@response.body) - refute response.key? 'links' - end end end end diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 198662fdb..2a624ab7c 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -27,15 +27,11 @@ def using_will_paginate end def data - { - data: [{ - id:"2", - type:"profiles", - attributes:{ - name:"Name 2", - description:"Description 2" - } - }] + { data:[ + { id:"1", type:"profiles", attributes:{name:"Name 1", description:"Description 1" } }, + { id:"2", type:"profiles", attributes:{name:"Name 2", description:"Description 2" } }, + { id:"3", type:"profiles", attributes:{name:"Name 3", description:"Description 3" } } + ] } end @@ -56,42 +52,48 @@ def expected_response_without_pagination_links end def expected_response_with_pagination_links - data.merge links + {}.tap do |hash| + hash[:data] = [data.values.flatten.second] + hash.merge! links + end end def expected_response_with_pagination_links_and_additional_params new_links = links[:links].each_with_object({}) {|(key, value), hash| hash[key] = "#{value}&teste=teste" } - data.merge links: new_links + {}.tap do |hash| + hash[:data] = [data.values.flatten.second] + hash.merge! links: new_links + end end def test_pagination_links_using_kaminari serializer = ArraySerializer.new(using_kaminari) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) assert_equal expected_response_with_pagination_links, - adapter.serializable_hash(original_url: "http://example.com") + adapter.serializable_hash(pagination: { original_url: "http://example.com" }) end def test_pagination_links_using_will_paginate serializer = ArraySerializer.new(using_will_paginate) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) assert_equal expected_response_with_pagination_links, - adapter.serializable_hash(original_url: "http://example.com") + adapter.serializable_hash(pagination: { original_url: "http://example.com" }) end def test_pagination_links_with_additional_params serializer = ArraySerializer.new(using_will_paginate) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) assert_equal expected_response_with_pagination_links_and_additional_params, - adapter.serializable_hash(original_url: "http://example.com", - query_parameters: { teste: "teste"}) + adapter.serializable_hash(pagination: { original_url: "http://example.com", + query_parameters: { teste: "teste"}}) end def test_not_showing_pagination_links - serializer = ArraySerializer.new(using_will_paginate) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: false) + serializer = ArraySerializer.new(@array) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) assert_equal expected_response_without_pagination_links, adapter.serializable_hash end From 5031eb9f967e6c4c42d3b8d608037e72fa8fa94f Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Mon, 17 Aug 2015 15:17:15 -0300 Subject: [PATCH 15/20] add test to prev and first with additional params --- test/action_controller/json_api/pagination_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/action_controller/json_api/pagination_test.rb b/test/action_controller/json_api/pagination_test.rb index bc044c3f8..55db95ef2 100644 --- a/test/action_controller/json_api/pagination_test.rb +++ b/test/action_controller/json_api/pagination_test.rb @@ -96,6 +96,15 @@ def test_render_only_last_and_next_pagination_links_with_additional_params assert_equal expected_links, response['links'] end + def test_render_only_prev_and_first_pagination_links_with_additional_params + expected_links = { "self"=>"#{KAMINARI_URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1&teste=additional", + "first"=>"#{KAMINARI_URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1&teste=additional", + "prev"=>"#{KAMINARI_URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1&teste=additional"} + get :render_pagination_using_kaminari, page: { number: 3, size: 1 }, teste: "additional" + response = JSON.parse(@response.body) + assert_equal expected_links, response['links'] + end + def test_array_without_pagination_links get :render_array_without_pagination_links, page: { number: 2, size: 1 } response = JSON.parse(@response.body) From 01eab3bdb460a2b49ea974a0ae1192a86d7312a4 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Tue, 18 Aug 2015 19:05:20 -0300 Subject: [PATCH 16/20] send whole request context to model serializer --- lib/action_controller/serialization.rb | 14 +------- lib/active_model/serializer/adapter.rb | 6 ++-- .../serializer/adapter/json_api.rb | 11 ++++-- .../adapter/json_api/pagination_links.rb | 25 +++++++------ .../adapter/json_api/pagination_links_test.rb | 36 ++++++++++++------- 5 files changed, 48 insertions(+), 44 deletions(-) diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index 927160f3b..0850f7417 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -25,11 +25,6 @@ def get_serializer(resource, options = {}) "Please pass 'adapter: false' or see ActiveSupport::SerializableResource#serialize" options[:adapter] = false end - if resource.respond_to?(:current_page) && resource.respond_to?(:total_pages) - options[:pagination] = {} - options[:pagination][:original_url] = original_url - options[:pagination][:query_parameters] = query_parameters - end ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource| if serializable_resource.serializer? serializable_resource.serialization_scope ||= serialization_scope @@ -52,6 +47,7 @@ def use_adapter? [:_render_option_json, :_render_with_renderer_json].each do |renderer_method| define_method renderer_method do |resource, options| + options.fetch(:context) { options[:context] = request } serializable_resource = get_serializer(resource, options) super(serializable_resource, options) end @@ -62,13 +58,5 @@ def serialization_scope(scope) self._serialization_scope = scope end end - - def original_url - request.original_url[/\A[^?]+/] - end - - def query_parameters - request.query_parameters - end end end diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 7a48942d0..0b8118d60 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -21,9 +21,9 @@ def serializable_hash(options = nil) end def as_json(options = nil) - serializable_hash(options).tap do |hash| - include_meta(hash) - end + hash = serializable_hash(options) + include_meta(hash) + hash end def self.create(resource, options = {}) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index b48e1d20c..1b55a8121 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -164,11 +164,18 @@ def add_resource_relationships(attrs, serializer, options = {}) def add_links(options) links = @hash.fetch(:links) { {} } resources = serializer.instance_variable_get(:@resource) - @hash[:links] = add_pagination_links(links, resources, options) if options[:pagination] + @hash[:links] = add_pagination_links(links, resources, options) if is_paginated?(resources) end def add_pagination_links(links, resources, options) - links.update(JsonApi::PaginationLinks.new(resources).serializable_hash(options)) + pagination_links = JsonApi::PaginationLinks.new(resources, options[:context]).serializable_hash(options) + links.update(pagination_links) + end + + def is_paginated?(resource) + resource.respond_to?(:current_page) && + resource.respond_to?(:total_pages) && + resource.respond_to?(:size) end end end diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index 7b608e611..faa053e7d 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -5,16 +5,15 @@ class JsonApi < Adapter class PaginationLinks FIRST_PAGE = 1 - attr_reader :collection + attr_reader :collection, :context - def initialize(collection) - raise_unless_any_gem_installed + def initialize(collection, context) @collection = collection + @context = context end def serializable_hash(options = {}) pages_from.each_with_object({}) do |(key, value), hash| - query_parameters = options[:pagination].fetch(:query_parameters) { {} } params = query_parameters.merge(page: { number: value, size: collection.size }).to_query hash[key] = "#{url(options)}?#{params}" @@ -41,17 +40,17 @@ def pages_from end end - def raise_unless_any_gem_installed - return if defined?(WillPaginate) || defined?(Kaminari) - raise <<-EOF - AMS relies on either Kaminari or WillPaginate for pagination. - Please install either dependency by adding one of those to your Gemfile. - EOF - end - def url(options) self_link = options.fetch(:links) {{}} - self_link.fetch(:self) {} ? options[:links][:self] : options[:pagination][:original_url] + self_link.fetch(:self) {} ? options[:links][:self] : original_url + end + + def original_url + @original_url ||= context.original_url[/\A[^?]+/] + end + + def query_parameters + @query_parameters ||= context.query_parameters end end end diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 2a624ab7c..2fa7a1abc 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -9,6 +9,8 @@ class Serializer class Adapter class JsonApi class PaginationLinksTest < Minitest::Test + URI = 'http://example.com' + def setup ActionController::Base.cache_store.clear @array = [ @@ -18,6 +20,14 @@ def setup ] end + def mock_request(query_parameters={}, original_url=URI) + context = Minitest::Mock.new + context.expect(:original_url, original_url ) + context.expect(:query_parameters, query_parameters) + @options = {} + @options[:context] = context + end + def using_kaminari Kaminari.paginate_array(@array).page(2).per(1) end @@ -38,11 +48,11 @@ def data def links { links:{ - self: "http://example.com?page%5Bnumber%5D=2&page%5Bsize%5D=1", - first: "http://example.com?page%5Bnumber%5D=1&page%5Bsize%5D=1", - prev: "http://example.com?page%5Bnumber%5D=1&page%5Bsize%5D=1", - next: "http://example.com?page%5Bnumber%5D=3&page%5Bsize%5D=1", - last: "http://example.com?page%5Bnumber%5D=3&page%5Bsize%5D=1" + self: "#{URI}?page%5Bnumber%5D=2&page%5Bsize%5D=1", + first: "#{URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + prev: "#{URI}?page%5Bnumber%5D=1&page%5Bsize%5D=1", + next: "#{URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1", + last: "#{URI}?page%5Bnumber%5D=3&page%5Bsize%5D=1" } } end @@ -59,7 +69,7 @@ def expected_response_with_pagination_links end def expected_response_with_pagination_links_and_additional_params - new_links = links[:links].each_with_object({}) {|(key, value), hash| hash[key] = "#{value}&teste=teste" } + new_links = links[:links].each_with_object({}) {|(key, value), hash| hash[key] = "#{value}&test=test" } {}.tap do |hash| hash[:data] = [data.values.flatten.second] hash.merge! links: new_links @@ -70,25 +80,25 @@ def test_pagination_links_using_kaminari serializer = ArraySerializer.new(using_kaminari) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) - assert_equal expected_response_with_pagination_links, - adapter.serializable_hash(pagination: { original_url: "http://example.com" }) + mock_request + assert_equal expected_response_with_pagination_links, adapter.serializable_hash(@options) end def test_pagination_links_using_will_paginate serializer = ArraySerializer.new(using_will_paginate) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) - assert_equal expected_response_with_pagination_links, - adapter.serializable_hash(pagination: { original_url: "http://example.com" }) + mock_request + assert_equal expected_response_with_pagination_links, adapter.serializable_hash(@options) end def test_pagination_links_with_additional_params serializer = ArraySerializer.new(using_will_paginate) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) - assert_equal expected_response_with_pagination_links_and_additional_params, - adapter.serializable_hash(pagination: { original_url: "http://example.com", - query_parameters: { teste: "teste"}}) + mock_request({ test: 'test' }) + assert_equal expected_response_with_pagination_links_and_additional_params, + adapter.serializable_hash(@options) end def test_not_showing_pagination_links From f85027e631435879204645198726a3a4085835c1 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Wed, 19 Aug 2015 11:09:47 -0300 Subject: [PATCH 17/20] add more documentation to pagination links --- README.md | 4 +-- docs/README.md | 2 +- docs/howto/add_pagination_links.md | 54 +++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ef103ba16..24152c4f3 100644 --- a/README.md +++ b/README.md @@ -274,9 +274,9 @@ The `url` declaration describes which named routes to use while generating URLs for your JSON. Not every adapter will require URLs. ## Pagination -Pagination links will be included in your response automatically as long as the resource is paginated using [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate) and if you are using a ```JSON-API``` adapter. The others adapters does not have this feature. +Pagination links will be included in your response automatically as long as the resource is paginated using [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate) and if you are using a ```JSON-API``` adapter. -For more information about it, please see in our docs [How to add pagination links](https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md) +Although the others adapters does not have this feature, it is possible to implement pagination links to `JSON` adapter. For more information about it, please see in our docs [How to add pagination links](https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md) ## Caching diff --git a/docs/README.md b/docs/README.md index 2398528b9..5bd9c2e19 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ This is the documentation of AMS, it's focused on the **0.10.x version.** ## How to - [How to add root key](howto/add_root_key.md) -- [How to add pagination links](howto/add_pagination_links.md) (```JSON-API``` only) +- [How to add pagination links](howto/add_pagination_links.md) ## Getting Help diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index d49095b8c..87fc887f4 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -1,6 +1,8 @@ # How to add pagination links -Pagination links will be included in your response automatically as long as the resource is paginated and if you are using a ```JSON-API``` adapter. The others adapters does not have this feature. +### JSON-API adapter + +Pagination links will be included in your response automatically as long as the resource is paginated and if you are using a ```JSON-API``` adapter. If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). @@ -44,3 +46,53 @@ ex: ``` AMS relies on either [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). Please install either dependency by adding one of those to your Gemfile. + +### JSON adapter + +If you are using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key. + +In your action specify a custom serializer. +```ruby +render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer +``` + +And then, you could do something like the following class. +```ruby +class PaginatedSerializer < ActiveModel::Serializer::ArraySerializer + def initialize(object, options={}) + meta_key = options[:meta_key] || :meta + options[meta_key] ||= {} + options[meta_key] = { + current_page: object.current_page, + next_page: object.next_page, + prev_page: object.prev_page, + total_pages: object.total_pages, + total_count: object.total_count + } + super(object, options) + end +end +``` +ex. +```json +{ + "articles": [ + { + "id": 2, + "title": "JSON API paints my bikeshed!", + "body": "The shortest article. Ever." + } + ], + "meta": { + "current_page": 3, + "next_page": 4, + "prev_page": 2, + "total_pages": 10, + "total_count": 10 + } +} +``` + +### FlattenJSON adapter + +This adapter does not allow us to use `meta` key, due to that it is not possible to add pagination links. From 3c3578a9b83974c6656867c249d60ec1401df3a6 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Wed, 19 Aug 2015 11:16:53 -0300 Subject: [PATCH 18/20] improvements on how to get self link on pagination class --- .../serializer/adapter/json_api/pagination_links.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/active_model/serializer/adapter/json_api/pagination_links.rb b/lib/active_model/serializer/adapter/json_api/pagination_links.rb index faa053e7d..55e3280b8 100644 --- a/lib/active_model/serializer/adapter/json_api/pagination_links.rb +++ b/lib/active_model/serializer/adapter/json_api/pagination_links.rb @@ -41,8 +41,7 @@ def pages_from end def url(options) - self_link = options.fetch(:links) {{}} - self_link.fetch(:self) {} ? options[:links][:self] : original_url + @url ||= options.fetch(:links, {}).fetch(:self, nil) || original_url end def original_url From b73ffe25c81d2d14b720afdda6dd1687f8ee384d Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Thu, 20 Aug 2015 11:31:21 -0300 Subject: [PATCH 19/20] add kaminari and will_paginate examples --- docs/howto/add_pagination_links.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index 87fc887f4..4241012e9 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -6,13 +6,26 @@ Pagination links will be included in your response automatically as long as the If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). +###### Kaminari examples ```ruby -#kaminari example -@posts = Kaminari.paginate_array(Post.all).page(3).per(1) +#array +@posts = Kaminari.paginate_array([1, 2, 3]).page(3).per(1) render json: @posts -#will_paginate example -@posts = Post.all.paginate(page: 3, per_page: 1) +#active_record +@posts = Post.page(3).per(1) +render json: @posts +``` + +###### WillPaginate examples + +```ruby +#array +@posts = [1,2,3].paginate(page: 3, per_page: 1) +render json: @posts + +#active_record +@posts = Post.page(3).per_page(1) render json: @posts ``` @@ -45,7 +58,8 @@ ex: } ``` -AMS relies on either [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). Please install either dependency by adding one of those to your Gemfile. +AMS pagination relies on a paginated collection with the methods `current_page`, `total_pages`, and `size`, such as are supported by both [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). + ### JSON adapter From d50262edbefe3bc2f2aa85c7beb5868816165d19 Mon Sep 17 00:00:00 2001 From: Bruno Bacarini Date: Thu, 20 Aug 2015 11:32:03 -0300 Subject: [PATCH 20/20] test pagination links the way the controller does --- test/adapter/json_api/pagination_links_test.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/adapter/json_api/pagination_links_test.rb b/test/adapter/json_api/pagination_links_test.rb index 2fa7a1abc..58a291205 100644 --- a/test/adapter/json_api/pagination_links_test.rb +++ b/test/adapter/json_api/pagination_links_test.rb @@ -28,6 +28,11 @@ def mock_request(query_parameters={}, original_url=URI) @options[:context] = context end + def load_adapter(paginated_collection, options = {}) + options = options.merge(adapter: :json_api) + ActiveModel::SerializableResource.new(paginated_collection, options) + end + def using_kaminari Kaminari.paginate_array(@array).page(2).per(1) end @@ -77,24 +82,21 @@ def expected_response_with_pagination_links_and_additional_params end def test_pagination_links_using_kaminari - serializer = ArraySerializer.new(using_kaminari) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + adapter = load_adapter(using_kaminari) mock_request assert_equal expected_response_with_pagination_links, adapter.serializable_hash(@options) end def test_pagination_links_using_will_paginate - serializer = ArraySerializer.new(using_will_paginate) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + adapter = load_adapter(using_will_paginate) mock_request assert_equal expected_response_with_pagination_links, adapter.serializable_hash(@options) end def test_pagination_links_with_additional_params - serializer = ArraySerializer.new(using_will_paginate) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + adapter = load_adapter(using_will_paginate) mock_request({ test: 'test' }) assert_equal expected_response_with_pagination_links_and_additional_params, @@ -102,8 +104,7 @@ def test_pagination_links_with_additional_params end def test_not_showing_pagination_links - serializer = ArraySerializer.new(@array) - adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + adapter = load_adapter(@array) assert_equal expected_response_without_pagination_links, adapter.serializable_hash end