Skip to content

Commit 185aea0

Browse files
committed
Added listener to allow Pager to work with API v1
Because `Pager` expects some metadata specific to V2 of the API to be present in the response, would not work with collections coming from v1 of the API. `ApiOneCollectionListener` will add to each v1 response (_which contains a collection_), the metadata specific to v2 which is expected by `Pager`. ref: #17
1 parent 0b1758d commit 185aea0

File tree

4 files changed

+322
-1
lines changed

4 files changed

+322
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77
### Added:
88
- Endpoint to get a list of teams to which caller has access.
99
- Endpoint to get emails for authenticated user.
10-
- Basic pager in oorder to support response pagination. (_@see #17_)
10+
- Basic pager in order to support response pagination. (_@see #17_)
1111

1212
### Changed:
1313
- Minimum required PHP version has been bumped to 5.4 from 5.3
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
/**
3+
* This file is part of the bitbucket-api package.
4+
*
5+
* (c) Alexandru G. <alex@gentle.ro>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace Bitbucket\API\Http\Listener;
11+
12+
use Buzz\Message\MessageInterface;
13+
use Buzz\Message\Request;
14+
use Buzz\Message\RequestInterface;
15+
16+
/**
17+
* Helper for `Pager`
18+
*
19+
* Inserts pagination metadata (_as is expected by `Pager`_),
20+
* in any response coming from v1 of the API which contains
21+
* a collection.
22+
*
23+
* @author Alexandru Guzinschi <alex@gentle.ro>
24+
*/
25+
class ApiOneCollectionListener implements ListenerInterface
26+
{
27+
/** @var array */
28+
private $urlComponents;
29+
30+
/** @var string */
31+
private $resource;
32+
33+
/**
34+
* {@inheritDoc}
35+
*/
36+
public function getName()
37+
{
38+
return 'api_one_collection';
39+
}
40+
41+
/**
42+
* {@inheritDoc}
43+
* @codeCoverageIgnore
44+
*/
45+
public function preSend(RequestInterface $request)
46+
{
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
*/
52+
public function postSend(RequestInterface $request, MessageInterface $response)
53+
{
54+
if ($this->isLegacyApiVersion($request)) {
55+
$this->parseRequest($request);
56+
57+
if ($this->canPaginate($response)) {
58+
$content = $this->insertPaginationMeta(
59+
$this->getContent($response),
60+
$this->getPaginationMeta($response)
61+
);
62+
63+
$response->setContent(json_encode($content));
64+
}
65+
}
66+
}
67+
68+
/**
69+
* @access public
70+
* @param MessageInterface $request
71+
* @return bool
72+
*/
73+
private function isLegacyApiVersion(MessageInterface $request)
74+
{
75+
/** @var Request $request */
76+
return strpos($request->getResource(), '/1.0/') !== false;
77+
}
78+
79+
/**
80+
* @access public
81+
* @param RequestInterface $request
82+
* @return void
83+
*/
84+
private function parseRequest(RequestInterface $request)
85+
{
86+
/** @var Request $request */
87+
$this->urlComponents = parse_url($request->getUrl());
88+
89+
if (array_key_exists('query', $this->urlComponents)) {
90+
parse_str($this->urlComponents['query'], $this->urlComponents['query']);
91+
} else {
92+
$this->urlComponents['query'] = [];
93+
}
94+
95+
$this->urlComponents['query']['start'] = array_key_exists('start', $this->urlComponents['query']) ?
96+
(int)$this->urlComponents['query']['start'] :
97+
0
98+
;
99+
$this->urlComponents['query']['limit'] = array_key_exists('limit', $this->urlComponents['query']) ?
100+
(int)$this->urlComponents['query']['limit'] :
101+
15
102+
;
103+
104+
$pcs = explode('/', $this->urlComponents['path']);
105+
$this->resource = strtolower(array_pop($pcs));
106+
}
107+
108+
/**
109+
* @access public
110+
* @param MessageInterface $response
111+
* @return bool
112+
*/
113+
private function canPaginate(MessageInterface $response)
114+
{
115+
$content = $this->getContent($response);
116+
return array_key_exists('count', $content) && array_key_exists($this->resource, $content);
117+
}
118+
119+
/**
120+
* @access private
121+
* @param MessageInterface $response
122+
* @return array
123+
*/
124+
private function getContent(MessageInterface $response)
125+
{
126+
$content = json_decode($response->getContent(), true);
127+
128+
if (is_array($content) && JSON_ERROR_NONE === json_last_error()) {
129+
return $content;
130+
}
131+
132+
return [];
133+
}
134+
135+
/**
136+
* @access private
137+
* @param array $content
138+
* @param array $pagination
139+
* @return array
140+
*/
141+
private function insertPaginationMeta(array $content, array $pagination)
142+
{
143+
// This is just a reference because duplicate data in response could create confusion between some users.
144+
$content['values'] = '.'.$this->resource;
145+
$content['size'] = $content['count'];
146+
$parts = $this->urlComponents;
147+
148+
// insert pagination links only if everything does not fit in a single page
149+
if ($content['count'] > count($content[$this->resource])) {
150+
if ($pagination['page'] > 1 || $pagination['page'] === $pagination['pages']) {
151+
$query = $parts['query'];
152+
$query['start'] -= $parts['query']['limit'];
153+
154+
$content['previous'] = sprintf(
155+
'%s://%s%s?%s',
156+
$parts['scheme'],
157+
$parts['host'],
158+
$parts['path'],
159+
http_build_query($query)
160+
);
161+
}
162+
163+
if ($pagination['page'] < $pagination['pages']) {
164+
$query = $parts['query'];
165+
$query['start'] += $parts['query']['limit'];
166+
167+
$content['next'] = sprintf(
168+
'%s://%s%s?%s',
169+
$parts['scheme'],
170+
$parts['host'],
171+
$parts['path'],
172+
http_build_query($query)
173+
);
174+
}
175+
}
176+
177+
return $content;
178+
}
179+
180+
/**
181+
* @access private
182+
* @param MessageInterface $response
183+
* @return array
184+
*/
185+
private function getPaginationMeta(MessageInterface $response)
186+
{
187+
$meta = [];
188+
189+
$content = $this->getContent($response);
190+
$meta['total'] = $content['count'];
191+
$meta['pages'] = (int)ceil($meta['total'] / $this->urlComponents['query']['limit']);
192+
$meta['page'] = ($this->urlComponents['query']['start']/$this->urlComponents['query']['limit']) === 0 ?
193+
1 :
194+
($this->urlComponents['query']['start']/$this->urlComponents['query']['limit'])+1
195+
;
196+
197+
if ($meta['page'] > $meta['pages']) {
198+
$meta['page'] = $meta['pages'];
199+
}
200+
201+
return $meta;
202+
}
203+
}

lib/Bitbucket/API/Http/Response/Pager.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ private function getContent()
130130
$content = json_decode($this->response->getContent(), true);
131131

132132
if (is_array($content) && JSON_ERROR_NONE === json_last_error()) {
133+
// replace reference inserted by `LegacyCollectionListener` with actual data.
134+
if (is_string($content['values']) && strpos($content['values'], '.') !== false) {
135+
$content['values'] = $content[str_replace('.', '', $content['values'])];
136+
}
133137
return $content;
134138
}
135139

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace Bitbucket\Tests\API\Http\Listener;
4+
5+
use Bitbucket\Tests\API as Tests;
6+
use Bitbucket\API\Http\Listener\ApiOneCollectionListener;
7+
use Buzz\Message\Request;
8+
use Buzz\Message\Response;
9+
10+
/**
11+
* @author Alexandru Guzinschi <alex@gentle.ro>
12+
*/
13+
class ApiOneCollectionListenerTest extends Tests\TestCase
14+
{
15+
public function testMetadataExistsForApiv1()
16+
{
17+
$listener = new ApiOneCollectionListener();
18+
$request = new Request('GET', '/1.0/repositories/team/repo/issues?limit=2&start=2', 'http://localhost');
19+
$response = new Response();
20+
$content = [
21+
'count' => 5,
22+
'issues' => [
23+
'issue_3' => [],
24+
'issue_4' => []
25+
]
26+
];
27+
$response->setContent(json_encode($content));
28+
29+
$listener->postSend($request, $response);
30+
31+
$this->assertInstanceOf('\Buzz\Message\Response', $response);
32+
$body = json_decode($response->getContent(), true);
33+
34+
$this->assertEquals('api_one_collection', $listener->getName());
35+
$this->assertEquals($content['issues'], $body['issues']);
36+
37+
$this->assertArrayHasKey('values', $body);
38+
$this->assertEquals($body['values'], '.issues');
39+
40+
$this->assertArrayHasKey('next', $body);
41+
$this->assertEquals('http://localhost/1.0/repositories/team/repo/issues?limit=2&start=4', $body['next']);
42+
$this->assertArrayHasKey('previous', $body);
43+
$this->assertEquals('http://localhost/1.0/repositories/team/repo/issues?limit=2&start=0', $body['previous']);
44+
}
45+
46+
public function testNonExistentPageReturnsLastPage()
47+
{
48+
$listener = new ApiOneCollectionListener();
49+
$request = new Request('GET', '/1.0/repositories/team/repo/issues?limit=2&start=6', 'http://localhost');
50+
$response = new Response();
51+
$content = [
52+
'count' => 5,
53+
'issues' => [
54+
'issue_5' => []
55+
]
56+
];
57+
$response->setContent(json_encode($content));
58+
59+
$listener->postSend($request, $response);
60+
61+
$this->assertInstanceOf('\Buzz\Message\Response', $response);
62+
$body = json_decode($response->getContent(), true);
63+
64+
$this->assertEquals($content['issues'], $body['issues']);
65+
66+
$this->assertArrayHasKey('values', $body);
67+
$this->assertEquals($body['values'], '.issues');
68+
69+
$this->assertArrayNotHasKey('next', $body);
70+
$this->assertArrayHasKey('previous', $body);
71+
$this->assertEquals('http://localhost/1.0/repositories/team/repo/issues?limit=2&start=4', $body['previous']);
72+
}
73+
74+
public function testInvalidJsonResponseShouldResultInANullBodyContent()
75+
{
76+
$listener = new ApiOneCollectionListener();
77+
$request = new Request('GET', '/1.0/repositories/team/repo/issues?limit=2&start=2', 'http://localhost');
78+
$response = new Response();
79+
$response->setContent('{"key": "value}');
80+
81+
$listener->postSend($request, $response);
82+
83+
$this->assertInstanceOf('\Buzz\Message\Response', $response);
84+
$body = json_decode($response->getContent(), true);
85+
86+
$this->assertNull($body);
87+
}
88+
89+
public function testResponseWithoutCollection()
90+
{
91+
$listener = new ApiOneCollectionListener();
92+
$request = new Request('GET', '/1.0/repositories/team/repo/issues', 'http://localhost');
93+
$response = new Response();
94+
$content = [
95+
'issues' => [
96+
'issue_3' => [],
97+
'issue_4' => []
98+
]
99+
];
100+
$response->setContent(json_encode($content));
101+
102+
$listener->postSend($request, $response);
103+
104+
$this->assertInstanceOf('\Buzz\Message\Response', $response);
105+
$body = json_decode($response->getContent(), true);
106+
107+
$this->assertEquals($content['issues'], $body['issues']);
108+
109+
$this->assertArrayNotHasKey('values', $body);
110+
$this->assertArrayNotHasKey('count', $body);
111+
$this->assertArrayNotHasKey('next', $body);
112+
$this->assertArrayNotHasKey('previous', $body);
113+
}
114+
}

0 commit comments

Comments
 (0)