Skip to content

Commit 70cfe5b

Browse files
committed
Allow bypassing the implicit render in paginate
When using Rails, a common pattern would be to simply paginate a collection and then render it using a custom serializer. Although this is currently possible by calling `_paginate_collection` directly, it was unintended. To better support custom serialization or things like JBuilder, allow users to bypass the implicit render at the end of `paginate` by passing their collection to it directly instead of via the `:json` or `:xml` options: ```ruby class MoviesController def index movies = paginate(Movie.all, per_page: 25) render json: MoviesSerializer.new(movies) end end ``` If the call to `paginate` still mimics what a call to `render` looks like, an implicit render will happen. If it looks more like the above, the collection will simply be paginated and returned after the headers are set up. Signed-off-by: David Celis <[email protected]>
1 parent 3f3a1d8 commit 70cfe5b

File tree

4 files changed

+61
-12
lines changed

4 files changed

+61
-12
lines changed

README.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ gem 'api-pagination'
2525

2626
## Rails
2727

28-
In your controller, provide a pageable collection to the `paginate` method:
28+
In your controller, provide a pageable collection to the `paginate` method. In its most convenient form, `paginate` simply mimics `render`:
2929

3030
```ruby
3131
class MoviesController < ApplicationController
@@ -47,18 +47,33 @@ class MoviesController < ApplicationController
4747
end
4848
```
4949

50-
`paginate` will:
50+
This will pull your collection from the `json` or `xml` option, paginate it for you using `params[:page]` and `params[:per_page]`, render Link headers, and call `ActionController::Base#render` with whatever you passed to `paginate`. This should work well with [ActiveModel::Serializers](https://github.com/rails-api/active_model-serializers). However, if you need more control over what is done with your paginated collection, you can pass the collection directly to `paginate` instead of in a way that mimics `render`:
5151

52-
1. Pull your collection from `json:` or `xml:`
53-
2. Use `params[:page]` and `params[:per_page]` to paginate your collection for you
54-
3. Use the paginated collection to render `Link` headers
55-
4. Call `ActionController::Base#render` with whatever you passed to `paginate`.
52+
```ruby
53+
class MoviesController < ApplicationController
54+
# GET /movies
55+
def index
56+
movies = paginate Movie.all
57+
58+
render json: MoviesSerializer.new(movies)
59+
end
60+
61+
# GET /movies/:id/cast
62+
def cast
63+
actors = paginate Movie.find(params[:id]).actors, per_page: 10
64+
65+
render json: ActorsSerializer.new(actors)
66+
end
67+
end
68+
```
69+
70+
This will avoid implicitly calling `render` at the end. Instead, `paginate` will simply set up the headers and return your collection so you can do whatever you want with it.
5671

57-
The collection sent to `paginate` _must_ respond to your paginator's methods. For Kaminari, `Kaminari.paginate_array` will be called for you behind-the-scenes. For WillPaginate, you're out of luck unless you somewhere `require 'will_paginate/array'`. Because this pollutes `Array`, it won't be done for you automatically.
72+
Note that the collection sent to `paginate` _must_ respond to your paginator's methods. For Kaminari, `Kaminari.paginate_array` will be called for you behind-the-scenes. For WillPaginate, you're out of luck unless you call `require 'will_paginate/array'` somewhere. Because this pollutes `Array`, it won't be done for you automatically.
5873

5974
## Grape
6075

61-
Grape is similar, though `paginate` won't take options. Only your collection. In your API endpoint:
76+
With Grape, `paginate` is used to declare that your endpoint takes a `:page` and `:per_page` param. Inside your API endpoint, it simply takes your collection:
6277

6378
```ruby
6479
class MoviesAPI < Grape::API

lib/rails/pagination.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ module Rails
22
module Pagination
33
protected
44

5-
def paginate(options)
5+
def paginate(*options_or_collection)
6+
options = options_or_collection.extract_options!
7+
collection = options_or_collection.first
8+
9+
return _paginate_collection(collection, options) if collection
10+
611
collection = options[:json] || options[:xml]
12+
collection = _paginate_collection(collection, options)
713

8-
collection = _paginate_collection(collection, options)
914
options[:json] = collection if options[:json]
1015
options[:xml] = collection if options[:xml]
1116

spec/rails_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@
5353
it_behaves_like 'an endpoint with a middle page'
5454
end
5555
end
56+
57+
context 'providing a block' do
58+
it 'yields to the block instead of implicitly rendering' do
59+
get :index_with_custom_render, :count => 100
60+
61+
json = { numbers: (1..10).map { |n| { number: n } } }.to_json
62+
63+
expect(response.body).to eq(json)
64+
end
65+
end
5666
end
5767
end
5868

spec/support/numbers_controller.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,25 @@ def teardown(*methods)
3939
end
4040

4141
Rails.application.routes.draw do
42-
resources :numbers, :only => [:index]
42+
resources :numbers, :only => [:index] do
43+
get :index_with_custom_render, on: :collection
44+
end
45+
end
46+
47+
class NumbersSerializer
48+
def initialize(numbers)
49+
@numbers = numbers
50+
end
51+
52+
def to_json(options = {})
53+
{ numbers: @numbers.map { |n| { number: n } } }.to_json
54+
end
4355
end
4456

4557
class NumbersController < ActionController::Base
4658
include Rails.application.routes.url_helpers
4759

4860
def index
49-
page = params.fetch(:page, 1).to_i
5061
total = params.fetch(:count).to_i
5162

5263
if params[:with_headers]
@@ -57,4 +68,12 @@ def index
5768

5869
paginate :json => (1..total).to_a, :per_page => 10
5970
end
71+
72+
def index_with_custom_render
73+
total = params.fetch(:count).to_i
74+
numbers = (1..total).to_a
75+
numbers = paginate numbers, :per_page => 10
76+
77+
render json: NumbersSerializer.new(numbers)
78+
end
6079
end

0 commit comments

Comments
 (0)