pax_global_header00006660000000000000000000000064134026023270014511gustar00rootroot0000000000000052 comment=df0519936951af77fccd76a8da87d48287d603d6 api-pagination-4.8.2/000077500000000000000000000000001340260232700144245ustar00rootroot00000000000000api-pagination-4.8.2/.gitignore000066400000000000000000000002651340260232700164170ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc .ruby-version .ruby-gemset Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp api-pagination-4.8.2/.rspec000066400000000000000000000000321340260232700155340ustar00rootroot00000000000000--color --format progress api-pagination-4.8.2/.travis.yml000066400000000000000000000002551340260232700165370ustar00rootroot00000000000000language: ruby sudo: false rvm: - '2.2' - 2.3.6 - 2.4.3 - 2.5.0 script: bundle exec rspec env: - PAGINATOR=pagy - PAGINATOR=kaminari - PAGINATOR=will_paginate api-pagination-4.8.2/CONTRIBUTING.md000066400000000000000000000006011340260232700166520ustar00rootroot00000000000000## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes and tests (`git commit -am 'Add some feature'`) 4. Run the tests (`PAGINATOR=pagy bundle exec rspec; PAGINATOR=kaminari bundle exec rspec; PAGINATOR=will_paginate bundle exec rspec`) 5. Push to the branch (`git push origin my-new-feature`) 6. Create a new Pull Request api-pagination-4.8.2/Gemfile000066400000000000000000000004161340260232700157200ustar00rootroot00000000000000source 'https://rubygems.org' # Specify your gem's dependencies in api_pagination.gemspec gemspec gem 'kaminari', require: false gem 'will_paginate', require: false gem 'pagy', require: false gem 'sqlite3', require: false gem 'sequel', require: false gem 'pry-suite' api-pagination-4.8.2/LICENSE.txt000066400000000000000000000020531340260232700162470ustar00rootroot00000000000000Copyright © 2014 David Celis MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. api-pagination-4.8.2/README.md000066400000000000000000000205101340260232700157010ustar00rootroot00000000000000# api-pagination Paginate in your headers, not in your response body. This follows the proposed [RFC-8288](https://tools.ietf.org/html/rfc8288) standard for Web linking. ## Installation In your `Gemfile`: ```ruby # Requires Rails (Rails-API is also supported), or Grape # v0.10.0 or later. If you're on an earlier version of # Grape, use api-pagination v3.0.2. gem 'rails', '>= 3.0.0' gem 'rails-api' gem 'grape', '>= 0.10.0' # Then choose your preferred paginator from the following: gem 'pagy' gem 'kaminari' gem 'will_paginate' # Finally... gem 'api-pagination' ``` ## Configuration (optional) 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: ```ruby ApiPagination.configure do |config| # If you have more than one gem included, you can choose a paginator. config.paginator = :kaminari # or :will_paginate # By default, this is set to 'Total' config.total_header = 'X-Total' # By default, this is set to 'Per-Page' config.per_page_header = 'X-Per-Page' # Optional: set this to add a header with the current page number. config.page_header = 'X-Page' # Optional: set this to add other response format. Useful with tools that define :jsonapi format config.response_formats = [:json, :xml, :jsonapi] # Optional: what parameter should be used to set the page option config.page_param = :page # or config.page_param do |params| params[:page][:number] if params[:page].is_a?(ActionController::Parameters) end # Optional: what parameter should be used to set the per page option config.per_page_param = :per_page # or config.per_page_param do |params| params[:page][:size] if params[:page].is_a?(ActionController::Parameters) end # Optional: Include the total and last_page link header # By default, this is set to true # Note: When using kaminari, this prevents the count call to the database config.include_total = false end ``` ### Pagy-specific configuration 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: ```ruby Pagy::VARS[:max_per_page] = 100 ``` If left unconfigured, clients can request as many items per page as they wish, so it's highly recommended that you configure this. ## Rails In your controller, provide a pageable collection to the `paginate` method. In its most convenient form, `paginate` simply mimics `render`: ```ruby class MoviesController < ApplicationController # GET /movies def index movies = Movie.all # Movie.scoped if using ActiveRecord 3.x paginate json: movies end # GET /movies/:id/cast def cast actors = Movie.find(params[:id]).actors # Override how many Actors get returned. If unspecified, # params[:per_page] (which defaults to 25) will be used. paginate json: actors, per_page: 10 end end ``` 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` to receive a paginated collection and have your headers set. Then, you can pass that paginated collection to a serializer or do whatever you want with it: ```ruby class MoviesController < ApplicationController # GET /movies def index movies = paginate Movie.all render json: MoviesSerializer.new(movies) end # GET /movies/:id/cast def cast actors = paginate Movie.find(params[:id]).actors, per_page: 10 render json: ActorsSerializer.new(actors) end end ``` 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`. **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: ```ruby class API::ApplicationController < ActionController::Base include Rails::Pagination end ``` ## Grape With Grape, `paginate` is used to declare that your endpoint takes a `:page` and `:per_page` param. You can also directly specify a `:max_per_page` that users aren't allowed to go over. Then, inside your API endpoint, it simply takes your collection: ```ruby class MoviesAPI < Grape::API format :json desc 'Return a paginated set of movies' paginate get do # This method must take an ActiveRecord::Relation # or some equivalent pageable set. paginate Movie.all end route_param :id do desc "Return one movie's cast, paginated" # Override how many Actors get returned. If unspecified, # params[:per_page] (which defaults to 25) will be used. # There is no default for `max_per_page`. paginate per_page: 10, max_per_page: 200 get :cast do paginate Movie.find(params[:id]).actors end desc "Return one movie's awards, paginated" # Enforce max_per_page value will add the alowed values # to the swagger docs, and cause grape to return an error # if outside that range paginate per_page: 10, max_per_page: 200, enforce_max_per_page: true get :awards do paginate Movie.find(params[:id]).awards end end end ``` ## Headers Then `curl --include` to see your header-based pagination in action: ```bash $ curl --include 'https://localhost:3000/movies?page=5' HTTP/1.1 200 OK Link: ; rel="first", ; rel="last", ; rel="next", ; rel="prev" Total: 4321 Per-Page: 10 # ... ``` ## A Note on Kaminari and WillPaginate api-pagination requires either Kaminari or WillPaginate in order to function, but some users may find themselves in situations where their application includes both. For example, you may have included [ActiveAdmin][activeadmin] (which uses Kaminari for pagination) and WillPaginate to do your own pagination. While it's suggested that you remove one paginator gem or the other, if you're unable to do so, you _must_ configure api-pagination explicitly: ```ruby ApiPagination.configure do |config| config.paginator = :will_paginate end ``` If you don't do this, an annoying warning will print once your app starts seeing traffic. You should also configure Kaminari to use a different name for its `per_page` method (see https://github.com/activeadmin/activeadmin/wiki/How-to-work-with-will_paginate): ```ruby Kaminari.configure do |config| config.page_method_name = :per_page_kaminari end ``` [activeadmin]: https://github.com/activeadmin/activeadmin [kaminari]: https://github.com/amatsuda/kaminari [will_paginate]: https://github.com/mislav/will_paginate [travis]: https://travis-ci.org/davidcelis/api-pagination [travis-badge]: http://img.shields.io/travis/davidcelis/api-pagination/master.svg [coveralls]: https://coveralls.io/r/davidcelis/api-pagination [coveralls-badge]: http://img.shields.io/coveralls/davidcelis/api-pagination/master.svg [code-climate]: https://codeclimate.com/github/davidcelis/api-pagination [code-climate-badge]: http://img.shields.io/codeclimate/github/davidcelis/api-pagination.svg [gemnasium]: http://gemnasium.com/davidcelis/api-pagination [gemnasium-badge]: http://img.shields.io/gemnasium/davidcelis/api-pagination.svg [gittip]: https://gittip.com/davidcelis [gittip-badge]: http://img.shields.io/gittip/davidcelis.svg api-pagination-4.8.2/api-pagination.gemspec000066400000000000000000000017371340260232700207010ustar00rootroot00000000000000# encoding: utf-8 $:.unshift(File.expand_path('../lib', __FILE__)) require 'api-pagination/version' Gem::Specification.new do |s| s.name = 'api-pagination' s.version = ApiPagination::VERSION s.authors = ['David Celis'] s.email = ['me@davidcel.is'] s.description = 'Link header pagination for Rails and Grape APIs' s.summary = "Link header pagination for Rails and Grape APIs. Don't use the request body." s.homepage = 'https://github.com/davidcelis/api-pagination' s.license = 'MIT' s.files = Dir['lib/**/*'] s.test_files = Dir['spec/**/*'] s.require_paths = ['lib'] s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'grape', '>= 0.10.0' s.add_development_dependency 'railties', '>= 5.0.0' s.add_development_dependency 'actionpack', '>= 5.0.0' s.add_development_dependency 'sequel', '>= 4.9.0' s.add_development_dependency 'activerecord-nulldb-adapter', '>= 0.3.9' end api-pagination-4.8.2/lib/000077500000000000000000000000001340260232700151725ustar00rootroot00000000000000api-pagination-4.8.2/lib/api-pagination.rb000066400000000000000000000111541340260232700204210ustar00rootroot00000000000000require 'api-pagination/configuration' require 'api-pagination/version' module ApiPagination class << self def paginate(collection, options = {}) options[:page] = options[:page].to_i options[:page] = 1 if options[:page] <= 0 options[:per_page] = options[:per_page].to_i case ApiPagination.config.paginator when :pagy paginate_with_pagy(collection, options) when :kaminari paginate_with_kaminari(collection, options, options[:paginate_array_options] || {}) when :will_paginate paginate_with_will_paginate(collection, options) else raise StandardError, "Unknown paginator: #{ApiPagination.config.paginator}" end end def pages_from(collection, options = {}) return pagy_pages_from(collection) if ApiPagination.config.paginator == :pagy && collection.is_a?(Pagy) {}.tap do |pages| unless collection.first_page? pages[:first] = 1 pages[:prev] = collection.current_page - 1 end unless collection.last_page? || (ApiPagination.config.paginator == :kaminari && collection.out_of_range?) pages[:last] = collection.total_pages if ApiPagination.config.include_total pages[:next] = collection.current_page + 1 end end end def total_from(collection) case ApiPagination.config.paginator when :pagy then collection.count.to_s when :kaminari then collection.total_count.to_s when :will_paginate then collection.total_entries.to_s end end private def paginate_with_pagy(collection, options) if Pagy::VARS[:max_per_page] && options[:per_page] > Pagy::VARS[:max_per_page] options[:per_page] = Pagy::VARS[:max_per_page] elsif options[:per_page] <= 0 options[:per_page] = Pagy::VARS[:items] end pagy = pagy_from(collection, options) collection = if collection.respond_to?(:offset) && collection.respond_to?(:limit) collection.offset(pagy.offset).limit(pagy.items) else collection[pagy.offset, pagy.items] end return [collection, pagy] end def pagy_from(collection, options) if options[:count] count = options[:count] else count = collection.is_a?(Array) ? collection.count : collection.count(:all) end Pagy.new(count: count, items: options[:per_page], page: options[:page]) end def pagy_pages_from(pagy) {}.tap do |pages| unless pagy.page == 1 pages[:first] = 1 pages[:prev] = pagy.prev end unless pagy.page == pagy.pages pages[:last] = pagy.pages if ApiPagination.config.include_total pages[:next] = pagy.next end end end def paginate_with_kaminari(collection, options, paginate_array_options = {}) if Kaminari.config.max_per_page && options[:per_page] > Kaminari.config.max_per_page options[:per_page] = Kaminari.config.max_per_page elsif options[:per_page] <= 0 options[:per_page] = get_default_per_page_for_kaminari(collection) end collection = Kaminari.paginate_array(collection, paginate_array_options) if collection.is_a?(Array) collection = collection.page(options[:page]).per(options[:per_page]) collection.without_count if !collection.is_a?(Array) && !ApiPagination.config.include_total [collection, nil] end def paginate_with_will_paginate(collection, options) if options[:per_page] <= 0 options[:per_page] = default_per_page_for_will_paginate(collection) end collection = if defined?(Sequel::Dataset) && collection.kind_of?(Sequel::Dataset) collection.paginate(options[:page], options[:per_page]) else supported_options = [:page, :per_page, :total_entries] options = options.dup.keep_if { |k,v| supported_options.include?(k.to_sym) } collection.paginate(options) end [collection, nil] end def get_default_per_page_for_kaminari(collection) default = Kaminari.config.default_per_page extract_per_page_from_model(collection, :default_per_page) || default end def default_per_page_for_will_paginate(collection) default = WillPaginate.per_page extract_per_page_from_model(collection, :per_page) || default end def extract_per_page_from_model(collection, accessor) klass = if collection.respond_to?(:klass) collection.klass else collection.first.class end return unless klass.respond_to?(accessor) klass.send(accessor) end end end require 'api-pagination/hooks' api-pagination-4.8.2/lib/api-pagination/000077500000000000000000000000001340260232700200725ustar00rootroot00000000000000api-pagination-4.8.2/lib/api-pagination/configuration.rb000066400000000000000000000065361340260232700233000ustar00rootroot00000000000000module ApiPagination class Configuration attr_accessor :total_header attr_accessor :per_page_header attr_accessor :page_header attr_accessor :include_total attr_accessor :base_url attr_accessor :response_formats def configure(&block) yield self end def initialize @total_header = 'Total' @per_page_header = 'Per-Page' @page_header = nil @include_total = true @base_url = nil @response_formats = [:json, :xml] end ['page', 'per_page'].each do |param_name| method_name = "#{param_name}_param" instance_variable_name = "@#{method_name}" define_method method_name do |params = nil, &block| if block.is_a?(Proc) instance_variable_set(instance_variable_name, block) return end if instance_variable_get(instance_variable_name).nil? # use :page & :per_page by default instance_variable_set(instance_variable_name, (lambda { |p| p[param_name.to_sym] })) end instance_variable_get(instance_variable_name).call(params) end define_method "#{method_name}=" do |param| if param.is_a?(Symbol) || param.is_a?(String) instance_variable_set(instance_variable_name, (lambda { |params| params[param] })) else raise ArgumentError, "Cannot set page_param option" end end end def paginator if instance_variable_defined? :@paginator @paginator else set_paginator end end def paginator=(paginator) case paginator.to_sym when :pagy use_pagy when :kaminari use_kaminari when :will_paginate use_will_paginate else raise StandardError, "Unknown paginator: #{paginator}" end end private def set_paginator conditions = [defined?(Pagy), defined?(Kaminari), defined?(WillPaginate::CollectionMethods)] if conditions.compact.size > 1 Kernel.warn <<-WARNING Warning: api-pagination relies on Pagy, Kaminari, or WillPaginate, but more than one are currently active. If possible, you should remove one or the other. If you can't, you _must_ configure api-pagination on your own. For example: ApiPagination.configure do |config| config.paginator = :kaminari end You should also configure Kaminari to use a different `per_page` method name as using these gems together causes a conflict; some information can be found at https://github.com/activeadmin/activeadmin/wiki/How-to-work-with-will_paginate Kaminari.configure do |config| config.page_method_name = :per_page_kaminari end WARNING elsif defined?(Pagy) use_pagy elsif defined?(Kaminari) use_kaminari elsif defined?(WillPaginate::CollectionMethods) use_will_paginate end end def use_pagy @paginator = :pagy end def use_kaminari require 'kaminari/models/array_extension' @paginator = :kaminari end def use_will_paginate WillPaginate::CollectionMethods.module_eval do def first_page?() !previous_page end def last_page?() !next_page end end @paginator = :will_paginate end end class << self def configure yield config end def config @config ||= Configuration.new end alias :configuration :config end end api-pagination-4.8.2/lib/api-pagination/hooks.rb000066400000000000000000000021461340260232700215450ustar00rootroot00000000000000begin; require 'grape'; rescue LoadError; end if defined?(Grape::API) require 'grape/pagination' klass = if Grape::VERSION >= '1.2.0' || defined?(Grape::API::Instance) Grape::API::Instance else Grape::API end klass.send(:include, Grape::Pagination) end begin; require 'pagy'; rescue LoadError; end begin; require 'kaminari'; rescue LoadError; end begin; require 'will_paginate'; rescue LoadError; end unless defined?(Pagy) || defined?(Kaminari) || defined?(WillPaginate::CollectionMethods) Kernel.warn <<-WARNING.gsub(/^\s{4}/, '') Warning: api-pagination relies on either Pagy, Kaminari, or WillPaginate. Please install a paginator by adding one of the following to your Gemfile: gem 'pagy' gem 'kaminari' gem 'will_paginate' WARNING end if defined?(Rails) module ApiPagination module Hooks def self.rails_parent_controller if Rails::VERSION::MAJOR >= 5 || defined?(ActionController::API) ActionController::API else ActionController::Base end end end end require 'api-pagination/railtie' end api-pagination-4.8.2/lib/api-pagination/railtie.rb000066400000000000000000000007031340260232700220500ustar00rootroot00000000000000require 'rails/railtie' module ApiPagination class Railtie < ::Rails::Railtie initializer :api_pagination do ActiveSupport.on_load(:action_controller) do require 'rails/pagination' klass = if Rails::VERSION::MAJOR >= 5 || defined?(ActionController::API) ActionController::API else ActionController::Base end klass.send(:include, Rails::Pagination) end end end end api-pagination-4.8.2/lib/api-pagination/version.rb000066400000000000000000000002641340260232700221060ustar00rootroot00000000000000module ApiPagination class Version MAJOR = 4 MINOR = 8 PATCH = 2 def self.to_s [MAJOR, MINOR, PATCH].join('.') end end VERSION = Version.to_s end api-pagination-4.8.2/lib/grape/000077500000000000000000000000001340260232700162705ustar00rootroot00000000000000api-pagination-4.8.2/lib/grape/pagination.rb000066400000000000000000000044131340260232700207500ustar00rootroot00000000000000module Grape module Pagination def self.included(base) Grape::Endpoint.class_eval do def paginate(collection) per_page = ApiPagination.config.per_page_param(params) || route_setting(:per_page) options = { :page => ApiPagination.config.page_param(params), :per_page => [per_page, route_setting(:max_per_page)].compact.min } collection, pagy = ApiPagination.paginate(collection, options) links = (header['Link'] || "").split(',').map(&:strip) url = request.url.sub(/\?.*$/, '') pages = ApiPagination.pages_from(pagy || collection, options) pages.each do |k, v| old_params = Rack::Utils.parse_nested_query(request.query_string) new_params = old_params.merge('page' => v) links << %(<#{url}?#{new_params.to_param}>; rel="#{k}") end total_header = ApiPagination.config.total_header per_page_header = ApiPagination.config.per_page_header page_header = ApiPagination.config.page_header include_total = ApiPagination.config.include_total header 'Link', links.join(', ') unless links.empty? header total_header, ApiPagination.total_from(pagy || collection).to_s if include_total header per_page_header, options[:per_page].to_s header page_header, options[:page].to_s unless page_header.nil? return collection end end base.class_eval do def self.paginate(options = {}) route_setting :per_page, options[:per_page] route_setting :max_per_page, options[:max_per_page] enforce_max_per_page = options[:max_per_page] && options[:enforce_max_per_page] per_page_values = enforce_max_per_page ? 0..options[:max_per_page] : nil params do optional :page, :type => Integer, :default => 1, :desc => 'Page of results to fetch.' optional :per_page, :type => Integer, :default => options[:per_page], :desc => 'Number of results to return per page.', :values => per_page_values end end end end end end api-pagination-4.8.2/lib/rails/000077500000000000000000000000001340260232700163045ustar00rootroot00000000000000api-pagination-4.8.2/lib/rails/pagination.rb000066400000000000000000000045201340260232700207630ustar00rootroot00000000000000module Rails module Pagination protected def paginate(*options_or_collection) options = options_or_collection.extract_options! collection = options_or_collection.first return _paginate_collection(collection, options) if collection response_format = _discover_format(options) collection = options[response_format] collection = _paginate_collection(collection, options) options[response_format] = collection if options[response_format] render options end def paginate_with(collection) respond_with _paginate_collection(collection) end private def _discover_format(options) for response_format in ApiPagination.config.response_formats return response_format if options.key?(response_format) end end def _paginate_collection(collection, options={}) options[:page] = ApiPagination.config.page_param(params) options[:per_page] ||= ApiPagination.config.per_page_param(params) collection, pagy = ApiPagination.paginate(collection, options) links = (headers['Link'] || '').split(',').map(&:strip) url = base_url + request.path_info pages = ApiPagination.pages_from(pagy || collection, options) pages.each do |k, v| new_params = request.query_parameters.merge(:page => v) links << %(<#{url}?#{new_params.to_param}>; rel="#{k}") end total_header = ApiPagination.config.total_header per_page_header = ApiPagination.config.per_page_header page_header = ApiPagination.config.page_header include_total = ApiPagination.config.include_total headers['Link'] = links.join(', ') unless links.empty? headers[per_page_header] = options[:per_page].to_s headers[page_header] = options[:page].to_s unless page_header.nil? headers[total_header] = total_count(pagy || collection, options).to_s if include_total return collection end def total_count(collection, options) total_count = if ApiPagination.config.paginator == :kaminari paginate_array_options = options[:paginate_array_options] paginate_array_options[:total_count] if paginate_array_options end total_count || ApiPagination.total_from(collection) end def base_url ApiPagination.config.base_url || request.base_url end end end api-pagination-4.8.2/spec/000077500000000000000000000000001340260232700153565ustar00rootroot00000000000000api-pagination-4.8.2/spec/active_record_spec.rb000066400000000000000000000027331340260232700215330ustar00rootroot00000000000000require 'spec_helper' require 'support/active_record/foo' require 'nulldb_rspec' ActiveRecord::Base.establish_connection( adapter: :nulldb, schema: 'spec/support/active_record/schema.rb' ) NullDB.configure { |ndb| def ndb.project_root; Dir.pwd; end; } shared_examples 'produces_correct_sql' do it 'produces correct sql for first page' do allow(collection).to receive(:count).and_return(collection_size) paginated_sql, _ = ApiPagination.paginate(collection, per_page: per_page) expect(paginated_sql.to_sql).to eql(Foo.limit(per_page).offset(0).to_sql) end end describe 'ActiveRecord Support' do let(:collection) { Foo.all } let(:collection_size) { 50 } let(:per_page) { 5 } if ApiPagination.config.paginator == :will_paginate require 'will_paginate/active_record' end context "pagination with #{ApiPagination.config.paginator}" do include_examples 'produces_correct_sql' end if ApiPagination.config.paginator != :pagy context 'reflections' do it 'invokes the correct methods to determine type' do expect(collection).to receive(:klass).at_least(:once) .and_call_original ApiPagination.paginate(collection) end it 'does not fail if table name is not snake cased class name' do allow(collection).to receive(:table_name).and_return(SecureRandom.uuid) expect { ApiPagination.paginate(collection) }.to_not raise_error end end end end api-pagination-4.8.2/spec/api-pagination_spec.rb000066400000000000000000000050451340260232700216210ustar00rootroot00000000000000require 'spec_helper' describe ApiPagination do let(:collection) {(1..100).to_a} let(:active_record_relation) {double("ActiveRecord_Relation").as_null_object} let(:paginate_array_options) {{ total_count: 1000 }} describe "#paginate" do if ENV['PAGINATOR'].to_sym == :kaminari context 'Using kaminari' do describe '.paginate' do it 'should accept paginate_array_options option' do expect(Kaminari).to receive(:paginate_array) .with(collection, paginate_array_options) .and_call_original ApiPagination.paginate( collection, { per_page: 30, paginate_array_options: paginate_array_options } ) end context 'configured not to include the total' do before { ApiPagination.config.include_total = false } context 'and paginating an array' do it 'should not call without_count on the collection' do expect(collection).to_not receive :without_count ApiPagination.paginate(collection) end end context 'and paginating an active record relation' do it 'should call without_count on the relation' do expect(active_record_relation).to receive :without_count ApiPagination.paginate(active_record_relation) end end after { ApiPagination.config.include_total = true } end end describe '.pages_from' do subject { described_class.pages_from(collection) } context 'on empty collection' do let(:collection) { ApiPagination.paginate([], page: 1).first } it { is_expected.to be_empty } end end end end if ENV['PAGINATOR'].to_sym == :will_paginate context 'Using will_paginate' do context 'passing in total_entries in options' do it 'should set total_entries using the passed in value' do paginated_collection = ApiPagination.paginate(collection, total_entries: 3000).first expect(paginated_collection.total_entries).to eq(3000) end end context 'passing in collection only' do it 'should set total_entries using the size of the collection ' do paginated_collection = ApiPagination.paginate(collection).first expect(paginated_collection.total_entries).to eq(100) end end end end end end api-pagination-4.8.2/spec/grape_spec.rb000066400000000000000000000122411340260232700200130ustar00rootroot00000000000000require 'spec_helper' require 'support/shared_examples/existing_headers' require 'support/shared_examples/first_page' require 'support/shared_examples/middle_page' require 'support/shared_examples/last_page' describe NumbersAPI do it { is_expected.to be_kind_of(Grape::Pagination) } describe 'GET #index' do let(:link) { last_response.headers['Link'] } let(:links) { link.split(', ') } let(:total) { last_response.headers['Total'].to_i } let(:per_page) { last_response.headers['Per-Page'].to_i } context 'without enough items to give more than one page' do before { get '/numbers', :count => 10 } it 'should not paginate' do expect(last_response.headers.keys).not_to include('Link') end it 'should give a Total header' do expect(total).to eq(10) end it 'should give a Per-Page header' do expect(per_page).to eq(10) end it 'should list all numbers in the response body' do body = '[1,2,3,4,5,6,7,8,9,10]' expect(last_response.body).to eq(body) end end context 'with existing Link headers' do before { get '/numbers', :count => 30, :with_headers => true } it_behaves_like 'an endpoint with existing Link headers' end context 'with enough items to paginate' do context 'when on the first page' do before { get '/numbers', :count => 100 } it_behaves_like 'an endpoint with a first page' end context 'when on the last page' do before { get '/numbers', :count => 100, :page => 10 } it_behaves_like 'an endpoint with a last page' end context 'when somewhere comfortably in the middle' do before { get '/numbers', :count => 100, :page => 2 } it_behaves_like 'an endpoint with a middle page' end context 'without a max_per_page setting' do before { get '/numbers', :count => 100, :per_page => 30 } it 'should list all numbers within per page in the response body' do body = '[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]' expect(last_response.body).to eq(body) end end context 'with a max_per_page setting not enforced' do before { get '/numbers_with_max_per_page', :count => 100, :per_page => 30 } it 'should not go above the max_per_page_limit' do body = '[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]' expect(last_response.body).to eq(body) end end context 'with a max_per_page setting enforced' do before { get '/numbers_with_enforced_max_per_page', :count => 100, :per_page => 30 } it 'should not allow value above the max_per_page_limit' do body = '{"error":"per_page does not have a valid value"}' expect(last_response.body).to eq(body) end end end context 'with custom response headers' do before do ApiPagination.config.total_header = 'X-Total-Count' ApiPagination.config.per_page_header = 'X-Per-Page' ApiPagination.config.page_header = 'X-Page' get '/numbers', count: 10 end after do ApiPagination.config.total_header = 'Total' ApiPagination.config.per_page_header = 'Per-Page' ApiPagination.config.page_header = nil end let(:total) { last_response.header['X-Total-Count'].to_i } let(:per_page) { last_response.header['X-Per-Page'].to_i } let(:page) { last_response.header['X-Page'].to_i } it 'should give a X-Total-Count header' do headers_keys = last_response.headers.keys expect(headers_keys).not_to include('Total') expect(headers_keys).to include('X-Total-Count') expect(total).to eq(10) end it 'should give a X-Per-Page header' do headers_keys = last_response.headers.keys expect(headers_keys).not_to include('Per-Page') expect(headers_keys).to include('X-Per-Page') expect(per_page).to eq(10) end it 'should give a X-Page header' do headers_keys = last_response.headers.keys expect(headers_keys).to include('X-Page') expect(page).to eq(1) end end context 'configured not to include the total' do before { ApiPagination.config.include_total = false } it 'should not include a Total header' do get '/numbers', count: 10 expect(last_response.header['Total']).to be_nil end it 'should not include a link with rel "last"' do get '/numbers', count: 100 expect(link).to_not include('rel="last"') end after { ApiPagination.config.include_total = true } end context 'with query string including array parameter' do before do get '/numbers', { count: 100, parity: ['odd', 'even']} end it 'returns links with with same received parameters' do expect(links).to include('; rel="last"') expect(links).to include('; rel="next"') end end end end api-pagination-4.8.2/spec/rails_spec.rb000066400000000000000000000215201340260232700200270ustar00rootroot00000000000000require 'spec_helper' require 'support/shared_examples/existing_headers' require 'support/shared_examples/first_page' require 'support/shared_examples/middle_page' require 'support/shared_examples/last_page' describe NumbersController, :type => :controller do before { request.host = 'example.org' } describe 'GET #index' do let(:link) { response.headers['Link'] } let(:links) { link.split(', ') } let(:total) { response.headers['Total'].to_i } let(:per_page) { response.headers['Per-Page'].to_i } context 'without enough items to give more than one page' do before { get :index, params: {count: 10} } it 'should not paginate' do expect(response.headers.keys).not_to include('Link') end it 'should give a Total header' do expect(total).to eq(10) end it 'should give a Per-Page header' do expect(per_page).to eq(10) end it 'should list all numbers in the response body' do body = '[1,2,3,4,5,6,7,8,9,10]' expect(response.body).to eq(body) end end context 'with existing Link headers' do before { get :index, params: {count: 30, with_headers: true} } it_behaves_like 'an endpoint with existing Link headers' end context 'with enough items to paginate' do context 'when on the first page' do before { get :index, params: {count: 100} } it_behaves_like 'an endpoint with a first page' end context 'when on the last page' do before { get :index, params: {count: 100, page: 10} } it_behaves_like 'an endpoint with a last page' end context 'when somewhere comfortably in the middle' do before { get :index, params: {count: 100, page: 2} } it_behaves_like 'an endpoint with a middle page' end end context 'providing a block' do it 'yields to the block instead of implicitly rendering' do get :index_with_custom_render, params: {count: 100} json = { numbers: (1..10).map { |n| { number: n } } }.to_json expect(response.body).to eq(json) end end context 'with custom response headers' do before do ApiPagination.config.total_header = 'X-Total-Count' ApiPagination.config.per_page_header = 'X-Per-Page' ApiPagination.config.page_header = 'X-Page' ApiPagination.config.base_url = 'http://guybrush:3000' get :index, params: params end after do ApiPagination.config.total_header = 'Total' ApiPagination.config.per_page_header = 'Per-Page' ApiPagination.config.page_header = nil ApiPagination.config.base_url = nil end let(:params) { { count: 10 } } let(:total) { response.header['X-Total-Count'].to_i } let(:per_page) { response.header['X-Per-Page'].to_i } let(:page) { response.header['X-Page'].to_i } let(:link) { response.header['Link'] } it 'should give a X-Total-Count header' do headers_keys = response.headers.keys expect(headers_keys).not_to include('Total') expect(headers_keys).to include('X-Total-Count') expect(total).to eq(10) end it 'should give a X-Per-Page header' do headers_keys = response.headers.keys expect(headers_keys).not_to include('Per-Page') expect(headers_keys).to include('X-Per-Page') expect(per_page).to eq(10) end it 'should give a X-Page header' do headers_keys = response.headers.keys expect(headers_keys).to include('X-Page') expect(page).to eq(1) end context 'with paginated result' do let(:params) { { count: 20 } } it 'should use custom base_url in the Link header' do expect(response.headers['Link']).to eq( '; rel="last", ; rel="next"') end end end context 'configured not to include the total' do before { ApiPagination.config.include_total = false } it 'should not include a Total header' do get :index, params: {count: 10} expect(response.header['Total']).to be_nil end it 'should not include a link with rel "last"' do get :index, params: { count: 100 } expect(link).to_not include('rel="last"') end after { ApiPagination.config.include_total = true } end context 'custom page param' do context 'page_param as a symbol' do before do ApiPagination.config.page_param = :foo ApiPagination.config.page_header = 'Page' end after do ApiPagination.config.page_param = :page ApiPagination.config.page_header = nil end it 'should work' do get :index, params: {foo: 2, count: 100} expect(response.header['Page']).to eq('2') end end context 'page_param as a block' do before do ApiPagination.config.page_param do |params| params[:foo][:bar] end ApiPagination.config.page_header = 'Page' end after do ApiPagination.config.page_param = :page ApiPagination.config.page_header = nil end it 'should work' do get :index, params: {foo: {bar: 2}, count: 100} expect(response.header['Page']).to eq('2') end end end context 'custom per_page param' do context 'per_page_param as a symbol' do before do ApiPagination.config.per_page_param = :foo end after do ApiPagination.config.per_page_param = :per_page end it 'should work' do get :index_with_no_per_page, params: {foo: 2, count: 100} expect(response.header['Per-Page']).to eq('2') end end context 'page_param as a block' do before do ApiPagination.config.per_page_param do |params| params[:foo][:bar] end end after do ApiPagination.config.per_page_param = :per_page end it 'should work' do get :index_with_no_per_page, params: {foo: {bar: 2}, count: 100} expect(response.header['Per-Page']).to eq('2') end end end if ApiPagination.config.paginator.to_sym == :kaminari context 'paginate array options' do let(:paginate_array_total_count) { 300 } let(:total_header) { 300 } let(:count) { 50 } let(:params) do { paginate_array_total_count: paginate_array_total_count, count: count, } end it 'has a properly set Total header' do get :index_with_paginate_array_options, params: params expect(response.header['Total']).to be_kind_of(String) expect(response.header['Total'].to_i).to eq total_header end end end if [:will_paginate, :kaminari].include?(ApiPagination.config.paginator.to_sym) context 'default per page in model' do before do class Fixnum @default_per_page = 6 @per_page = 6 class << self attr_accessor :default_per_page, :per_page end end end after do class Fixnum @default_per_page = 25 @per_page = 25 end end after :all do class Fixnum class << self undef_method :default_per_page, :per_page end end end it 'should use default per page from model' do get :index_with_no_per_page, params: {count: 100} expect(response.header['Per-Page']).to eq('6') end it 'should not fail if the model yields nil for per page' do class Fixnum @default_per_page = nil @per_page = nil end get :index_with_no_per_page, params: {count: 100} expect(response.header['Per-Page']).to eq( case ApiPagination.config.paginator when :pagy then Pagy::VARS[:items].to_s when :kaminari then Kaminari.config.default_per_page.to_s when :will_paginate then WillPaginate.per_page.to_s end ) end end end context 'default per page in objects without paginator defaults' do it 'should not fail if model does not respond to per page' do get :index_with_no_per_page, params: {count: 100} expect(response.header['Per-Page']).to eq( case ApiPagination.config.paginator when :pagy then Pagy::VARS[:items].to_s when :kaminari then Kaminari.config.default_per_page.to_s when :will_paginate then WillPaginate.per_page.to_s end ) end end end endapi-pagination-4.8.2/spec/sequel_spec.rb000066400000000000000000000011761340260232700202200ustar00rootroot00000000000000require 'spec_helper' if ApiPagination.config.paginator == :will_paginate require 'sqlite3' require 'sequel' require 'will_paginate/sequel' DB = Sequel.sqlite DB.extension :pagination DB.create_table :people do primary_key :id String :name end describe 'Using will_paginate with Sequel' do let(:people) do DB[:people] end before(:each) do people.insert(name: 'John') people.insert(name: 'Mary') end it 'returns a Sequel::Dataset' do collection = ApiPagination.paginate(people).first expect(collection.kind_of?(Sequel::Dataset)).to be_truthy end end end api-pagination-4.8.2/spec/spec_helper.rb000066400000000000000000000020111340260232700201660ustar00rootroot00000000000000require 'support/numbers_controller' require 'support/numbers_api' require 'api-pagination' if ENV['PAGINATOR'].nil? warn <<-WARNING No PAGINATOR set. Defaulting to pagy. To test against kaminari, run `PAGINATOR=kaminari bundle exec rspec` To test against will_paginate, run `PAGINATOR=will_paginate bundle exec rspec` WARNING ENV['PAGINATOR'] = 'pagy' end require ENV['PAGINATOR'] ApiPagination.config.paginator = ENV['PAGINATOR'].to_sym require 'will_paginate/array' if ENV['PAGINATOR'].to_sym == :will_paginate RSpec.configure do |config| config.include Rack::Test::Methods config.include ControllerExampleGroup, :type => :controller # Disable the 'should' syntax. config.expect_with :rspec do |c| c.syntax = :expect end # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = 'random' def app NumbersAPI end end api-pagination-4.8.2/spec/support/000077500000000000000000000000001340260232700170725ustar00rootroot00000000000000api-pagination-4.8.2/spec/support/active_record/000077500000000000000000000000001340260232700217035ustar00rootroot00000000000000api-pagination-4.8.2/spec/support/active_record/foo.rb000066400000000000000000000000741340260232700230140ustar00rootroot00000000000000require 'active_record' class Foo < ActiveRecord::Base; endapi-pagination-4.8.2/spec/support/active_record/schema.rb000066400000000000000000000001661340260232700234730ustar00rootroot00000000000000ActiveRecord::Schema.define(version: 0) do create_table "foos", :force => true do |t| t.string "foo" end endapi-pagination-4.8.2/spec/support/numbers_api.rb000066400000000000000000000021611340260232700217230ustar00rootroot00000000000000require 'grape' require 'api-pagination' class NumbersAPI < Grape::API format :json desc 'Return some paginated set of numbers' paginate :per_page => 10 params do requires :count, :type => Integer optional :with_headers, :default => false, :type => Boolean end get :numbers do if params[:with_headers] url = request.url.sub(/\?.*/, '') query = Rack::Utils.parse_query(request.query_string) query.delete('with_headers') header 'Link', %(<#{url}?#{query.to_query}>; rel="without") end paginate (1..params[:count]).to_a end desc 'Return some paginated set of numbers with max_per_page' paginate :per_page => 10, :max_per_page => 25 params do requires :count, :type => Integer end get :numbers_with_max_per_page do paginate (1..params[:count]).to_a end desc 'Return some paginated set of numbers with max_per_page enforced' paginate :per_page => 10, :max_per_page => 25, :enforce_max_per_page => true params do requires :count, :type => Integer end get :numbers_with_enforced_max_per_page do paginate (1..params[:count]).to_a end end api-pagination-4.8.2/spec/support/numbers_controller.rb000066400000000000000000000047001340260232700233360ustar00rootroot00000000000000require 'action_controller/railtie' require 'api-pagination/hooks' require 'ostruct' module Rails def self.application @application ||= begin routes = ActionDispatch::Routing::RouteSet.new OpenStruct.new(:routes => routes, :env_config => {}) end end end module ControllerExampleGroup def self.included(base) base.extend ClassMethods base.send(:include, ActionController::TestCase::Behavior) base.prepend_before do @routes = Rails.application.routes @controller = described_class.new end end module ClassMethods def setup(*methods) methods.each do |method| if method.to_s =~ /^setup_(fixtures|controller_request_and_response)$/ prepend_before { send method } else before { send method } end end end def teardown(*methods) methods.each { |method| after { send method } } end end end Rails.application.routes.draw do resources :numbers, :only => [:index] do collection do get :index_with_custom_render get :index_with_no_per_page get :index_with_paginate_array_options end end end class NumbersSerializer def initialize(numbers) @numbers = numbers end def to_json(options = {}) { numbers: @numbers.map { |n| { number: n } } }.to_json end end class NumbersController < ActionController::API include Rails.application.routes.url_helpers def index total = params.fetch(:count).to_i if params[:with_headers] query = request.query_parameters.dup query.delete(:with_headers) headers['Link'] = %(<#{numbers_url}?#{query.to_param}>; rel="without") end paginate :json => (1..total).to_a, :per_page => 10 end def index_with_custom_render total = params.fetch(:count).to_i numbers = (1..total).to_a numbers = paginate numbers, :per_page => 10 render json: NumbersSerializer.new(numbers) end def index_with_no_per_page total = params.fetch(:count).to_i numbers = (1..total).to_a numbers = paginate numbers render json: NumbersSerializer.new(numbers) end def index_with_paginate_array_options count = params.fetch(:count).to_i total_count = params.fetch(:paginate_array_total_count).to_i numbers = (1..count).to_a numbers = paginate numbers, paginate_array_options: {total_count: total_count} render json: NumbersSerializer.new(numbers) end end ApiPagination::Railtie.initializers.each(&:run) api-pagination-4.8.2/spec/support/shared_examples/000077500000000000000000000000001340260232700222365ustar00rootroot00000000000000api-pagination-4.8.2/spec/support/shared_examples/existing_headers.rb000066400000000000000000000010171340260232700261070ustar00rootroot00000000000000shared_examples 'an endpoint with existing Link headers' do it 'should keep existing Links' do expect(links).to include('; rel="without"') end it 'should contain pagination Links' do expect(links).to include('; rel="next"') expect(links).to include('; rel="last"') end it 'should give a Total header' do expect(total).to eq(30) end end api-pagination-4.8.2/spec/support/shared_examples/first_page.rb000066400000000000000000000015321340260232700247070ustar00rootroot00000000000000shared_examples 'an endpoint with a first page' do it 'should not give a link with rel "first"' do expect(link).not_to include('rel="first"') end it 'should not give a link with rel "prev"' do expect(link).not_to include('rel="prev"') end it 'should give a link with rel "last"' do expect(links).to include('; rel="last"') end it 'should give a link with rel "next"' do expect(links).to include('; rel="next"') end it 'should give a Total header' do expect(total).to eq(100) end it 'should list the first page of numbers in the response body' do body = '[1,2,3,4,5,6,7,8,9,10]' if defined?(response) expect(response.body).to eq(body) else expect(last_response.body).to eq(body) end end end api-pagination-4.8.2/spec/support/shared_examples/last_page.rb000066400000000000000000000015421340260232700245240ustar00rootroot00000000000000shared_examples 'an endpoint with a last page' do it 'should not give a link with rel "last"' do expect(link).not_to include('rel="last"') end it 'should not give a link with rel "next"' do expect(link).not_to include('rel="next"') end it 'should give a link with rel "first"' do expect(links).to include('; rel="first"') end it 'should give a link with rel "prev"' do expect(links).to include('; rel="prev"') end it 'should give a Total header' do expect(total).to eq(100) end it 'should list the last page of numbers in the response body' do body = '[91,92,93,94,95,96,97,98,99,100]' if defined?(response) expect(response.body).to eq(body) else expect(last_response.body).to eq(body) end end end api-pagination-4.8.2/spec/support/shared_examples/middle_page.rb000066400000000000000000000014251340260232700250170ustar00rootroot00000000000000shared_examples 'an endpoint with a middle page' do it 'should give all pagination links' do expect(links).to include('; rel="first"') expect(links).to include('; rel="last"') expect(links).to include('; rel="next"') expect(links).to include('; rel="prev"') end it 'should give a Total header' do expect(total).to eq(100) end it 'should list a middle page of numbers in the response body' do body = '[11,12,13,14,15,16,17,18,19,20]' if defined?(response) expect(response.body).to eq(body) else expect(last_response.body).to eq(body) end end end