From 3880496bd578a2badcce4138de99fbdff1c0ddaa Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Thu, 28 Feb 2019 22:32:34 +0100 Subject: [PATCH 1/4] make it easier to handle objects we can use pagination on --- src/CollectionDocument.php | 8 +++----- src/interfaces/PaginableInterface.php | 13 +++++++++++++ src/objects/RelationshipObject.php | 8 +++----- 3 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 src/interfaces/PaginableInterface.php diff --git a/src/CollectionDocument.php b/src/CollectionDocument.php index 463448e..a6b50b9 100644 --- a/src/CollectionDocument.php +++ b/src/CollectionDocument.php @@ -5,6 +5,7 @@ use alsvanzelf\jsonapi\DataDocument; use alsvanzelf\jsonapi\Document; use alsvanzelf\jsonapi\exceptions\InputException; +use alsvanzelf\jsonapi\interfaces\PaginableInterface; use alsvanzelf\jsonapi\interfaces\RecursiveResourceContainerInterface; use alsvanzelf\jsonapi\interfaces\ResourceContainerInterface; use alsvanzelf\jsonapi\interfaces\ResourceInterface; @@ -15,7 +16,7 @@ * this document is a set of Resources * this document should be used if there could be multiple, also if only one or even none is returned */ -class CollectionDocument extends DataDocument implements ResourceContainerInterface { +class CollectionDocument extends DataDocument implements PaginableInterface, ResourceContainerInterface { /** @var ResourceInterface[] */ protected $resources = []; /** @var array */ @@ -60,10 +61,7 @@ public function add($type, $id, array $attributes=[]) { } /** - * @param string $previousHref optional - * @param string $nextHref optional - * @param string $firstHref optional - * @param string $lastHref optional + * @inheritDoc */ public function setPaginationLinks($previousHref=null, $nextHref=null, $firstHref=null, $lastHref=null) { if ($previousHref !== null) { diff --git a/src/interfaces/PaginableInterface.php b/src/interfaces/PaginableInterface.php new file mode 100644 index 0000000..2068244 --- /dev/null +++ b/src/interfaces/PaginableInterface.php @@ -0,0 +1,13 @@ + Date: Thu, 28 Feb 2019 22:34:04 +0100 Subject: [PATCH 2/4] add methods to use the cursor pagination profile --- src/profiles/CursorPaginationProfile.php | 402 +++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 src/profiles/CursorPaginationProfile.php diff --git a/src/profiles/CursorPaginationProfile.php b/src/profiles/CursorPaginationProfile.php new file mode 100644 index 0000000..1700f92 --- /dev/null +++ b/src/profiles/CursorPaginationProfile.php @@ -0,0 +1,402 @@ +generatePreviousLink($baseOrCurrentUrl, $firstCursor)); + $nextLinkObject = new LinkObject($this->generateNextLink($baseOrCurrentUrl, $lastCursor)); + + $this->setPaginationLinkObjects($paginable, $previousLinkObject, $nextLinkObject); + } + + /** + * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param string $baseOrCurrentUrl + * @param string $lastCursor + */ + public function setLinksFirstPage(PaginableInterface $paginable, $baseOrCurrentUrl, $lastCursor) { + $this->setPaginationLinkObjectsWithoutPrevious($paginable, $baseOrCurrentUrl, $lastCursor); + } + + /** + * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param string $baseOrCurrentUrl + * @param string $firstCursor + */ + public function setLinksLastPage(PaginableInterface $paginable, $baseOrCurrentUrl, $firstCursor) { + $this->setPaginationLinkObjectsWithoutNext($paginable, $baseOrCurrentUrl, $firstCursor); + } + + /** + * set the cursor of a specific resource to allow pagination after or before this resource + * + * @param ResourceInterface $resource + * @param string $cursor + */ + public function setCursor(ResourceInterface $resource, $cursor) { + $this->setItemMeta($resource, $cursor); + } + + /** + * set count(s) to tell about the (estimated) total size + * + * @param PaginableInterface $paginable a CollectionDocument or RelationshipObject + * @param int $exactTotal optional + * @param int $bestGuessTotal optional + */ + public function setCount(PaginableInterface $paginable, $exactTotal=null, $bestGuessTotal=null) { + $this->setPaginationMeta($paginable, $exactTotal, $bestGuessTotal); + } + + /** + * spec api + */ + + /** + * helper to get generate a correct page[before] link, use to apply manually + * + * @param string $baseOrCurrentUrl + * @param string $beforeCursor + * @return string + */ + public function generatePreviousLink($baseOrCurrentUrl, $beforeCursor) { + return $this->setQueryParameter($baseOrCurrentUrl, $this->getKeyword('page').'[before]', $beforeCursor); + } + + /** + * helper to get generate a correct page[after] link, use to apply manually + * + * @param string $baseOrCurrentUrl + * @param string $afterCursor + * @return string + */ + public function generateNextLink($baseOrCurrentUrl, $afterCursor) { + return $this->setQueryParameter($baseOrCurrentUrl, $this->getKeyword('page').'[after]', $afterCursor); + } + + /** + * pagination links are inside the links object that is a sibling of the paginated data + * + * ends up at one of: + * - /links/prev & /links/next + * - /data/relationships/foo/links/prev & /data/relationships/foo/links/next + * - /data/0/relationships/foo/links/prev & /data/0/relationships/foo/links/next + * + * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#terms-pagination-links + * + * @param PaginableInterface $paginable + * @param LinkObject $previousLinkObject + * @param LinkObject $nextLinkObject + */ + public function setPaginationLinkObjects(PaginableInterface $paginable, LinkObject $previousLinkObject, LinkObject $nextLinkObject) { + $paginable->addLinkObject('prev', $previousLinkObject); + $paginable->addLinkObject('next', $nextLinkObject); + } + + /** + * @param PaginableInterface $paginable + * @param string $baseOrCurrentUrl + * @param string $firstCursor + */ + public function setPaginationLinkObjectsWithoutNext(PaginableInterface $paginable, $baseOrCurrentUrl, $firstCursor) { + $this->setPaginationLinkObjects($paginable, new LinkObject($this->generatePreviousLink($baseOrCurrentUrl, $firstCursor)), new LinkObject()); + } + + /** + * @param PaginableInterface $paginable + * @param string $baseOrCurrentUrl + * @param string $lastCursor + */ + public function setPaginationLinkObjectsWithoutPrevious(PaginableInterface $paginable, $baseOrCurrentUrl, $lastCursor) { + $this->setPaginationLinkObjects($paginable, new LinkObject(), new LinkObject($this->generateNextLink($baseOrCurrentUrl, $lastCursor))); + } + + /** + * @param PaginableInterface $paginable + */ + public function setPaginationLinkObjectsExplicitlyEmpty(PaginableInterface $paginable) { + $this->setPaginationLinkObjects($paginable, new LinkObject(), new LinkObject()); + } + + /** + * pagination item metadata is the page meta at the top-level of a paginated item + * + * ends up at one of: + * - /data/meta/page + * - /data/relationships/foo/meta/page + * - /data/0/relationships/foo/meta/page + * + * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#terms-pagination-item-metadata + * + * @param ResourceInterface $resource + * @param string $cursor + */ + public function setItemMeta(ResourceInterface $resource, $cursor) { + $metadata = [ + 'cursor' => $cursor, + ]; + + if ($resource instanceof ResourceDocument) { + $resource->addMeta($this->getKeyword('page'), $metadata, $level=Document::LEVEL_RESOURCE); + } + else { + $resource->addMeta($this->getKeyword('page'), $metadata); + } + } + + /** + * pagination metadata is the page meta that is a sibling of the paginated data (and pagination links) + * + * ends up at one of: + * - /meta/page/total & /meta/page/estimatedTotal/bestGuess & /meta/page/rangeTruncated + * - /data/relationships/foo/meta/page/total & /data/relationships/foo/meta/page/estimatedTotal/bestGuess & /data/relationships/foo/meta/page/rangeTruncated + * - /data/0/relationships/foo/meta/page/total & /data/0/relationships/foo/meta/page/estimatedTotal/bestGuess & /data/0/relationships/foo/meta/page/rangeTruncated + * + * @see https://jsonapi.org/profiles/ethanresnick/cursor-pagination/#terms-pagination-metadata + * + * @param PaginableInterface $paginable + * @param int $exactTotal optional + * @param int $bestGuessTotal optional + * @param boolean $rangeIsTruncated optional, if both after and before are supplied but the items exceed requested or max size + */ + public function setPaginationMeta(PaginableInterface $paginable, $exactTotal=null, $bestGuessTotal=null, $rangeIsTruncated=null) { + $metadata = []; + + if ($exactTotal !== null) { + $metadata['total'] = $exactTotal; + } + if ($bestGuessTotal !== null) { + $metadata['estimatedTotal'] = [ + 'bestGuess' => $bestGuessTotal, + ]; + } + if ($rangeIsTruncated !== null) { + $metadata['rangeTruncated'] = $rangeIsTruncated; + } + + $paginable->addMeta($this->getKeyword('page'), $metadata); + } + + /** + * get an ErrorObject for when the requested sorting cannot efficiently be paginated + * + * ends up at: + * - /errors/0/code + * - /errors/0/status + * - /errors/0/source/parameter + * - /errors/0/links/type/0 + * - /errors/0/title optional + * - /errors/0/detail optional + * + * @param string $genericTitle optional + * @param string $specificDetails optional + * @return ErrorObject + */ + public function getUnsupportedSortErrorObject($genericTitle=null, $specificDetails=null) { + $errorObject = new ErrorObject('Unsupported sort'); + $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort'); + $errorObject->blameQueryParameter('sort'); + $errorObject->setHttpStatusCode(400); + + if ($genericTitle !== null) { + $errorObject->setHumanExplanation($genericTitle, $specificDetails); + } + + return $errorObject; + } + + /** + * get an ErrorObject for when the requested page size exceeds the server-defined max page size + * + * ends up at: + * - /errors/0/code + * - /errors/0/status + * - /errors/0/source/parameter + * - /errors/0/links/type/0 + * - /errors/0/meta/page/maxSize + * - /errors/0/title optional + * - /errors/0/detail optional + * + * @param int $maxSize + * @param string $genericTitle optional, e.g. 'Page size requested is too large.' + * @param string $specificDetails optional, e.g. 'You requested a size of 200, but 100 is the maximum.' + * @return ErrorObject + */ + public function getMaxPageSizeExceededErrorObject($maxSize, $genericTitle=null, $specificDetails=null) { + $errorObject = new ErrorObject('Max page size exceeded'); + $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded'); + $errorObject->blameQueryParameter($this->getKeyword('page').'[size]'); + $errorObject->setHttpStatusCode(400); + $errorObject->addMeta($this->getKeyword('page'), $value=['maxSize' => $maxSize]); + + if ($genericTitle !== null) { + $errorObject->setHumanExplanation($genericTitle, $specificDetails); + } + + return $errorObject; + } + + /** + * get an ErrorObject for when the requested page size is not a positive integer, or when the requested page after/before is not a valid cursor + * + * ends up at: + * - /errors/0/code + * - /errors/0/status + * - /errors/0/source/parameter + * - /errors/0/links/type/0 optional + * - /errors/0/title optional + * - /errors/0/detail optional + * + * @param int $queryParameter e.g. 'sort' or 'page[size]', aliasing should already be done using {@see getKeyword} + * @param string $typeLink optional + * @param string $genericTitle optional, e.g. 'Invalid Parameter.' + * @param string $specificDetails optional, e.g. 'page[size] must be a positive integer; got 0' + * @return ErrorObject + */ + public function getInvalidParameterValueErrorObject($queryParameter, $typeLink=null, $genericTitle=null, $specificDetails=null) { + $errorObject = new ErrorObject('Invalid parameter value'); + $errorObject->blameQueryParameter($queryParameter); + $errorObject->setHttpStatusCode(400); + + if ($typeLink !== null) { + $errorObject->appendTypeLink($typeLink); + } + + if ($genericTitle !== null) { + $errorObject->setHumanExplanation($genericTitle, $specificDetails); + } + + return $errorObject; + } + + /** + * get an ErrorObject for when range pagination requests (when both 'page[after]' and 'page[before]' are requested) are not supported + * + * ends up at: + * - /errors/0/code + * - /errors/0/status + * - /errors/0/links/type/0 + * + * @param string $genericTitle optional + * @param string $specificDetails optional + * @return ErrorObject + */ + public function getRangePaginationNotSupportedErrorObject($genericTitle=null, $specificDetails=null) { + $errorObject = new ErrorObject('Range pagination not supported'); + $errorObject->appendTypeLink('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported'); + $errorObject->setHttpStatusCode(400); + + if ($genericTitle !== null) { + $errorObject->setHumanExplanation($genericTitle, $specificDetails); + } + + return $errorObject; + } + + /** + * internal api + */ + + /** + * add or adjust a key in the query string of a url + * + * @param string $url + * @param string $key + * @param string $value + */ + private function setQueryParameter($url, $key, $value) { + $originalQuery = parse_url($url, PHP_URL_QUERY); + $decodedQuery = urldecode($originalQuery); + $originalIsEncoded = ($decodedQuery !== $originalQuery); + + $originalParameters = []; + parse_str($decodedQuery, $originalParameters); + + $newParameters = []; + parse_str($key.'='.$value, $newParameters); + + $fullParameters = array_replace_recursive($originalParameters, $newParameters); + + $newQuery = http_build_query($fullParameters); + if ($originalIsEncoded === false) { + $newQuery = urldecode($newQuery); + } + + $newUrl = str_replace($originalQuery, $newQuery, $url); + + return $newUrl; + } + + /** + * ProfileInterface + */ + + /** + * @inheritDoc + */ + public function getOfficialLink() { + return 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination/'; + } + + /** + * @inheritDoc + */ + public function getOfficialKeywords() { + return ['page']; + } +} From 5f50c11d3bf1c3596494e7380f5e972cba74dd11 Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Thu, 28 Feb 2019 22:47:59 +0100 Subject: [PATCH 3/4] add tests and examples --- examples/cursor_pagination_profile.php | 36 ++ examples/index.html | 1 + .../cursor_pagination_profile.json | 51 +++ .../cursor_pagination_profile.php | 29 ++ .../profiles/CursorPaginationProfileTest.php | 308 ++++++++++++++++++ 5 files changed, 425 insertions(+) create mode 100644 examples/cursor_pagination_profile.php create mode 100644 tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json create mode 100644 tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php create mode 100644 tests/profiles/CursorPaginationProfileTest.php diff --git a/examples/cursor_pagination_profile.php b/examples/cursor_pagination_profile.php new file mode 100644 index 0000000..c70fef7 --- /dev/null +++ b/examples/cursor_pagination_profile.php @@ -0,0 +1,36 @@ +setCursor($user1, 'ford'); +$profile->setCursor($user2, 'arthur'); +$profile->setCursor($user42, 'zaphod'); + +$document = CollectionDocument::fromResources($user1, $user2, $user42); +$document->applyProfile($profile); + +$profile->setCount($document, $exactTotal=3, $bestGuessTotal=10); +$profile->setLinksFirstPage($document, $currentUrl='/users?sort=42&page[size]=10', $lastCursor='zaphod'); + +/** + * get the json + */ + +$options = [ + 'prettyPrint' => true, +]; +echo '
'.$document->toJson($options);
diff --git a/examples/index.html b/examples/index.html
index 5a191c4..45100ac 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -49,6 +49,7 @@ 

Misc

  • Meta-only use-cases
  • Status-only
  • Example profile
  • +
  • Cursor pagination profile
  • Different ways to output
  • diff --git a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json new file mode 100644 index 0000000..b5aafee --- /dev/null +++ b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json @@ -0,0 +1,51 @@ +{ + "jsonapi": { + "version": "1.0" + }, + "links": { + "profile": [ + "https://jsonapi.org/profiles/ethanresnick/cursor-pagination/" + ], + "prev": null, + "next": { + "href": "/users?sort=42&page[size]=10&page[after]=zaphod" + } + }, + "meta": { + "page": { + "total": 3, + "estimatedTotal": { + "bestGuess": 10 + } + } + }, + "data": [ + { + "type": "user", + "id": "1", + "meta": { + "page": { + "cursor": "ford" + } + } + }, + { + "type": "user", + "id": "2", + "meta": { + "page": { + "cursor": "arthur" + } + } + }, + { + "type": "user", + "id": "42", + "meta": { + "page": { + "cursor": "zaphod" + } + } + } + ] +} diff --git a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php new file mode 100644 index 0000000..dc4db89 --- /dev/null +++ b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.php @@ -0,0 +1,29 @@ +setCursor($user1, 'ford'); + $profile->setCursor($user2, 'arthur'); + $profile->setCursor($user42, 'zaphod'); + + $document = CollectionDocument::fromResources($user1, $user2, $user42); + $document->applyProfile($profile); + + $profile->setCount($document, $exactTotal=3, $bestGuessTotal=10); + $profile->setLinksFirstPage($document, $currentUrl='/users?sort=42&page[size]=10', $lastCursor='zaphod'); + + return $document; + } +} diff --git a/tests/profiles/CursorPaginationProfileTest.php b/tests/profiles/CursorPaginationProfileTest.php new file mode 100644 index 0000000..183df74 --- /dev/null +++ b/tests/profiles/CursorPaginationProfileTest.php @@ -0,0 +1,308 @@ + 'pagination']); + $collection = new CollectionDocument(); + $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $firstCursor = 'bar'; + $lastCursor = 'foo'; + + $profile->setLinks($collection, $baseOrCurrentUrl, $firstCursor, $lastCursor); + + $array = $collection->toArray(); + + $this->assertArrayHasKey('links', $array); + $this->assertCount(2, $array['links']); + $this->assertArrayHasKey('prev', $array['links']); + $this->assertArrayHasKey('next', $array['links']); + $this->assertArrayHasKey('href', $array['links']['prev']); + $this->assertArrayHasKey('href', $array['links']['next']); + $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[before]='.$firstCursor, $array['links']['prev']['href']); + $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[after]='.$lastCursor, $array['links']['next']['href']); + } + + public function test_WithRelationship() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $document = new ResourceDocument('test', 1); + + $person1 = new ResourceObject('person', 1); + $person2 = new ResourceObject('person', 2); + $person42 = new ResourceObject('person', 42); + $profile->setCursor($person1, 'ford'); + $profile->setCursor($person2, 'arthur'); + $profile->setCursor($person42, 'zaphod'); + + $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $firstCursor = 'ford'; + $lastCursor = 'zaphod'; + $exactTotal = 3; + $bestGuessTotal = 10; + + $relationship = RelationshipObject::fromAnything([$person1, $person2, $person42]); + $profile->setLinks($relationship, $baseOrCurrentUrl, $firstCursor, $lastCursor); + $profile->setCount($relationship, $exactTotal, $bestGuessTotal); + + $document->addRelationshipObject('people', $relationship); + + $array = $document->toArray(); + + $this->assertArrayHasKey('data', $array); + $this->assertArrayHasKey('relationships', $array['data']); + $this->assertArrayHasKey('people', $array['data']['relationships']); + $this->assertArrayHasKey('links', $array['data']['relationships']['people']); + $this->assertArrayHasKey('data', $array['data']['relationships']['people']); + $this->assertArrayHasKey('meta', $array['data']['relationships']['people']); + $this->assertArrayHasKey('prev', $array['data']['relationships']['people']['links']); + $this->assertArrayHasKey('next', $array['data']['relationships']['people']['links']); + $this->assertArrayHasKey('pagination', $array['data']['relationships']['people']['meta']); + $this->assertArrayHasKey('href', $array['data']['relationships']['people']['links']['prev']); + $this->assertArrayHasKey('href', $array['data']['relationships']['people']['links']['next']); + $this->assertArrayHasKey('total', $array['data']['relationships']['people']['meta']['pagination']); + $this->assertArrayHasKey('estimatedTotal', $array['data']['relationships']['people']['meta']['pagination']); + $this->assertArrayHasKey('bestGuess', $array['data']['relationships']['people']['meta']['pagination']['estimatedTotal']); + $this->assertCount(3, $array['data']['relationships']['people']['data']); + $this->assertArrayHasKey('meta', $array['data']['relationships']['people']['data'][0]); + $this->assertArrayHasKey('pagination', $array['data']['relationships']['people']['data'][0]['meta']); + $this->assertArrayHasKey('cursor', $array['data']['relationships']['people']['data'][0]['meta']['pagination']); + } + + public function testSetLinksFirstPage_HappyPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $collection = new CollectionDocument(); + $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $lastCursor = 'foo'; + + $profile->setLinksFirstPage($collection, $baseOrCurrentUrl, $lastCursor); + + $array = $collection->toArray(); + + $this->assertArrayHasKey('links', $array); + $this->assertCount(2, $array['links']); + $this->assertArrayHasKey('prev', $array['links']); + $this->assertArrayHasKey('next', $array['links']); + $this->assertNull($array['links']['prev']); + $this->assertArrayHasKey('href', $array['links']['next']); + $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[after]='.$lastCursor, $array['links']['next']['href']); + } + + public function testSetLinksLastPage_HappyPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $collection = new CollectionDocument(); + $baseOrCurrentUrl = '/people?'.$profile->getKeyword('page').'[size]=10'; + $firstCursor = 'bar'; + + $profile->setLinksLastPage($collection, $baseOrCurrentUrl, $firstCursor); + + $array = $collection->toArray(); + + $this->assertArrayHasKey('links', $array); + $this->assertCount(2, $array['links']); + $this->assertArrayHasKey('prev', $array['links']); + $this->assertArrayHasKey('next', $array['links']); + $this->assertArrayHasKey('href', $array['links']['prev']); + $this->assertNull($array['links']['next']); + $this->assertSame('/people?'.$profile->getKeyword('page').'[size]=10&'.$profile->getKeyword('page').'[before]='.$firstCursor, $array['links']['prev']['href']); + } + + public function testSetCursor() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $resourceDocument = new ResourceDocument('user', 42); + + $profile->setCursor($resourceDocument, 'foo'); + + $array = $resourceDocument->toArray(); + + $this->assertArrayHasKey('data', $array); + $this->assertArrayHasKey('meta', $array['data']); + $this->assertArrayHasKey('pagination', $array['data']['meta']); + $this->assertArrayHasKey('cursor', $array['data']['meta']['pagination']); + $this->assertSame('foo', $array['data']['meta']['pagination']['cursor']); + } + + public function testSetPaginationLinkObjectsExplicitlyEmpty_HapptPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $collection = new CollectionDocument(); + + $profile->setPaginationLinkObjectsExplicitlyEmpty($collection); + + $array = $collection->toArray(); + + $this->assertArrayHasKey('links', $array); + $this->assertCount(2, $array['links']); + $this->assertArrayHasKey('prev', $array['links']); + $this->assertArrayHasKey('next', $array['links']); + $this->assertNull($array['links']['prev']); + $this->assertNull($array['links']['next']); + } + + public function testSetPaginationMeta() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $collection = new CollectionDocument(); + $exactTotal = 42; + $bestGuessTotal = 100; + $rangeIsTruncated = true; + + $profile->setPaginationMeta($collection, $exactTotal, $bestGuessTotal, $rangeIsTruncated); + + $array = $collection->toArray(); + + $this->assertArrayHasKey('meta', $array); + $this->assertArrayHasKey('pagination', $array['meta']); + $this->assertArrayHasKey('total', $array['meta']['pagination']); + $this->assertArrayHasKey('estimatedTotal', $array['meta']['pagination']); + $this->assertArrayHasKey('bestGuess', $array['meta']['pagination']['estimatedTotal']); + $this->assertArrayHasKey('rangeTruncated', $array['meta']['pagination']); + $this->assertSame(42, $array['meta']['pagination']['total']); + $this->assertSame(100, $array['meta']['pagination']['estimatedTotal']['bestGuess']); + $this->assertSame(true, $array['meta']['pagination']['rangeTruncated']); + } + + public function testGetUnsupportedSortErrorObject_HappyPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $genericTitle = 'foo'; + $specificDetails = 'bar'; + + $errorObject = $profile->getUnsupportedSortErrorObject($genericTitle, $specificDetails); + + $array = $errorObject->toArray(); + + $this->assertArrayHasKey('status', $array); + $this->assertArrayHasKey('code', $array); + $this->assertArrayHasKey('title', $array); + $this->assertArrayHasKey('detail', $array); + $this->assertArrayHasKey('links', $array); + $this->assertArrayHasKey('type', $array['links']); + $this->assertArrayHasKey('source', $array); + $this->assertArrayHasKey('parameter', $array['source']); + $this->assertCount(1, $array['links']['type']); + $this->assertSame('400', $array['status']); + $this->assertSame('Unsupported sort', $array['code']); + $this->assertSame($genericTitle, $array['title']); + $this->assertSame($specificDetails, $array['detail']); + $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/unsupported-sort', $array['links']['type'][0]); + $this->assertSame('sort', $array['source']['parameter']); + } + + public function testGetMaxPageSizeExceededErrorObject_HappyPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $maxSize = 42; + $genericTitle = 'foo'; + $specificDetails = 'bar'; + + $errorObject = $profile->getMaxPageSizeExceededErrorObject($maxSize, $genericTitle, $specificDetails); + + $array = $errorObject->toArray(); + + $this->assertArrayHasKey('status', $array); + $this->assertArrayHasKey('code', $array); + $this->assertArrayHasKey('title', $array); + $this->assertArrayHasKey('detail', $array); + $this->assertArrayHasKey('links', $array); + $this->assertArrayHasKey('type', $array['links']); + $this->assertArrayHasKey('source', $array); + $this->assertArrayHasKey('parameter', $array['source']); + $this->assertArrayHasKey('meta', $array); + $this->assertArrayHasKey('pagination', $array['meta']); + $this->assertArrayHasKey('maxSize', $array['meta']['pagination']); + $this->assertCount(1, $array['links']['type']); + $this->assertSame('400', $array['status']); + $this->assertSame('Max page size exceeded', $array['code']); + $this->assertSame($genericTitle, $array['title']); + $this->assertSame($specificDetails, $array['detail']); + $this->assertSame('pagination[size]', $array['source']['parameter']); + $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/max-size-exceeded', $array['links']['type'][0]); + $this->assertSame(42, $array['meta']['pagination']['maxSize']); + } + + public function testGetInvalidParameterValueErrorObject_HappyPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $queryParameter = 'pagination[size]'; + $typeLink = 'https://jsonapi.org'; + $genericTitle = 'foo'; + $specificDetails = 'bar'; + + $errorObject = $profile->getInvalidParameterValueErrorObject($queryParameter, $typeLink, $genericTitle, $specificDetails); + + $array = $errorObject->toArray(); + + $this->assertArrayHasKey('status', $array); + $this->assertArrayHasKey('code', $array); + $this->assertArrayHasKey('title', $array); + $this->assertArrayHasKey('detail', $array); + $this->assertArrayHasKey('links', $array); + $this->assertArrayHasKey('type', $array['links']); + $this->assertArrayHasKey('source', $array); + $this->assertArrayHasKey('parameter', $array['source']); + $this->assertCount(1, $array['links']['type']); + $this->assertSame('400', $array['status']); + $this->assertSame('Invalid parameter value', $array['code']); + $this->assertSame($genericTitle, $array['title']); + $this->assertSame($specificDetails, $array['detail']); + $this->assertSame('pagination[size]', $array['source']['parameter']); + $this->assertSame('https://jsonapi.org', $array['links']['type'][0]); + } + + public function testGetRangePaginationNotSupportedErrorObject_HappyPath() { + $profile = new CursorPaginationProfile(['page' => 'pagination']); + $genericTitle = 'foo'; + $specificDetails = 'bar'; + + $errorObject = $profile->getRangePaginationNotSupportedErrorObject($genericTitle, $specificDetails); + + $array = $errorObject->toArray(); + + $this->assertArrayHasKey('status', $array); + $this->assertArrayHasKey('code', $array); + $this->assertArrayHasKey('title', $array); + $this->assertArrayHasKey('detail', $array); + $this->assertArrayHasKey('links', $array); + $this->assertArrayHasKey('type', $array['links']); + $this->assertCount(1, $array['links']['type']); + $this->assertSame('400', $array['status']); + $this->assertSame('Range pagination not supported', $array['code']); + $this->assertSame($genericTitle, $array['title']); + $this->assertSame($specificDetails, $array['detail']); + $this->assertSame('https://jsonapi.org/profiles/ethanresnick/cursor-pagination/range-pagination-not-supported', $array['links']['type'][0]); + } + + public function testSetQueryParameter_HappyPath() { + $profile = new CursorPaginationProfile(); + $method = new \ReflectionMethod($profile, 'setQueryParameter'); + $method->setAccessible(true); + + $url = '/people?sort=x&page[size]=10&page[after]=foo'; + $key = 'page[after]'; + $value = 'bar'; + + $newUrl = $method->invoke($profile, $url, $key, $value); + + $this->assertSame('/people?sort=x&page[size]=10&page[after]=bar', $newUrl); + } + + public function testSetQueryParameter_EncodedUrl() { + $profile = new CursorPaginationProfile(); + $method = new \ReflectionMethod($profile, 'setQueryParameter'); + $method->setAccessible(true); + + $url = '/people?sort=x&page%5Bsize%5D=10&page%5Bafter%5D=foo'; + $key = 'page[after]'; + $value = 'bar'; + + $newUrl = $method->invoke($profile, $url, $key, $value); + + $this->assertSame('/people?sort=x&page%5Bsize%5D=10&page%5Bafter%5D=bar', $newUrl); + } +} From 13a2e37eed96c6081366b24e821fed8dc8c1c69a Mon Sep 17 00:00:00 2001 From: Lode Claassen Date: Sun, 3 Mar 2019 11:30:19 +0100 Subject: [PATCH 4/4] fix merge conflict --- .../cursor_pagination_profile/cursor_pagination_profile.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json index b5aafee..cb84fda 100644 --- a/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json +++ b/tests/example_output/cursor_pagination_profile/cursor_pagination_profile.json @@ -1,6 +1,6 @@ { "jsonapi": { - "version": "1.0" + "version": "1.1" }, "links": { "profile": [