api-pagination 1.1.1 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/api-pagination.rb +27 -25
- data/lib/api-pagination/hooks.rb +47 -0
- data/lib/api-pagination/version.rb +2 -2
- data/lib/grape/pagination.rb +37 -0
- data/lib/rails/pagination.rb +24 -0
- data/spec/grape_spec.rb +44 -0
- data/spec/rails_spec.rb +50 -0
- data/spec/spec_helper.rb +32 -37
- data/spec/support/numbers_api.rb +23 -0
- data/spec/support/numbers_controller.rb +63 -0
- data/spec/support/shared_examples/existing_headers.rb +10 -0
- data/spec/support/shared_examples/first_page.rb +17 -0
- data/spec/support/shared_examples/last_page.rb +17 -0
- data/spec/support/shared_examples/middle_page.rb +8 -0
- metadata +45 -17
- data/spec/controllers/numbers_controller_spec.rb +0 -109
- data/spec/dummy/log/development.log +0 -17807
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39ada68101786c8ae919218460cd300a41c03dab
|
4
|
+
data.tar.gz: e738863fb5ca287442afa734d7b87a01c0173e86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 332904cbbbcb0bbfee70edab279f0aecb400d8c3d4e8b76db5ecbec51ff532132467060b5b8a238436c846ab8b4f48b5927eb8575bf1878cc31e74227a8bcb0f
|
7
|
+
data.tar.gz: 3d9bad3a6aff7918d9f104abb8fa351566c7d8c745afe0178d0bb1afed61a611bb95a01c7bb7abef07687ec82b747421d2e663ca1aa7e615e3b3aab9ce8793c7
|
data/lib/api-pagination.rb
CHANGED
@@ -1,37 +1,39 @@
|
|
1
|
+
require 'api-pagination/hooks'
|
1
2
|
require 'api-pagination/version'
|
2
3
|
|
3
4
|
module ApiPagination
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
url = request.original_url.sub(/\?.*$/, '')
|
8
|
-
pages = {}
|
5
|
+
class << self
|
6
|
+
attr_writer :kaminari
|
7
|
+
attr_writer :will_paginate
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
pages[:prev] = scope.current_page - 1
|
13
|
-
end
|
9
|
+
def kaminari?() !!@kaminari end
|
10
|
+
def will_paginate?() !!@will_paginate end
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
12
|
+
def paginate(collection, options = {}, &block)
|
13
|
+
options[:page] ||= 1
|
14
|
+
options[:per_page] ||= 10
|
19
15
|
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
if ApiPagination.kaminari?
|
17
|
+
collection.page(options[:page]).per(options[:per_page]).tap(&block)
|
18
|
+
elsif ApiPagination.will_paginate?
|
19
|
+
collection.paginate(:page => options[:page], :per_page => options[:per_page]).tap(&block)
|
23
20
|
end
|
24
|
-
|
25
|
-
headers['Link'] = links.join(', ') unless links.empty?
|
26
21
|
end
|
27
|
-
end
|
28
22
|
|
29
|
-
|
30
|
-
|
23
|
+
def pages_from(collection)
|
24
|
+
{}.tap do |pages|
|
25
|
+
unless collection.first_page?
|
26
|
+
pages[:first] = 1
|
27
|
+
pages[:prev] = collection.current_page - 1
|
28
|
+
end
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
unless collection.last_page?
|
31
|
+
pages[:last] = collection.total_pages
|
32
|
+
pages[:next] = collection.current_page + 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
36
|
end
|
37
37
|
end
|
38
|
+
|
39
|
+
ApiPagination::Hooks.init
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ApiPagination
|
2
|
+
class Hooks
|
3
|
+
def self.init
|
4
|
+
begin; require 'rails'; rescue LoadError; end
|
5
|
+
if defined?(ActionController::Base)
|
6
|
+
require 'rails/pagination'
|
7
|
+
ActionController::Base.send(:include, Rails::Pagination)
|
8
|
+
end
|
9
|
+
|
10
|
+
begin; require 'rails-api'; rescue LoadError; end
|
11
|
+
if defined?(ActionController::API)
|
12
|
+
require 'rails/pagination'
|
13
|
+
ActionController::API.send(:include, Rails::Pagination)
|
14
|
+
end
|
15
|
+
|
16
|
+
begin; require 'grape'; rescue LoadError; end
|
17
|
+
if defined?(Grape::API)
|
18
|
+
require 'grape/pagination'
|
19
|
+
Grape::API.send(:include, Grape::Pagination)
|
20
|
+
end
|
21
|
+
|
22
|
+
begin; require 'will_paginate'; rescue LoadError; end
|
23
|
+
if defined?(WillPaginate::CollectionMethods)
|
24
|
+
WillPaginate::CollectionMethods.module_eval do
|
25
|
+
def first_page?() !previous_page end
|
26
|
+
def last_page?() !next_page end
|
27
|
+
end
|
28
|
+
|
29
|
+
ApiPagination.will_paginate = true
|
30
|
+
end
|
31
|
+
|
32
|
+
begin; require 'kaminari'; rescue LoadError; end
|
33
|
+
if defined?(Kaminari)
|
34
|
+
ApiPagination.kaminari = true
|
35
|
+
end
|
36
|
+
|
37
|
+
STDERR.puts <<-EOC unless defined?(Kaminari) || defined?(WillPaginate)
|
38
|
+
Warning: api-pagination relies on either Kaminari or WillPaginate. Please
|
39
|
+
install either dependency by adding one of the following to your Gemfile:
|
40
|
+
|
41
|
+
gem 'kaminari'
|
42
|
+
gem 'will_paginate'
|
43
|
+
|
44
|
+
EOC
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Grape
|
2
|
+
module Pagination
|
3
|
+
def self.included(base)
|
4
|
+
Grape::Endpoint.class_eval do
|
5
|
+
def paginate(collection)
|
6
|
+
block = Proc.new do |collection|
|
7
|
+
links = (header['Link'] || "").split(',').map(&:strip)
|
8
|
+
url = request.url.sub(/\?.*$/, '')
|
9
|
+
pages = ApiPagination.pages_from(collection)
|
10
|
+
|
11
|
+
pages.each do |k, v|
|
12
|
+
old_params = Rack::Utils.parse_query(request.query_string)
|
13
|
+
new_params = old_params.merge('page' => v)
|
14
|
+
links << %(<#{url}?#{new_params.to_param}>; rel="#{k}")
|
15
|
+
end
|
16
|
+
|
17
|
+
header 'Link', links.join(', ') unless links.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
ApiPagination.paginate(collection, params, &block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
base.class_eval do
|
25
|
+
def self.paginate(options = {})
|
26
|
+
options.reverse_merge!(:per_page => 10)
|
27
|
+
params do
|
28
|
+
optional :page, :type => Integer, :default => 1,
|
29
|
+
:desc => 'Page of results to fetch.'
|
30
|
+
optional :per_page, :type => Integer, :default => options[:per_page],
|
31
|
+
:desc => 'Number of results to return per page.'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rails
|
2
|
+
module Pagination
|
3
|
+
protected
|
4
|
+
|
5
|
+
def paginate(collection)
|
6
|
+
collection = instance_variable_get(:"@#{collection}")
|
7
|
+
|
8
|
+
block = Proc.new do |collection|
|
9
|
+
links = (headers['Link'] || "").split(',').map(&:strip)
|
10
|
+
url = request.original_url.sub(/\?.*$/, '')
|
11
|
+
pages = ApiPagination.pages_from(collection)
|
12
|
+
|
13
|
+
pages.each do |k, v|
|
14
|
+
new_params = request.query_parameters.merge(:page => v)
|
15
|
+
links << %(<#{url}?#{new_params.to_param}>; rel="#{k}")
|
16
|
+
end
|
17
|
+
|
18
|
+
headers['Link'] = links.join(', ') unless links.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
ApiPagination.paginate(collection, params, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/spec/grape_spec.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/shared_examples/existing_headers'
|
3
|
+
require 'support/shared_examples/first_page'
|
4
|
+
require 'support/shared_examples/middle_page'
|
5
|
+
require 'support/shared_examples/last_page'
|
6
|
+
|
7
|
+
describe NumbersAPI do
|
8
|
+
describe 'GET #index' do
|
9
|
+
let(:links) { last_response.headers['Link'].split(', ') }
|
10
|
+
|
11
|
+
context 'without enough items to give more than one page' do
|
12
|
+
it 'should not paginate' do
|
13
|
+
get :numbers, :count => 20
|
14
|
+
expect(last_response.headers.keys).not_to include('Link')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with existing Link headers' do
|
19
|
+
before { get :numbers, :count => 30, :with_headers => true }
|
20
|
+
|
21
|
+
it_behaves_like 'an endpoint with existing Link headers'
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with enough items to paginate' do
|
25
|
+
context 'when on the first page' do
|
26
|
+
before { get :numbers, :count => 100 }
|
27
|
+
|
28
|
+
it_behaves_like 'an endpoint with a first page'
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when on the last page' do
|
32
|
+
before { get :numbers, :count => 100, :page => 4 }
|
33
|
+
|
34
|
+
it_behaves_like 'an endpoint with a last page'
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when somewhere comfortably in the middle' do
|
38
|
+
before { get :numbers, :count => 100, :page => 2 }
|
39
|
+
|
40
|
+
it_behaves_like 'an endpoint with a middle page'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/rails_spec.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/shared_examples/existing_headers'
|
3
|
+
require 'support/shared_examples/first_page'
|
4
|
+
require 'support/shared_examples/middle_page'
|
5
|
+
require 'support/shared_examples/last_page'
|
6
|
+
|
7
|
+
describe NumbersController, :type => :controller do
|
8
|
+
before { request.host = 'example.org' }
|
9
|
+
describe 'GET #index' do
|
10
|
+
let(:links) { response.headers['Link'].split(', ') }
|
11
|
+
|
12
|
+
context 'without enough items to give more than one page' do
|
13
|
+
it 'should not paginate' do
|
14
|
+
get :index, :count => 20
|
15
|
+
expect(response.headers.keys).not_to include('Link')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with existing Link headers' do
|
20
|
+
before { get :index, :count => 30, :with_headers => true }
|
21
|
+
|
22
|
+
it_behaves_like 'an endpoint with existing Link headers'
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with enough items to paginate' do
|
26
|
+
context 'when on the first page' do
|
27
|
+
before { get :index, :count => 100 }
|
28
|
+
|
29
|
+
it_behaves_like 'an endpoint with a first page'
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when on the last page' do
|
33
|
+
before { get :index, :count => 100, :page => 4 }
|
34
|
+
|
35
|
+
it_behaves_like 'an endpoint with a last page'
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'when somewhere comfortably in the middle' do
|
39
|
+
before { get :index, :count => 100, :page => 2 }
|
40
|
+
|
41
|
+
it_behaves_like 'an endpoint with a middle page'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
|
data/spec/spec_helper.rb
CHANGED
@@ -1,49 +1,42 @@
|
|
1
|
-
require '
|
1
|
+
require 'support/numbers_controller'
|
2
|
+
require 'support/numbers_api'
|
2
3
|
require 'api-pagination'
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@application ||= begin
|
9
|
-
routes = ActionDispatch::Routing::RouteSet.new
|
10
|
-
OpenStruct.new(routes: routes, env_config: {})
|
11
|
-
end
|
4
|
+
|
5
|
+
# Quacks like Kaminari and will_paginate
|
6
|
+
PaginatedSet = Struct.new(:current_page, :per_page, :total_count) do
|
7
|
+
def total_pages
|
8
|
+
total_count.zero? ? 1 : (total_count.to_f / per_page).ceil
|
12
9
|
end
|
13
|
-
end
|
14
10
|
|
15
|
-
|
16
|
-
def
|
17
|
-
base.extend ClassMethods
|
18
|
-
base.send(:include, ActionController::TestCase::Behavior)
|
11
|
+
def first_page?() current_page == 1 end
|
12
|
+
def last_page?() current_page == total_pages end
|
19
13
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
14
|
+
def page(page)
|
15
|
+
current_page = page
|
16
|
+
self
|
24
17
|
end
|
25
18
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def teardown(*methods)
|
38
|
-
methods.each { |method| after { send method } }
|
39
|
-
end
|
19
|
+
def per(per)
|
20
|
+
per_page = per
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def paginate(options = {})
|
25
|
+
page(options[:page]).per(options[:per_page])
|
40
26
|
end
|
41
27
|
end
|
42
28
|
|
29
|
+
ApiPagination.kaminari = ENV['PAGINATOR'] == 'kaminari'
|
30
|
+
ApiPagination.will_paginate = ENV['PAGINATOR'] == 'will_paginate'
|
31
|
+
|
43
32
|
RSpec.configure do |config|
|
44
|
-
config.
|
45
|
-
config.
|
46
|
-
|
33
|
+
config.include Rack::Test::Methods
|
34
|
+
config.include ControllerExampleGroup, :type => :controller
|
35
|
+
|
36
|
+
# Disable the 'should' syntax.
|
37
|
+
config.expect_with :rspec do |c|
|
38
|
+
c.syntax = :expect
|
39
|
+
end
|
47
40
|
|
48
41
|
# Run specs in random order to surface order dependencies. If you find an
|
49
42
|
# order dependency and want to debug it, you can fix the order by providing
|
@@ -51,5 +44,7 @@ RSpec.configure do |config|
|
|
51
44
|
# --seed 1234
|
52
45
|
config.order = 'random'
|
53
46
|
|
54
|
-
|
47
|
+
def app
|
48
|
+
NumbersAPI
|
49
|
+
end
|
55
50
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'grape'
|
2
|
+
require 'api-pagination'
|
3
|
+
|
4
|
+
class NumbersAPI < Grape::API
|
5
|
+
format :json
|
6
|
+
|
7
|
+
desc 'Return some paginated set of numbers'
|
8
|
+
paginate :per_page => 25
|
9
|
+
params do
|
10
|
+
requires :count, :type => Integer
|
11
|
+
optional :with_headers, :default => false, :type => Boolean
|
12
|
+
end
|
13
|
+
get :numbers do
|
14
|
+
if params[:with_headers]
|
15
|
+
url = request.url.sub(/\?.*/, '')
|
16
|
+
query = Rack::Utils.parse_query(request.query_string)
|
17
|
+
query.delete('with_headers')
|
18
|
+
header 'Link', %(<#{url}?#{query.to_query}>; rel="without")
|
19
|
+
end
|
20
|
+
|
21
|
+
paginate PaginatedSet.new(params[:page], params[:per_page], params[:count])
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'action_controller'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module Rails
|
5
|
+
def self.application
|
6
|
+
@application ||= begin
|
7
|
+
routes = ActionDispatch::Routing::RouteSet.new
|
8
|
+
OpenStruct.new(:routes => routes, :env_config => {})
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ControllerExampleGroup
|
14
|
+
def self.included(base)
|
15
|
+
base.extend ClassMethods
|
16
|
+
base.send(:include, ActionController::TestCase::Behavior)
|
17
|
+
|
18
|
+
base.prepend_before do
|
19
|
+
@routes = Rails.application.routes
|
20
|
+
@controller = described_class.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def setup(*methods)
|
26
|
+
methods.each do |method|
|
27
|
+
if method.to_s =~ /^setup_(fixtures|controller_request_and_response)$/
|
28
|
+
prepend_before { send method }
|
29
|
+
else
|
30
|
+
before { send method }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def teardown(*methods)
|
36
|
+
methods.each { |method| after { send method } }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Rails.application.routes.draw do
|
42
|
+
resources :numbers, :only => [:index]
|
43
|
+
end
|
44
|
+
|
45
|
+
class NumbersController < ActionController::Base
|
46
|
+
include Rails.application.routes.url_helpers
|
47
|
+
|
48
|
+
after_filter :only => [:index] { paginate(:numbers) }
|
49
|
+
|
50
|
+
def index
|
51
|
+
page = params.fetch(:page, 1).to_i
|
52
|
+
total = params.fetch(:count).to_i
|
53
|
+
|
54
|
+
if params[:with_headers]
|
55
|
+
query = request.query_parameters.dup
|
56
|
+
query.delete(:with_headers)
|
57
|
+
headers['Link'] = %(<#{numbers_url}?#{query.to_param}>; rel="without")
|
58
|
+
end
|
59
|
+
|
60
|
+
@numbers = PaginatedSet.new(page, 25, total)
|
61
|
+
render :json => @numbers
|
62
|
+
end
|
63
|
+
end
|