Skip to content

Commit ef4f000

Browse files
committed
Add support for Pagy (closes #97)
1 parent 6126bfd commit ef4f000

File tree

11 files changed

+163
-130
lines changed

11 files changed

+163
-130
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ rvm:
77
- 2.5.0
88
script: bundle exec rspec
99
env:
10+
- PAGINATOR=pagy
1011
- PAGINATOR=kaminari
1112
- PAGINATOR=will_paginate

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ gemspec
55

66
gem 'kaminari', require: false
77
gem 'will_paginate', require: false
8+
gem 'pagy', require: false
89

910
gem 'sqlite3', require: false
1011
gem 'sequel', require: false

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gem 'rails-api'
1818
gem 'grape', '>= 0.10.0'
1919

2020
# Then choose your preferred paginator from the following:
21+
gem 'pagy'
2122
gem 'kaminari'
2223
gem 'will_paginate'
2324

@@ -27,11 +28,11 @@ gem 'api-pagination'
2728

2829
## Configuration (optional)
2930

30-
By default, api-pagination will detect whether you're using Kaminari or WillPaginate, and name headers appropriately. If you want to change any of the configurable settings, you may do so:
31+
By default, api-pagination will detect whether you're using Pagy, Kaminari, or WillPaginate, and it will name headers appropriately. If you want to change any of the configurable settings, you may do so:
3132

3233
```ruby
3334
ApiPagination.configure do |config|
34-
# If you have both gems included, you can choose a paginator.
35+
# If you have more than one gem included, you can choose a paginator.
3536
config.paginator = :kaminari # or :will_paginate
3637

3738
# By default, this is set to 'Total'
@@ -59,6 +60,16 @@ ApiPagination.configure do |config|
5960
end
6061
```
6162

63+
### Pagy-specific configuration
64+
65+
Pagy does not have a built-in way to specify a maximum number of items per page, but `api-pagination` will check if you've set a `:max_per_page` variable. To configure this, you can use the following code somewhere in an initializer:
66+
67+
```ruby
68+
Pagy::VARS[:max_per_page] = 100
69+
```
70+
71+
If left unconfigured, clients can request as many items per page as they wish, so it's highly recommended that you configure this.
72+
6273
## Rails
6374

6475
In your controller, provide a pageable collection to the `paginate` method. In its most convenient form, `paginate` simply mimics `render`:
@@ -103,7 +114,7 @@ class MoviesController < ApplicationController
103114
end
104115
```
105116

106-
Note that the collection sent to `paginate` _must_ respond to your paginator's methods. This is typically fine unless you're dealing with a stock Array. 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.
117+
Note that the collection sent to `paginate` _must_ respond to your paginator's methods. This is typically fine unless you're dealing with a stock Array. 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. If you use Pagy, it doesn't matter, because Pagy doesn't care what you're paginating. It will just work, as long as the collection responds to `count`.
107118

108119
**NOTE:** In versions 4.4.0 and below, the `Rails::Pagination` module would end up included in `ActionController::Base` even if `ActionController::API` was defined. As of version 4.5.0, this is no longer the case. If for any reason your API controllers cannot easily changed be changed to inherit from `ActionController::API` instead, you can manually include the module:
109120

lib/api-pagination.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ def paginate(collection, options = {})
99
options[:per_page] = options[:per_page].to_i
1010

1111
case ApiPagination.config.paginator
12+
when :pagy
13+
paginate_with_pagy(collection, options)
1214
when :kaminari
1315
paginate_with_kaminari(collection, options, options[:paginate_array_options] || {})
1416
when :will_paginate
@@ -18,7 +20,9 @@ def paginate(collection, options = {})
1820
end
1921
end
2022

21-
def pages_from(collection)
23+
def pages_from(collection, options = {})
24+
return pagy_pages_from(collection) if collection.is_a?(Pagy)
25+
2226
{}.tap do |pages|
2327
unless collection.first_page?
2428
pages[:first] = 1
@@ -34,13 +38,49 @@ def pages_from(collection)
3438

3539
def total_from(collection)
3640
case ApiPagination.config.paginator
41+
when :pagy then collection.count.to_s
3742
when :kaminari then collection.total_count.to_s
3843
when :will_paginate then collection.total_entries.to_s
3944
end
4045
end
4146

4247
private
4348

49+
def paginate_with_pagy(collection, options)
50+
if Pagy::VARS[:max_per_page] && options[:per_page] > Pagy::VARS[:max_per_page]
51+
options[:per_page] = Pagy::VARS[:max_per_page]
52+
elsif options[:per_page] <= 0
53+
options[:per_page] = Pagy::VARS[:items]
54+
end
55+
56+
pagy = pagy_from(collection, options)
57+
collection = if collection.respond_to?(:offset) && collection.respond_to?(:limit)
58+
collection.offset(pagy.offset).limit(pagy.items)
59+
else
60+
collection[pagy.offset, pagy.items]
61+
end
62+
63+
return [collection, pagy]
64+
end
65+
66+
def pagy_from(collection, options)
67+
Pagy.new(count: collection.count, items: options[:per_page], page: options[:page])
68+
end
69+
70+
def pagy_pages_from(pagy)
71+
{}.tap do |pages|
72+
unless pagy.page == 1
73+
pages[:first] = 1
74+
pages[:prev] = pagy.prev
75+
end
76+
77+
unless pagy.page == pagy.pages
78+
pages[:last] = pagy.pages
79+
pages[:next] = pagy.next
80+
end
81+
end
82+
end
83+
4484
def paginate_with_kaminari(collection, options, paginate_array_options = {})
4585
if Kaminari.config.max_per_page && options[:per_page] > Kaminari.config.max_per_page
4686
options[:per_page] = Kaminari.config.max_per_page

lib/api-pagination/configuration.rb

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ def paginator
5555

5656
def paginator=(paginator)
5757
case paginator.to_sym
58+
when :pagy
59+
use_pagy
5860
when :kaminari
5961
use_kaminari
6062
when :will_paginate
@@ -67,11 +69,12 @@ def paginator=(paginator)
6769
private
6870

6971
def set_paginator
70-
if defined?(Kaminari) && defined?(WillPaginate::CollectionMethods)
72+
conditions = [defined?(Pagy), defined?(Kaminari), defined?(WillPaginate::CollectionMethods)]
73+
if conditions.compact.size > 1
7174
Kernel.warn <<-WARNING
72-
Warning: api-pagination relies on either Kaminari or WillPaginate, but both are
73-
currently active. If possible, you should remove one or the other. If you can't,
74-
you _must_ configure api-pagination on your own. For example:
75+
Warning: api-pagination relies on Pagy, Kaminari, or WillPaginate, but more than
76+
one are currently active. If possible, you should remove one or the other. If
77+
you can't, you _must_ configure api-pagination on your own. For example:
7578
7679
ApiPagination.configure do |config|
7780
config.paginator = :kaminari
@@ -86,13 +89,19 @@ def set_paginator
8689
end
8790
8891
WARNING
92+
elsif defined?(Pagy)
93+
use_pagy
8994
elsif defined?(Kaminari)
90-
return use_kaminari
95+
use_kaminari
9196
elsif defined?(WillPaginate::CollectionMethods)
92-
return use_will_paginate
97+
use_will_paginate
9398
end
9499
end
95100

101+
def use_pagy
102+
@paginator = :pagy
103+
end
104+
96105
def use_kaminari
97106
require 'kaminari/models/array_extension'
98107
@paginator = :kaminari

lib/api-pagination/hooks.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44
Grape::API.send(:include, Grape::Pagination)
55
end
66

7+
begin; require 'pagy'; rescue LoadError; end
78
begin; require 'kaminari'; rescue LoadError; end
89
begin; require 'will_paginate'; rescue LoadError; end
910

10-
unless defined?(Kaminari) || defined?(WillPaginate::CollectionMethods)
11+
unless defined?(Pagy) || defined?(Kaminari) || defined?(WillPaginate::CollectionMethods)
1112
Kernel.warn <<-WARNING.gsub(/^\s{4}/, '')
12-
Warning: api-pagination relies on either Kaminari or WillPaginate. Please
13-
install either dependency by adding one of the following to your Gemfile:
13+
Warning: api-pagination relies on either Pagy, Kaminari, or WillPaginate.
14+
Please install a paginator by adding one of the following to your Gemfile:
1415
16+
gem 'pagy'
1517
gem 'kaminari'
1618
gem 'will_paginate'
1719
WARNING

lib/grape/pagination.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ def paginate(collection)
99
:page => ApiPagination.config.page_param(params),
1010
:per_page => [per_page, route_setting(:max_per_page)].compact.min
1111
}
12-
collection = ApiPagination.paginate(collection, options)
12+
collection, pagy = ApiPagination.paginate(collection, options)
1313

1414
links = (header['Link'] || "").split(',').map(&:strip)
1515
url = request.url.sub(/\?.*$/, '')
16-
pages = ApiPagination.pages_from(collection)
16+
pages = ApiPagination.pages_from(pagy || collection, options)
1717

1818
pages.each do |k, v|
1919
old_params = Rack::Utils.parse_nested_query(request.query_string)
@@ -27,7 +27,7 @@ def paginate(collection)
2727
include_total = ApiPagination.config.include_total
2828

2929
header 'Link', links.join(', ') unless links.empty?
30-
header total_header, ApiPagination.total_from(collection).to_s if include_total
30+
header total_header, ApiPagination.total_from(pagy || collection).to_s if include_total
3131
header per_page_header, options[:per_page].to_s
3232
header page_header, options[:page].to_s unless page_header.nil?
3333

lib/rails/pagination.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ def _paginate_collection(collection, options={})
2727
options[:page] = ApiPagination.config.page_param(params)
2828
options[:per_page] ||= ApiPagination.config.per_page_param(params)
2929

30-
collection = ApiPagination.paginate(collection, options)
30+
collection, pagy = ApiPagination.paginate(collection, options)
3131

3232
links = (headers['Link'] || '').split(',').map(&:strip)
3333
url = base_url + request.path_info
34-
pages = ApiPagination.pages_from(collection)
34+
pages = ApiPagination.pages_from(pagy || collection, options)
3535

3636
pages.each do |k, v|
3737
new_params = request.query_parameters.merge(:page => v)
@@ -46,7 +46,7 @@ def _paginate_collection(collection, options={})
4646
headers['Link'] = links.join(', ') unless links.empty?
4747
headers[per_page_header] = options[:per_page].to_s
4848
headers[page_header] = options[:page].to_s unless page_header.nil?
49-
headers[total_header] = total_count(collection, options).to_s if include_total
49+
headers[total_header] = total_count(pagy || collection, options).to_s if include_total
5050

5151
return collection
5252
end
@@ -62,6 +62,5 @@ def total_count(collection, options)
6262
def base_url
6363
ApiPagination.config.base_url || request.base_url
6464
end
65-
6665
end
6766
end

spec/api-pagination_spec.rb

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,48 @@
55
let(:paginate_array_options) {{ total_count: 1000 }}
66

77
describe "#paginate" do
8-
context 'Using kaminari' do
9-
before do
10-
ApiPagination.config.paginator = :kaminari
11-
end
12-
13-
after do
14-
ApiPagination.config.paginator = ENV['PAGINATOR'].to_sym
15-
end
16-
17-
it 'should accept paginate_array_options option' do
18-
expect(Kaminari).to receive(:paginate_array)
19-
.with(collection, paginate_array_options)
20-
.and_call_original
21-
22-
ApiPagination.paginate(
23-
collection,
24-
{
25-
per_page: 30,
26-
paginate_array_options: paginate_array_options
27-
}
28-
)
29-
end
8+
if ENV['PAGINATOR'].to_sym == :kaminari
9+
context 'Using kaminari' do
10+
it 'should accept paginate_array_options option' do
11+
expect(Kaminari).to receive(:paginate_array)
12+
.with(collection, paginate_array_options)
13+
.and_call_original
14+
15+
ApiPagination.paginate(
16+
collection,
17+
{
18+
per_page: 30,
19+
paginate_array_options: paginate_array_options
20+
}
21+
)
22+
end
3023

31-
describe '.pages_from' do
32-
subject {described_class.pages_from collection}
24+
describe '.pages_from' do
25+
subject {described_class.pages_from collection}
3326

34-
context 'on empty collection' do
35-
let(:collection) {ApiPagination.paginate [], page: 1}
27+
context 'on empty collection' do
28+
let(:collection) {ApiPagination.paginate [], page: 1}
3629

37-
it {is_expected.to be_empty}
30+
it {is_expected.to be_empty}
31+
end
3832
end
3933
end
4034
end
4135

42-
context 'Using will_paginate' do
43-
before do
44-
ApiPagination.config.paginator = :will_paginate
45-
end
46-
47-
after do
48-
ApiPagination.config.paginator = ENV['PAGINATOR'].to_sym
49-
end
50-
51-
context 'passing in total_entries in options' do
52-
it 'should set total_entries using the passed in value' do
53-
paginated_collection = ApiPagination.paginate(collection, total_entries: 3000)
54-
expect(paginated_collection.total_entries).to eq(3000)
36+
if ENV['PAGINATOR'].to_sym == :will_paginate
37+
context 'Using will_paginate' do
38+
context 'passing in total_entries in options' do
39+
it 'should set total_entries using the passed in value' do
40+
paginated_collection = ApiPagination.paginate(collection, total_entries: 3000)
41+
expect(paginated_collection.total_entries).to eq(3000)
42+
end
5543
end
56-
end
5744

58-
context 'passing in collection only' do
59-
it 'should set total_entries using the size of the collection ' do
60-
paginated_collection = ApiPagination.paginate(collection)
61-
expect(paginated_collection.total_entries).to eq(100)
45+
context 'passing in collection only' do
46+
it 'should set total_entries using the size of the collection ' do
47+
paginated_collection = ApiPagination.paginate(collection)
48+
expect(paginated_collection.total_entries).to eq(100)
49+
end
6250
end
6351
end
6452
end

0 commit comments

Comments
 (0)