Skip to content

Commit

Permalink
Merge pull request #193 from dblock/auto-paginate
Browse files Browse the repository at this point in the history
Auto-paginate embedded resources, closes #106.
  • Loading branch information
dblock authored Dec 28, 2020
2 parents ed9a796 + bac5911 commit d7924d2
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 32 deletions.
6 changes: 3 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ inherit_from: .rubocop_todo.yml
AllCops:
TargetRubyVersion: 2.3

Metrics/BlockLength:
ExcludedMethods: [it, describe]
Metrics:
Enabled: false

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

# Offense count: 1
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 103

# Offense count: 4
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
Max: 25

# Offense count: 3
# Configuration parameters: CountComments.
Metrics/ModuleLength:
Max: 265

# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
Expand Down Expand Up @@ -59,9 +44,9 @@ Style/MethodMissingSuper:
Exclude:
- 'lib/hyperclient/collection.rb'

# Offense count: 94
# Offense count: 101
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 142
Max: 147
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## Changelog

### 0.9.4 (Next)
### 1.0.0 (Next)

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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ api.connection.use :http_cache

## Resources and Attributes

Hyperclient will fetch and discover the resources from your API.
Hyperclient will fetch and discover the resources from your API and automatically paginate when possible.

```ruby
api.splines.each do |spline|
Expand Down
5 changes: 5 additions & 0 deletions features/api_navigation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Feature: API navigation
When I connect to the API
Then I should be able to navigate to posts and authors

Scenario: Links
When I connect to the API
Then I should be able to paginate posts
Then I should be able to paginate authors

Scenario: Templated links
Given I connect to the API
When I search for a post with a templated link
Expand Down
19 changes: 16 additions & 3 deletions features/steps/api_navigation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps
assert_requested :get, 'http://api.example.org/authors'
end

step 'I should be able to paginate posts' do
assert_kind_of Enumerator, api.posts.each
assert_equal 4, api.posts.to_a.count
assert_requested :get, 'http://api.example.org/posts'
assert_requested :get, 'http://api.example.org/posts?page=2'
assert_requested :get, 'http://api.example.org/posts?page=3'
end

step 'I should be able to paginate authors' do
assert_equal 1, api._links['api:authors'].to_a.count
assert_requested :get, 'http://api.example.org/authors'
end

step 'I search for a post with a templated link' do
api._links.search._expand(q: 'something')._resource
end
Expand Down Expand Up @@ -50,15 +63,15 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps
step 'I should be able to count embedded items' do
assert_equal 2, api._links.posts._resource._embedded.posts.count
assert_equal 2, api.posts._embedded.posts.count
assert_equal 2, api.posts.count
assert_equal 2, api.posts.map.count
assert_equal 4, api.posts.count
assert_equal 4, api.posts.map.count
end

step 'I should be able to iterate over embedded items' do
count = 0
api.posts.each do |_post|
count += 1
end
assert_equal 2, count
assert_equal 4, count
end
end
4 changes: 2 additions & 2 deletions features/steps/default_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Spinach::Features::DefaultConfig < Spinach::FeatureSteps
end

step 'it should have been parsed as JSON' do
@posts._attributes.total_posts.to_i.must_equal 2
@posts._attributes['total_posts'].to_i.must_equal 2
@posts._attributes.total_posts.to_i.must_equal 4
@posts._attributes['total_posts'].to_i.must_equal 4
end
end
8 changes: 7 additions & 1 deletion features/support/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ module API
WebMock::Config.instance.query_values_notation = :flat_array

stub_request(:any, /api.example.org*/).to_return(body: root_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org').to_return(body: root_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/authors').to_return(body: authors_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts').to_return(body: posts_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts/1').to_return(body: post_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts?page=2').to_return(body: posts_page2_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts?page=3').to_return(body: posts_page3_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts/1').to_return(body: post1_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts/2').to_return(body: post2_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/posts/3').to_return(body: post3_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/page2').to_return(body: page2_response, headers: { 'Content-Type' => 'application/hal+json' })
stub_request(:get, 'api.example.org/page3').to_return(body: page3_response, headers: { 'Content-Type' => 'application/hal+json' })
end
Expand Down
98 changes: 96 additions & 2 deletions features/support/fixtures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,32 @@ def root_response
}'
end

def authors_response
'{
"_links": {
"self": { "href": "/authors" }
},
"_embedded": {
"api:authors": [
{
"name": "Lorem Ipsum",
"_links": {
"self": { "href": "/authors/1" }
}
}
]
}
}'
end

def posts_response
'{
"_links": {
"self": { "href": "/posts" },
"next": {"href": "/posts?page=2"},
"last_post": {"href": "/posts/1"}
},
"total_posts": "2",
"total_posts": "4",
"_embedded": {
"posts": [
{
Expand All @@ -43,7 +62,48 @@ def posts_response
}'
end

def post_response
def posts_page2_response
'{
"_links": {
"self": { "href": "/posts?page=2" },
"next": { "href": "/posts?page=3" }
},
"total_posts": "4",
"_embedded": {
"posts": [
{
"title": "My third blog post",
"body": "Lorem ipsum dolor sit amet",
"_links": {
"self": { "href": "/posts/3" }
}
}
]
}
}'
end

def posts_page3_response
'{
"_links": {
"self": { "href": "/posts?page=3" }
},
"total_posts": "4",
"_embedded": {
"posts": [
{
"title": "My third blog post",
"body": "Lorem ipsum dolor sit amet",
"_links": {
"self": { "href": "/posts/4" }
}
}
]
}
}'
end

def post1_response
'{
"_links": {
"self": { "href": "/posts/1" }
Expand All @@ -60,6 +120,40 @@ def post_response
}'
end

def post2_response
'{
"_links": {
"self": { "href": "/posts/2" }
},
"title": "My first blog post",
"body": "Lorem ipsum dolor sit amet",
"_embedded": {
"comments": [
{
"title": "Some comment"
}
]
}
}'
end

def post3_response
'{
"_links": {
"self": { "href": "/posts/3" }
},
"title": "My first blog post",
"body": "Lorem ipsum dolor sit amet",
"_embedded": {
"comments": [
{
"title": "Some comment"
}
]
}
}'
end

def page2_response
'{
"_links": {
Expand Down
21 changes: 21 additions & 0 deletions lib/hyperclient/link.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Hyperclient
# Internal: The Link is used to let a Resource interact with the API.
#
class Link
include Enumerable

# Public: Initializes a new Link.
#
# key - The key or name of the link.
Expand All @@ -19,6 +21,25 @@ def initialize(key, link, entry_point, uri_variables = nil)
@resource = nil
end

# Public: Each implementation to allow the class to use the Enumerable
# benefits for paginated, embedded items.
#
# Returns an Enumerator.
def each(&block)
if block_given?
current = self
while current
coll = current.respond_to?(@key) ? current.send(@key) : _resource
coll.each(&block)
break unless current._links[:next]

current = current._links.next
end
else
to_enum(:each)
end
end

# Public: Indicates if the link is an URITemplate or a regular URI.
#
# Returns true if it is templated.
Expand Down
2 changes: 1 addition & 1 deletion lib/hyperclient/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Hyperclient
VERSION = '0.9.4'.freeze
VERSION = '1.0.0'.freeze
end

0 comments on commit d7924d2

Please sign in to comment.