Skip to content

Commit 8fad9cf

Browse files
authored
Merge branch 'master' into fix-resource-serialization-involving-last-result-set
2 parents c2deea5 + 94f45e2 commit 8fad9cf

File tree

9 files changed

+539
-3
lines changed

9 files changed

+539
-3
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ Gemfile.lock
2424
*.gemfile.lock
2525

2626
/coverage
27+
28+
# IntellJ/RubyMine
29+
*.iml
30+
.idea/
31+
32+
# asdf config
33+
.tool-versions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
- [#351](https://github.com/JsonApiClient/json_api_client/pull/351) - Remove rudimental `last_result_set` relationship from serializer
66

7+
## 1.13.0
8+
9+
- [#348](https://github.com/JsonApiClient/json_api_client/pull/348) - add NestedParamPaginator to address inconsistency in handling of pagination query string params (issue [#347](https://github.com/JsonApiClient/json_api_client/issues/347)).
10+
711
## 1.12.2
812

913
- [#350](https://github.com/JsonApiClient/json_api_client/pull/350) - fix resource including with blank `relationships` response data

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,20 @@ class MyApi::Base < JsonApiClient::Resource
599599
end
600600
```
601601
602+
### NestedParamPaginator
603+
604+
The default `JsonApiClient::Paginating::Paginator` is not strict about how it handles the param keys ([#347](https://github.com/JsonApiClient/json_api_client/issues/347)). There is a second paginator that more rigorously adheres to the JSON:API pagination recommendation style of `page[page]=1&page[per_page]=10`.
605+
606+
If this second style suits your needs better, it is available as a class override:
607+
608+
```ruby
609+
class Order < JsonApiClient::Resource
610+
self.paginator = JsonApiClient::Paginating::NestedParamPaginator
611+
end
612+
```
613+
614+
You can also extend `NestedParamPaginator` in your custom paginators or assign the `page_param` or `per_page_param` as with the default version above.
615+
602616
### Custom type
603617
604618
If your model must be named differently from classified type of resource you can easily customize it.

lib/json_api_client/paginating.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module JsonApiClient
22
module Paginating
33
autoload :Paginator, 'json_api_client/paginating/paginator'
4+
autoload :NestedParamPaginator, 'json_api_client/paginating/nested_param_paginator'
45
end
5-
end
6+
end
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
module JsonApiClient
2+
module Paginating
3+
# An alternate, more consistent Paginator that always wraps
4+
# pagination query string params in a top-level wrapper_name,
5+
# e.g. page[offset]=2, page[limit]=10.
6+
class NestedParamPaginator
7+
DEFAULT_WRAPPER_NAME = "page".freeze
8+
DEFAULT_PAGE_PARAM = "page".freeze
9+
DEFAULT_PER_PAGE_PARAM = "per_page".freeze
10+
11+
# Define class accessors as methods to enforce standard way
12+
# of defining pagination related query string params.
13+
class << self
14+
15+
def wrapper_name
16+
@_wrapper_name ||= DEFAULT_WRAPPER_NAME
17+
end
18+
19+
def wrapper_name=(param = DEFAULT_WRAPPER_NAME)
20+
raise ArgumentError, "don't wrap wrapper_name" unless valid_param?(param)
21+
22+
@_wrapper_name = param.to_s
23+
end
24+
25+
def page_param
26+
@_page_param ||= DEFAULT_PAGE_PARAM
27+
"#{wrapper_name}[#{@_page_param}]"
28+
end
29+
30+
def page_param=(param = DEFAULT_PAGE_PARAM)
31+
raise ArgumentError, "don't wrap page_param" unless valid_param?(param)
32+
33+
@_page_param = param.to_s
34+
end
35+
36+
def per_page_param
37+
@_per_page_param ||= DEFAULT_PER_PAGE_PARAM
38+
"#{wrapper_name}[#{@_per_page_param}]"
39+
end
40+
41+
def per_page_param=(param = DEFAULT_PER_PAGE_PARAM)
42+
raise ArgumentError, "don't wrap per_page_param" unless valid_param?(param)
43+
44+
@_per_page_param = param
45+
end
46+
47+
private
48+
49+
def valid_param?(param)
50+
!(param.nil? || param.to_s.include?("[") || param.to_s.include?("]"))
51+
end
52+
53+
end
54+
55+
attr_reader :params, :result_set, :links
56+
57+
def initialize(result_set, data)
58+
@params = params_for_uri(result_set.uri)
59+
@result_set = result_set
60+
@links = data["links"]
61+
end
62+
63+
def next
64+
result_set.links.fetch_link("next")
65+
end
66+
67+
def prev
68+
result_set.links.fetch_link("prev")
69+
end
70+
71+
def first
72+
result_set.links.fetch_link("first")
73+
end
74+
75+
def last
76+
result_set.links.fetch_link("last")
77+
end
78+
79+
def total_pages
80+
if links["last"]
81+
uri = result_set.links.link_url_for("last")
82+
last_params = params_for_uri(uri)
83+
last_params.fetch(page_param, &method(:current_page)).to_i
84+
else
85+
current_page
86+
end
87+
end
88+
89+
# this is an estimate, not necessarily an exact count
90+
def total_entries
91+
per_page * total_pages
92+
end
93+
def total_count; total_entries; end
94+
95+
def offset
96+
per_page * (current_page - 1)
97+
end
98+
99+
def per_page
100+
params.fetch(per_page_param) do
101+
result_set.length
102+
end.to_i
103+
end
104+
105+
def current_page
106+
params.fetch(page_param, 1).to_i
107+
end
108+
109+
def out_of_bounds?
110+
current_page > total_pages
111+
end
112+
113+
def previous_page
114+
current_page > 1 ? (current_page - 1) : nil
115+
end
116+
117+
def next_page
118+
current_page < total_pages ? (current_page + 1) : nil
119+
end
120+
121+
def page_param
122+
self.class.page_param
123+
end
124+
125+
def per_page_param
126+
self.class.per_page_param
127+
end
128+
129+
alias limit_value per_page
130+
131+
protected
132+
133+
def params_for_uri(uri)
134+
return {} unless uri
135+
uri = Addressable::URI.parse(uri)
136+
( uri.query_values || {} ).with_indifferent_access
137+
end
138+
end
139+
end
140+
end

lib/json_api_client/query/builder.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,13 @@ def primary_key_params
146146
end
147147

148148
def pagination_params
149-
@pagination_params.empty? ? {} : {page: @pagination_params}
149+
if klass.paginator.ancestors.include?(Paginating::Paginator)
150+
# Original Paginator inconsistently wraps pagination params here. Keeping
151+
# default behavior for now so as not to break backward compatibility.
152+
@pagination_params.empty? ? {} : {page: @pagination_params}
153+
else
154+
@pagination_params
155+
end
150156
end
151157

152158
def includes_params

lib/json_api_client/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module JsonApiClient
2-
VERSION = "1.12.2"
2+
VERSION = "1.13.0"
33
end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
require 'test_helper'
2+
3+
class NestedParamPaginatorTest < MiniTest::Test
4+
5+
class Book < JsonApiClient::Resource
6+
self.site = "http://example.com/"
7+
end
8+
9+
def setup
10+
@nested_param_paginator = JsonApiClient::Paginating::NestedParamPaginator
11+
@default_paginator = JsonApiClient::Paginating::Paginator
12+
Article.paginator = @nested_param_paginator
13+
end
14+
15+
def teardown
16+
@nested_param_paginator.page_param = @nested_param_paginator::DEFAULT_PAGE_PARAM
17+
@nested_param_paginator.per_page_param = @nested_param_paginator::DEFAULT_PER_PAGE_PARAM
18+
Article.paginator = @default_paginator
19+
end
20+
21+
def test_default_page_params_wrapped_consistently
22+
assert_equal "page[page]", @nested_param_paginator.page_param
23+
assert_equal "page[per_page]", @nested_param_paginator.per_page_param
24+
end
25+
26+
def test_custom_page_params_wrapped_consistently
27+
@nested_param_paginator.page_param = "offset"
28+
@nested_param_paginator.per_page_param = "limit"
29+
assert_equal "page[offset]", @nested_param_paginator.page_param
30+
assert_equal "page[limit]", @nested_param_paginator.per_page_param
31+
end
32+
33+
def test_custom_page_param_does_not_allow_double_wrap
34+
assert_raises ArgumentError do
35+
@nested_param_paginator.page_param = "page[number]"
36+
end
37+
end
38+
39+
def test_custom_per_page_param_does_not_allow_double_wrap
40+
assert_raises ArgumentError do
41+
@nested_param_paginator.per_page_param = "page[size]"
42+
end
43+
end
44+
45+
def test_pagination_params_total_calculations
46+
@nested_param_paginator.page_param = "number"
47+
@nested_param_paginator.per_page_param = "size"
48+
stub_request(:get, "http://example.com/articles?page[number]=1&page[size]=2")
49+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
50+
data: [{
51+
type: "articles",
52+
id: "1",
53+
attributes: {
54+
title: "JSON API paints my bikeshed!"
55+
}
56+
},
57+
{
58+
type: "articles",
59+
id: "2",
60+
attributes: {
61+
title: "json_api_client counts pages correctly"
62+
}
63+
}],
64+
links: {
65+
self: "http://example.com/articles?page[number]=1&page[size]=2",
66+
next: "http://example.com/articles?page[number]=2&page[size]=2",
67+
prev: nil,
68+
first: "http://example.com/articles?page[number]=1&page[size]=2",
69+
last: "http://example.com/articles?page[number]=4&page[size]=2"
70+
}
71+
}.to_json)
72+
73+
articles = Article.paginate(page: 1, per_page: 2).to_a
74+
assert_equal 1, articles.current_page
75+
assert_equal 4, articles.total_pages
76+
assert_equal 8, articles.total_entries
77+
ensure
78+
@nested_param_paginator.page_param = "page"
79+
@nested_param_paginator.per_page_param = "per_page"
80+
end
81+
82+
end

0 commit comments

Comments
 (0)