diff --git a/lib/grape/pagination.rb b/lib/grape/pagination.rb index faa7583..f732a90 100644 --- a/lib/grape/pagination.rb +++ b/lib/grape/pagination.rb @@ -8,10 +8,16 @@ def paginate(collection) :per_page => (params[:per_page] || settings[:per_page]) } collection = ApiPagination.paginate(collection, options) + links = (header['Link'] || "").split(',').map(&:strip) + pages = ApiPagination.pages_from(collection) - links = (header['Link'] || "").split(',').map(&:strip) - url = request.url.sub(/\?.*$/, '') - pages = ApiPagination.pages_from(collection) + url = request.url.sub(/\?.*$/, '') + url = URI.parse(url).request_uri if settings[:relative_uri] + + if settings[:exclude_base_path] + base_path_regexp = Regexp.new("^" + settings[:exclude_base_path]) + url = url.sub(base_path_regexp, "") + end pages.each do |k, v| old_params = Rack::Utils.parse_query(request.query_string) @@ -28,6 +34,25 @@ def paginate(collection) base.class_eval do def self.paginate(options = {}) + # URIs can also be relative, useful when using API proxies. + # + # From RFC 5988 (Web Linking): + # "If the URI-Reference is relative, parsers MUST resolve it + # as per [RFC3986], Section 5."" + set :relative_uri, (options[:relative_uri] || false) + + # When using API proxies, you probably want to hide base path + # from returned links since proxies point to your API base path + # directly making its own resources not have such base path. + # + # E.g. + # If your application has the following API resource: + # mywebsite.example.com/api/v1/resource.json + # And that's accessible though the following API proxy resource: + # myproxy.example.com/v1/resource.json + # You don't want links to return '/api/v1/...', but '/v1/...'. + set :exclude_base_path, (options[:exclude_base_path] || false) + set :per_page, (options[:per_page] || 25) params do optional :page, :type => Integer, :default => 1, diff --git a/spec/grape_spec.rb b/spec/grape_spec.rb index bf85ffb..44216d9 100644 --- a/spec/grape_spec.rb +++ b/spec/grape_spec.rb @@ -53,3 +53,19 @@ end end end + +describe NumbersProxiedAPI do + describe 'GET #index' do + let(:links) { last_response.headers['Link'].split(', ') } + let(:total) { last_response.headers['Total'].to_i } + + context 'with existing Link headers' do + before { get "/api/numbers", :count => 30 } + + it 'should contain relative pagination Links without base path' do + expect(links).to include('; rel="next"') + expect(links).to include('; rel="last"') + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d397a1b..0778525 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ require 'support/numbers_controller' require 'support/numbers_api' +require 'support/numbers_proxied_api' require 'api-pagination' if ENV['PAGINATOR'] @@ -31,6 +32,8 @@ config.order = 'random' def app - NumbersAPI + # Allows us to test different API apps. + # e.g. NumbersApi, NumbersProxiedApi... + described_class end end diff --git a/spec/support/numbers_proxied_api.rb b/spec/support/numbers_proxied_api.rb new file mode 100644 index 0000000..4df3054 --- /dev/null +++ b/spec/support/numbers_proxied_api.rb @@ -0,0 +1,22 @@ +require 'grape' +require 'api-pagination' + +class NumbersProxiedAPI < Grape::API + format :json + + # This namespace is here simulate that the API is nested in a directory. + # When using an API proxy, this base path vanishes since the proxy host + # is the new base. That's why we have 'exclude_base_path'. + namespace :api do + desc 'Return some paginated set of numbers' + paginate :per_page => 10, + :exclude_base_path => "/api", + :relative_uri => true + params do + requires :count, :type => Integer + end + get :numbers do + paginate (1..params[:count]).to_a + end + end +end