Skip to content

Commit

Permalink
refactoring: added customizable headers; normalized hash and headers;…
Browse files Browse the repository at this point in the history
… code improvements; updated doc and tests
  • Loading branch information
ddnexus committed Mar 11, 2019
1 parent 2254279 commit 50773df
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 36 deletions.
10 changes: 8 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require "rubocop/rake_task" unless ENV['SKIP_RUBOCOP']
Rake::TestTask.new(:test_common) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList.new.include("test/**/*_test.rb").exclude('test/**/i18n_test.rb', 'test/**/items_test.rb', 'test/**/overflow_test.rb', 'test/**/trim_test.rb', 'test/**/elasticsearch_rails_test.rb', 'test/**/searchkick_test.rb')
t.test_files = FileList.new.include("test/**/*_test.rb").exclude('test/**/i18n_test.rb', 'test/**/items_test.rb', 'test/**/overflow_test.rb', 'test/**/trim_test.rb', 'test/**/elasticsearch_rails_test.rb', 'test/**/searchkick_test.rb', 'test/**/support_test.rb')
end

Rake::TestTask.new(:test_extra_i18n) do |t|
Expand Down Expand Up @@ -46,7 +46,13 @@ Rake::TestTask.new(:test_extra_elasticsearch) do |t|
t.test_files = FileList['test/**/elasticsearch_rails_test.rb', 'test/**/searchkick_test.rb']
end

task :test => [:test_common, :test_extra_items, :test_extra_i18n, :test_extra_overflow, :test_extra_trim, :test_extra_elasticsearch ]
Rake::TestTask.new(:test_support) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList['test/**/support_test.rb']
end

task :test => [:test_common, :test_extra_items, :test_extra_i18n, :test_extra_overflow, :test_extra_trim, :test_extra_elasticsearch, :test_support ]

if ENV['SKIP_RUBOCOP']
task :default => [:test]
Expand Down
58 changes: 41 additions & 17 deletions docs/extras/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ title: Headers

This extra implements the [RFC-8288](https://tools.ietf.org/html/rfc8288) compilant http response headers (and other helpers) useful for API pagination.

- It removes the need for an extra dependency
- No need for an extra dependency
- No need to learn yet another interface
- It saves quite a lot of memory and CPU
- It works with any type of collection and/or `pagy_*` constructor (even `pagy_countless`)
- It works with any pagy object (including `Pagy::Countless`) regardless the type of collection
- It offers more explicit flexibility and simplicity

## Synopsis
Expand All @@ -26,11 +26,27 @@ In your controller action:
```ruby
# paginate as usual with any pagy_* backend constructor
pagy, records = pagy(Product.all)
# explicitly merge the headers to the response
pagy_headers_merge(pagy)
render json: records
```

Or if you can reuse it (e.g. in a pure API app with very standard actions), you are encouraged to define a custom `pagy_render` method in your application controller like:
### Suggestions

Instead of explicitly merging the headers before each rendering, you can get them automatically merged (application-wide and when `@pagy` is available), by overriding the `render` method in your application controller:

```ruby
def render(*args, &block)
pagy_headers_merge(@pagy) if @pagy
super
end

# and use it in any action (notice @pagy)
@pagy, records = pagy(Product.all)
render json: records
```

If your code in different actions is similar enough, you can encapsulate the statements in a custom `pagy_render` method in your application controller. For example:

```ruby
def pagy_render(collection, vars={})
Expand All @@ -45,36 +61,44 @@ pagy_render(Product.all)

## Files

This extra is composed of 1 small file:

- [headers.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/headers.rb)

## Headers

This extra adds the standard/legacy headers used by various API-pagination gems with `Link`, `Per-Page` and `Total` headers, so it is a convenient replacement for legacy apps that use external gems.

For new apps, and for consistency with the Pagy naming, you may want to use the `Items` (instead of Per-Page) and `Count` (instead of Total) aliases.
This extra generates the standard `Link` header defined in the
[RFC-8288](https://tools.ietf.org/html/rfc8288), and adds 3 customizable headers useful for pagination: `Page-Items`, `Total-Pages` and `Total-Count` headers.

Example of HTTP headers produced:
Example of the default HTTP headers produced:

```
Link <https://example.com:8080/foo?page=1>; rel="first", <https://example.com:8080/foo?page=2>; rel="prev", <https://example.com:8080/foo?page=4>; rel="next", <https://example.com:8080/foo?page=50>; rel="last"
Items 20
Per-Page 20
Count 1000
Total 1000
Page-Items 20
Total-Pages 50
Total-Count 1000
```

#### Customize the header names

If you are replacing any of the existing API-pagination gems in some already working app, you may want to customize the header names so you will not have to change the client app that consumes them. You can do so by using the `:headers` variable, set as usual at the global level or instance level.

For example, the following will change the header names and will suppres the `:pages` ('Total-Pages') header:

```ruby
# globally
Pagy::VARS[:headers] = {items: 'Per-Page', pages: false, count: 'Total'}
# or for single instance
pagy, records = pagy(collection, items: 'Per-Page', pages: false, count: 'Total'}
```

#### Countless Pagination

If your requirements allow to save one count-query per rendering by using the `pagy_countless` constructor, the headers will miss only the `rel="last"` link and the `Total`/`Count` (unknown with countless pagination).
If your requirements allow to save one count-query per rendering by using the `pagy_countless` constructor, the headers will miss only the `rel="last"` link and the `Total-Count` (unknown with countless pagination).

Example of HTTP headers produced from a `Pagy::Countless` object:

```
Link <https://example.com:8080/foo?page=1>; rel="first", <https://example.com:8080/foo?page=2>; rel="prev", <https://example.com:8080/foo?page=4>; rel="next"
Items 20
Per-Page 20
Page-Items 20
```

## Methods
Expand All @@ -95,7 +119,7 @@ This method generates a hash of [RFC-8288](https://tools.ietf.org/html/rfc8288)

### pagy_headers_hash(pagy)

This method generates a hash of headers, useful if you want to include some meta-data within your json. For example:
This method generates a hash structure of the headers, useful if you want to include some meta-data within your json. For example:

```ruby
render json: records.as_json.merge!(meta: {pagination: pagy_headers_hash(pagy)})
Expand Down
21 changes: 12 additions & 9 deletions lib/pagy/extras/headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class Pagy
# Add specialized backend methods to add pagination response headers
module Backend ; private

VARS[:headers] = { items: 'Page-Items', count: 'Total-Count', pages: 'Total-Pages' }

include Helpers

def pagy_headers_merge(pagy)
Expand All @@ -14,21 +16,22 @@ def pagy_headers_merge(pagy)

def pagy_headers(pagy)
hash = pagy_headers_hash(pagy)
{ 'Link' => hash[:links].map{|rel, link| %(<#{link}>; rel="#{rel}")}.join(', ') }.tap do |h|
h['Items'] = h['Per-Page'] = hash[:items]
h['Count'] = h['Total'] = hash[:count] if hash.key?(:count)
end
hash['Link'] = hash['Link'].map{|rel, link| %(<#{link}>; rel="#{rel}")}.join(', ')
hash
end

def pagy_headers_hash(pagy)
countless = defined?(Pagy::Countless) && pagy.is_a?(Pagy::Countless)
rels = { first: 1, prev: pagy.prev, next: pagy.next }.tap{ |r| r[:last] = pagy.last unless countless }
rels = { 'first' => 1, 'prev' => pagy.prev, 'next' => pagy.next }; rels['last'] = pagy.last unless countless
a, b = pagy_url_for(Frontend::MARKER, pagy, :url).split(Frontend::MARKER, 2)
links = Hash[rels.map{|rel, n|[rel, "#{a}#{n}#{b}"] if n}.compact]
{ links: links }.tap do |h|
h[:items] = h[:per_page] = pagy.vars[:items]
h[:count] = h[:total] = pagy.count unless countless
hash = { 'Link' => Hash[rels.map{|rel, n|[rel, "#{a}#{n}#{b}"] if n}.compact] }
headers = pagy.vars[:headers]
hash[headers[:items]] = pagy.vars[:items] if headers[:items]
unless countless
hash[headers[:pages]] = pagy.pages if headers[:pages]
hash[headers[:count]] = pagy.count if headers[:count]
end
hash
end

end
Expand Down
2 changes: 1 addition & 1 deletion lib/pagy/frontend.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Pagy
module Helpers
# This works with all Rack-based frameworks (Sinatra, Padrino, Rails, ...)
def pagy_url_for(page, pagy, path_or_url=:path)
p_vars = pagy.vars; params = request.GET.merge(p_vars[:page_param].to_s => page).merge!(p_vars[:params])
p_vars = pagy.vars; params = request.GET; params[p_vars[:page_param].to_s] = page; params.merge!(p_vars[:params])
"#{request.send(path_or_url)}?#{Rack::Utils.build_nested_query(pagy_get_params(params))}#{p_vars[:anchor]}"
end

Expand Down
17 changes: 10 additions & 7 deletions test/pagy/extras/headers_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,27 @@

it 'returns the full headers hash' do
pagy, _records = @controller.send(:pagy, @collection)
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Items"=>20, "Per-Page"=>20, "Count"=>1000, "Total"=>1000})
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Page-Items"=>20, "Total-Pages"=>50, "Total-Count"=>1000})
end

it 'returns custom headers hash' do
pagy, _records = @controller.send(:pagy, @collection, headers:{items:'Per-Page', count: 'Total', pages:false})
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Per-Page"=>20, "Total"=>1000})
end

it 'returns the countless headers hash' do
pagy, _records = @controller.send(:pagy_countless, @collection)
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\"", "Items"=>20, "Per-Page"=>20})
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\"", "Page-Items"=>20})
end

it 'omit prev on first page' do
pagy, _records = @controller.send(:pagy, @collection, page: 1)
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Items"=>20, "Per-Page"=>20, "Count"=>1000, "Total"=>1000})
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Page-Items"=>20, "Total-Pages"=>50, "Total-Count"=>1000})
end

it 'omit next on last page' do
pagy, _records = @controller.send(:pagy, @collection, page: 50)
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=49>; rel=\"prev\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Items"=>20, "Per-Page"=>20, "Count"=>1000, "Total"=>1000})
@controller.send(:pagy_headers, pagy).must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=49>; rel=\"prev\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Page-Items"=>20, "Total-Pages"=>50, "Total-Count"=>1000})
end

end
Expand All @@ -48,11 +53,9 @@
it 'returns the full headers hash' do
pagy, _records = @controller.send(:pagy, @collection)
@controller.send(:pagy_headers_merge, pagy)
@controller.send(:response).headers.must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Items"=>20, "Per-Page"=>20, "Count"=>1000, "Total"=>1000})
@controller.send(:response).headers.must_equal({"Link"=>"<https://example.com:8080/foo?page=1>; rel=\"first\", <https://example.com:8080/foo?page=2>; rel=\"prev\", <https://example.com:8080/foo?page=4>; rel=\"next\", <https://example.com:8080/foo?page=50>; rel=\"last\"", "Page-Items"=>20, "Total-Pages"=>50, "Total-Count"=>1000})
end

end



end

0 comments on commit 50773df

Please sign in to comment.