Skip to content

Commit d7924d2

Browse files
authored
Merge pull request #193 from dblock/auto-paginate
Auto-paginate embedded resources, closes #106.
2 parents ed9a796 + bac5911 commit d7924d2

File tree

11 files changed

+157
-32
lines changed

11 files changed

+157
-32
lines changed

.rubocop.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ inherit_from: .rubocop_todo.yml
33
AllCops:
44
TargetRubyVersion: 2.3
55

6-
Metrics/BlockLength:
7-
ExcludedMethods: [it, describe]
6+
Metrics:
7+
Enabled: false
88

99
Style/FrozenStringLiteralComment:
10-
Enabled: false
10+
Enabled: false

.rubocop_todo.yml

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2020-05-14 17:08:39 -0400 using RuboCop version 0.81.0.
3+
# on 2020-12-03 14:08:14 -0500 using RuboCop version 0.81.0.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9-
# Offense count: 1
10-
# Configuration parameters: CountComments.
11-
Metrics/ClassLength:
12-
Max: 103
13-
14-
# Offense count: 4
15-
# Configuration parameters: CountComments, ExcludedMethods.
16-
Metrics/MethodLength:
17-
Max: 25
18-
19-
# Offense count: 3
20-
# Configuration parameters: CountComments.
21-
Metrics/ModuleLength:
22-
Max: 265
23-
249
# Offense count: 2
2510
# Cop supports --auto-correct.
2611
# Configuration parameters: AutoCorrect, EnforcedStyle.
@@ -59,9 +44,9 @@ Style/MethodMissingSuper:
5944
Exclude:
6045
- 'lib/hyperclient/collection.rb'
6146

62-
# Offense count: 94
47+
# Offense count: 101
6348
# Cop supports --auto-correct.
6449
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
6550
# URISchemes: http, https
6651
Layout/LineLength:
67-
Max: 142
52+
Max: 147

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
## Changelog
22

3-
### 0.9.4 (Next)
3+
### 1.0.0 (Next)
44

5+
* [#193](https://github.com/codegram/hyperclient/pull/193): Auto-paginate collections - [@dblock](https://github.com/dblock).
56
* [#163](https://github.com/codegram/hyperclient/pull/163): Test against Faraday 0.9, 0.17 and 1.0+ - [@dblock](https://github.com/dblock).
67
* Your contribution here.
78

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ api.connection.use :http_cache
102102

103103
## Resources and Attributes
104104

105-
Hyperclient will fetch and discover the resources from your API.
105+
Hyperclient will fetch and discover the resources from your API and automatically paginate when possible.
106106

107107
```ruby
108108
api.splines.each do |spline|

features/api_navigation.feature

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ Feature: API navigation
77
When I connect to the API
88
Then I should be able to navigate to posts and authors
99

10+
Scenario: Links
11+
When I connect to the API
12+
Then I should be able to paginate posts
13+
Then I should be able to paginate authors
14+
1015
Scenario: Templated links
1116
Given I connect to the API
1217
When I search for a post with a templated link

features/steps/api_navigation.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps
99
assert_requested :get, 'http://api.example.org/authors'
1010
end
1111

12+
step 'I should be able to paginate posts' do
13+
assert_kind_of Enumerator, api.posts.each
14+
assert_equal 4, api.posts.to_a.count
15+
assert_requested :get, 'http://api.example.org/posts'
16+
assert_requested :get, 'http://api.example.org/posts?page=2'
17+
assert_requested :get, 'http://api.example.org/posts?page=3'
18+
end
19+
20+
step 'I should be able to paginate authors' do
21+
assert_equal 1, api._links['api:authors'].to_a.count
22+
assert_requested :get, 'http://api.example.org/authors'
23+
end
24+
1225
step 'I search for a post with a templated link' do
1326
api._links.search._expand(q: 'something')._resource
1427
end
@@ -50,15 +63,15 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps
5063
step 'I should be able to count embedded items' do
5164
assert_equal 2, api._links.posts._resource._embedded.posts.count
5265
assert_equal 2, api.posts._embedded.posts.count
53-
assert_equal 2, api.posts.count
54-
assert_equal 2, api.posts.map.count
66+
assert_equal 4, api.posts.count
67+
assert_equal 4, api.posts.map.count
5568
end
5669

5770
step 'I should be able to iterate over embedded items' do
5871
count = 0
5972
api.posts.each do |_post|
6073
count += 1
6174
end
62-
assert_equal 2, count
75+
assert_equal 4, count
6376
end
6477
end

features/steps/default_config.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Spinach::Features::DefaultConfig < Spinach::FeatureSteps
2525
end
2626

2727
step 'it should have been parsed as JSON' do
28-
@posts._attributes.total_posts.to_i.must_equal 2
29-
@posts._attributes['total_posts'].to_i.must_equal 2
28+
@posts._attributes.total_posts.to_i.must_equal 4
29+
@posts._attributes['total_posts'].to_i.must_equal 4
3030
end
3131
end

features/support/api.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ module API
88
WebMock::Config.instance.query_values_notation = :flat_array
99

1010
stub_request(:any, /api.example.org*/).to_return(body: root_response, headers: { 'Content-Type' => 'application/hal+json' })
11+
stub_request(:get, 'api.example.org').to_return(body: root_response, headers: { 'Content-Type' => 'application/hal+json' })
12+
stub_request(:get, 'api.example.org/authors').to_return(body: authors_response, headers: { 'Content-Type' => 'application/hal+json' })
1113
stub_request(:get, 'api.example.org/posts').to_return(body: posts_response, headers: { 'Content-Type' => 'application/hal+json' })
12-
stub_request(:get, 'api.example.org/posts/1').to_return(body: post_response, headers: { 'Content-Type' => 'application/hal+json' })
14+
stub_request(:get, 'api.example.org/posts?page=2').to_return(body: posts_page2_response, headers: { 'Content-Type' => 'application/hal+json' })
15+
stub_request(:get, 'api.example.org/posts?page=3').to_return(body: posts_page3_response, headers: { 'Content-Type' => 'application/hal+json' })
16+
stub_request(:get, 'api.example.org/posts/1').to_return(body: post1_response, headers: { 'Content-Type' => 'application/hal+json' })
17+
stub_request(:get, 'api.example.org/posts/2').to_return(body: post2_response, headers: { 'Content-Type' => 'application/hal+json' })
18+
stub_request(:get, 'api.example.org/posts/3').to_return(body: post3_response, headers: { 'Content-Type' => 'application/hal+json' })
1319
stub_request(:get, 'api.example.org/page2').to_return(body: page2_response, headers: { 'Content-Type' => 'application/hal+json' })
1420
stub_request(:get, 'api.example.org/page3').to_return(body: page3_response, headers: { 'Content-Type' => 'application/hal+json' })
1521
end

features/support/fixtures.rb

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,32 @@ def root_response
1515
}'
1616
end
1717

18+
def authors_response
19+
'{
20+
"_links": {
21+
"self": { "href": "/authors" }
22+
},
23+
"_embedded": {
24+
"api:authors": [
25+
{
26+
"name": "Lorem Ipsum",
27+
"_links": {
28+
"self": { "href": "/authors/1" }
29+
}
30+
}
31+
]
32+
}
33+
}'
34+
end
35+
1836
def posts_response
1937
'{
2038
"_links": {
2139
"self": { "href": "/posts" },
40+
"next": {"href": "/posts?page=2"},
2241
"last_post": {"href": "/posts/1"}
2342
},
24-
"total_posts": "2",
43+
"total_posts": "4",
2544
"_embedded": {
2645
"posts": [
2746
{
@@ -43,7 +62,48 @@ def posts_response
4362
}'
4463
end
4564

46-
def post_response
65+
def posts_page2_response
66+
'{
67+
"_links": {
68+
"self": { "href": "/posts?page=2" },
69+
"next": { "href": "/posts?page=3" }
70+
},
71+
"total_posts": "4",
72+
"_embedded": {
73+
"posts": [
74+
{
75+
"title": "My third blog post",
76+
"body": "Lorem ipsum dolor sit amet",
77+
"_links": {
78+
"self": { "href": "/posts/3" }
79+
}
80+
}
81+
]
82+
}
83+
}'
84+
end
85+
86+
def posts_page3_response
87+
'{
88+
"_links": {
89+
"self": { "href": "/posts?page=3" }
90+
},
91+
"total_posts": "4",
92+
"_embedded": {
93+
"posts": [
94+
{
95+
"title": "My third blog post",
96+
"body": "Lorem ipsum dolor sit amet",
97+
"_links": {
98+
"self": { "href": "/posts/4" }
99+
}
100+
}
101+
]
102+
}
103+
}'
104+
end
105+
106+
def post1_response
47107
'{
48108
"_links": {
49109
"self": { "href": "/posts/1" }
@@ -60,6 +120,40 @@ def post_response
60120
}'
61121
end
62122

123+
def post2_response
124+
'{
125+
"_links": {
126+
"self": { "href": "/posts/2" }
127+
},
128+
"title": "My first blog post",
129+
"body": "Lorem ipsum dolor sit amet",
130+
"_embedded": {
131+
"comments": [
132+
{
133+
"title": "Some comment"
134+
}
135+
]
136+
}
137+
}'
138+
end
139+
140+
def post3_response
141+
'{
142+
"_links": {
143+
"self": { "href": "/posts/3" }
144+
},
145+
"title": "My first blog post",
146+
"body": "Lorem ipsum dolor sit amet",
147+
"_embedded": {
148+
"comments": [
149+
{
150+
"title": "Some comment"
151+
}
152+
]
153+
}
154+
}'
155+
end
156+
63157
def page2_response
64158
'{
65159
"_links": {

lib/hyperclient/link.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module Hyperclient
44
# Internal: The Link is used to let a Resource interact with the API.
55
#
66
class Link
7+
include Enumerable
8+
79
# Public: Initializes a new Link.
810
#
911
# key - The key or name of the link.
@@ -19,6 +21,25 @@ def initialize(key, link, entry_point, uri_variables = nil)
1921
@resource = nil
2022
end
2123

24+
# Public: Each implementation to allow the class to use the Enumerable
25+
# benefits for paginated, embedded items.
26+
#
27+
# Returns an Enumerator.
28+
def each(&block)
29+
if block_given?
30+
current = self
31+
while current
32+
coll = current.respond_to?(@key) ? current.send(@key) : _resource
33+
coll.each(&block)
34+
break unless current._links[:next]
35+
36+
current = current._links.next
37+
end
38+
else
39+
to_enum(:each)
40+
end
41+
end
42+
2243
# Public: Indicates if the link is an URITemplate or a regular URI.
2344
#
2445
# Returns true if it is templated.

0 commit comments

Comments
 (0)