Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to disable JsonApi automatic pagination #1596

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Breaking changes:

Features:
- [#1596](https://github.com/rails-api/active_model_serializers/pull/1596) Provide a way to prevent links to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting  `config.collection_serializer = NonPaginatedCollectionSerializer` now prevents the JSON API adapter from automatically rendering pagination Link objects

automatically rendered when using JSON API adapter. (@groyoh)
- [#1574](https://github.com/rails-api/active_model_serializers/pull/1574) Provide key translation. (@remear)
- [#1494](https://github.com/rails-api/active_model_serializers/pull/1494) Make serializers serializalbe
(using the Attributes adapter by default). (@bf4)
Expand Down
125 changes: 52 additions & 73 deletions docs/howto/add_pagination_links.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,34 @@

# How to add pagination links

### JSON API adapter
## JSON API adapter

Pagination links will be included in your response automatically as long as
the resource is paginated and if you are using the ```JsonApi``` adapter.
When using the `JsonApi` adapter, pagination links will be automatically included if you use [Kaminari](https://github.com/amatsuda/kaminari)
or [WillPaginate](https://github.com/mislav/will_paginate) within a Rails controller:

If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari)
or [WillPaginate](https://github.com/mislav/will_paginate).

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

###### Kaminari examples
* Using Kaminari:

```ruby
#array
@posts = Kaminari.paginate_array([1, 2, 3]).page(3).per(1)
render json: @posts

#active_record
@posts = Post.page(3).per(1)
render json: @posts
class PostsController < ApplicationController
def index
posts = Post.page(params[:page]).per(params[:per_page])
render json: posts
end
end
```

###### 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
```
* Using WillPaginate:

```ruby
ActiveModelSerializers.config.adapter = :json_api
class PostsController < ApplicationController
def index
posts = Post.page(params[:page]).per_page(params[:per_page])
render json: posts
end
end
```

ex:
The response might look like:
```json
{
"data": [
Expand All @@ -67,34 +54,50 @@ ex:
}
```

ActiveModelSerializers 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).

ActiveModelSerializers pagination relies on paginated collections which define the methods `#current_page`, `#total_pages`, and `#size`.
Such methods are supported by both [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate),
but you can also roll out your own paginated collection by defining these methods.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find the above changes very helpful. What was your motivation? Sell me!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At work we use neither Kaminari nor WillPaginate to paginate, but having automatic links is nice. We don't use this feature for now, but I plan to try it out soon. Therefore I would like to simply define a lightweight wrapper PaginatedCollection.new(actual_collection, current_page, total_pages, size) that would define these three methods and render the links. Does that make sense? My idea here is that other people might also want automatic pagination links with having to use Kaminari or WillPaginate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, I meant the entirely of the diff in the docs, not just the comment about rolling your own :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, some part did not really make sense e.g.:

-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 

Adding this to the JSONAPI section does not provide any value. If a user is looking for JSONAPI, he will not care about this. If he's looking for JSON, he'll probably just scroll down directly.

Then I think adding an explicit example instead of a small code snippet brings more value and is easier to understand. Here we know that we should use the pagination within an Rails controller:

-#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
-
+class PostsController < ApplicationController
+  def index
+    posts = Post.page(params[:page]).per_page(params[:per_page])
+    render json: posts
+  end
+end

It provides an example ready to use. Then the #array/#active_record examples are Kaminari/WillPaginate specific and does not make to much sense it. If users want to know how to use these tools, they should look the doc there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the #array/#active_record examples are Kaminari/WillPaginate specific and does not make to much sense it. If users want to know how to use these tools, they should look the doc there.

Ok, I buy that


### JSON adapter
If you do not want pagination links to be automatically rendered, you may disable it by setting the `ActiveModelSerializers.config.collection_serializer` config to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing a default is kinda risky, isn't it?

today, aren't all results non-paginated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default behavior does not change. The pagination links are included by default at the moment, see #1549.

`ActiveModel::Serializer::NonPaginatedCollectionSerializer`.

If you are using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key.
If you want to disable pagination links for a specific controller, you may set the `serializer` option to `ActiveModel::Serializer::NonPaginatedCollectionSerializer`:

Add this method to your base API controller.

```ruby
def pagination_dict(object)
{
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
}
``` ruby
class PostsController < ApplicationController
def index
posts = Post.page(params[:page]).per_page(params[:per_page])
render json: posts, serializer: ActiveModel::Serializer::NonPaginatedCollectionSerializer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems kinda funky -- what if I want to render posts with the MyPostSerializer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that would be each_serializer... idk.. this api seems funky

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render json: posts, serializer: ActiveModel::Serializer::NonPaginatedCollectionSerializer, each_serializer: MyPostSerializer

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was never fan of each_serializer to be honest 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, just seems super verbose, and requires knowledge of our class structure.

wouldn't

render json: posts, each_serializer: MyPostSerializer, paginate: false

be more appropriate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That one more option to maintain 😁 And I think that this use case (using each_serializer and disabling pagination on a specific controller) would be quite rare. I think the general use case would be to enable or disable it globally, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably, yeah -- even for oneoffs, you can effectively disable pagination by setting per_page really high

end
end
```

Then, use it on your render method.
### Json adapter

If you are using the `Json` adapter, pagination links will not be included automatically, but it is possible to handle pagination using the `meta` option:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, maybe I misread above -- so it is disabled by default


```ruby
render json: posts, meta: pagination_dict(posts)
class PostsController < ApplicationController
def index
posts = Post.page(params[:page]).per_page(params[:per_page])
render json: posts, meta: pagination_dict(posts)
end

private

def pagination_dict(object)
{
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
}
end
end
```

ex.
The response might look like:
```json
{
"posts": [
Expand All @@ -113,27 +116,3 @@ ex.
}
}
```

You can also achieve the same result if you have a helper method that adds the pagination info in the meta tag. For instance, in your action specify a custom serializer.

```ruby
render json: @posts, each_serializer: PostPreviewSerializer, meta: meta_attributes(@post)
```

```ruby
#expects pagination!
def meta_attributes(resource, extra_meta = {})
{
current_page: resource.current_page,
next_page: resource.next_page,
prev_page: resource.prev_page,
total_pages: resource.total_pages,
total_count: resource.total_count
}.merge(extra_meta)
end
```


### Attributes adapter

This adapter does not allow us to use `meta` key, due to that it is not possible to add pagination links.
2 changes: 1 addition & 1 deletion lib/active_model/serializable_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
require 'active_model_serializers/adapter'
module ActiveModel
class SerializableResource
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :meta, :meta_key, :links])
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :meta, :meta_key, :links, :serialization_context])
include ActiveModelSerializers::Logging

delegate :serializable_hash, :as_json, :to_json, to: :adapter
Expand Down
1 change: 1 addition & 0 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'thread_safe'
require 'active_model/serializer/collection_serializer'
require 'active_model/serializer/non_paginated_collection_serializer'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be autoloaded

require 'active_model/serializer/array_serializer'
require 'active_model/serializer/error_serializer'
require 'active_model/serializer/errors_serializer'
Expand Down
3 changes: 1 addition & 2 deletions lib/active_model/serializer/collection_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ def json_key

def paginated?
object.respond_to?(:current_page) &&
object.respond_to?(:total_pages) &&
object.respond_to?(:size)
object.respond_to?(:total_pages)
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I assume that every collection should respond to #size. Correct me if I'm wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size got lost here


protected
Expand Down
11 changes: 11 additions & 0 deletions lib/active_model/serializer/non_paginated_collection_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'active_model/serializer/collection_serializer'

module ActiveModel
class Serializer
class NonPaginatedCollectionSerializer < CollectionSerializer
def paginated?
false
end
end
end
end
1 change: 1 addition & 0 deletions lib/active_model/serializer/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def initialize(*)
super
@_links = {}
@_include_data = true
@_meta = nil
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix one ruby warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in master

end

def link(name, value = nil, &block)
Expand Down
14 changes: 11 additions & 3 deletions lib/active_model_serializers/adapter/json_api.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# coding: utf-8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need

# {http://jsonapi.org/format/ JSON API specification}
# rubocop:disable Style/AsciiComments
# TODO: implement!
Expand Down Expand Up @@ -35,6 +36,7 @@ def initialize(serializer, options = {})
super
@include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include])
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
@serialization_context = options[:serialization_context]
end

def default_key_transform
Expand Down Expand Up @@ -126,7 +128,7 @@ def success_document(options)
hash[:links].update(instance_options[:links])
end

if is_collection && serializer.paginated?
if paginate?
hash[:links] ||= {}
hash[:links].update(pagination_links_for(serializer, options))
end
Expand Down Expand Up @@ -185,7 +187,7 @@ def fragment_cache(cached_hash, non_cached_hash)

protected

attr_reader :fieldset
attr_reader :fieldset, :serialization_context

private

Expand Down Expand Up @@ -234,6 +236,12 @@ def resource_objects_for(serializers)
[@primary, @included]
end

def paginate?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wondered if this shouldn't be the concern of PaginationLinks e.g.

pagination_links = PaginationLinks.new(serializer, serialization_context)
hash[:links] = pagination_links.add_to(hash) # check if paginated and add the links in such case

the same way as the Jsonapi class does

!serialization_context.nil? &&
serializer.respond_to?(:paginated?) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two lines could be replaced with a try -- unless the team is against trys for some reason

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I'd rather not use try in the end as it might somehow implies that the serializer might be nil.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

serializer.paginated?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what was insufficient about is_collection && serializer.paginated? ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe the serializer sometimes doesn't respond to paginated? like.. maybe it's sometimes not an array serializer?

end

def process_resource(serializer, primary)
resource_identifier = ResourceIdentifier.new(serializer).as_json
return false unless @resource_identifiers.add?(resource_identifier)
Expand Down Expand Up @@ -499,7 +507,7 @@ def links_for(serializer)
# prs:
# https://github.com/rails-api/active_model_serializers/pull/1041
def pagination_links_for(serializer, options)
PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options)
PaginationLinks.new(serializer.object, serialization_context).serializable_hash(options)
end

# {http://jsonapi.org/format/#document-meta Docment Meta}
Expand Down
Loading