diff --git a/.gitignore b/.gitignore index 4e131195..ae7ba16c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor/ build/ .idea/ composer.lock +perf/blackfire.io.env diff --git a/NOTICE b/NOTICE index bf30d0d8..db101ce9 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ Framework agnostic JSON API implementation -Copyright 2015 info@neomerx.com +Copyright 2015-2019 info@neomerx.com -This product includes software developed at Neomerx (www.neomerx.com). \ No newline at end of file +This product includes software developed at Neomerx (www.neomerx.com). diff --git a/README.md b/README.md index d1dac92b..19c6c584 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -[![Project Management](https://img.shields.io/badge/project-management-blue.svg)](https://waffle.io/neomerx/json-api) +[![Build Status](https://travis-ci.org/neomerx/json-api.svg?branch=master)](https://travis-ci.org/neomerx/json-api) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/neomerx/json-api/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/neomerx/json-api/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/neomerx/json-api/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/neomerx/json-api/?branch=master) -[![Build Status](https://travis-ci.org/neomerx/json-api.svg?branch=master)](https://travis-ci.org/neomerx/json-api) [![License](https://img.shields.io/packagist/l/neomerx/json-api.svg)](https://packagist.org/packages/neomerx/json-api) ## Description @@ -10,18 +9,19 @@ A good API is one of most effective ways to improve the experience for your clients. Standardized approaches for data formats and communication protocols increase productivity and make integration between applications smooth. -This framework agnostic package implements [JSON API](http://jsonapi.org/) specification **version v1.0** and helps focusing on core application functionality rather than on protocol implementation. It supports document structure, errors, data fetching as described in [JSON API Format](http://jsonapi.org/format/) and covers parsing and checking HTTP request parameters and headers. For instance it helps to correctly respond with ```Unsupported Media Type``` (HTTP code 415) and ```Not Acceptable``` (HTTP code 406) to invalid requests. You don't need to manually validate all input parameters on every request. You can configure what parameters are supported by your services and this package will check incoming requests automatically. It greatly simplifies API development and fully support specification. In particular +This framework agnostic package implements [JSON API](http://jsonapi.org/) specification **version v1.1** and helps focusing on core application functionality rather than on protocol implementation. It supports document structure, errors, data fetching as described in [JSON API Format](http://jsonapi.org/format/) and covers parsing and checking HTTP request parameters and headers. For instance it helps to correctly respond with ```Unsupported Media Type``` (HTTP code 415) and ```Not Acceptable``` (HTTP code 406) to invalid requests. You don't need to manually validate all input parameters on every request. You can configure what parameters are supported by your services and this package will check incoming requests automatically. It greatly simplifies API development and fully support specification. In particular * Resource attributes and relationships * Polymorphic resource data and relationships * Compound documents with inclusion of related resources (circular resource references supported) * Meta information for document, resources, errors, relationship and link objects +* Profiles * Parsing HTTP `Accept` and `Content-Type` headers in accordance with [RFC 7231](https://tools.ietf.org/html/rfc7231) * Parsing HTTP query parameters (e.g. pagination, sorting and etc) * Sparse fieldsets and customized included paths * Errors -High code quality and **100% test coverage** with **200+ tests**. Production ready. +High code quality and **100% test coverage** with **150+ tests**. Production ready. **To find out more, please check out the [Wiki](https://github.com/neomerx/json-api/wiki) and [Sample App](/sample)**. @@ -50,8 +50,10 @@ Assuming you've got an ```$author``` of type ```\Author``` you can encode it to ```php $encoder = Encoder::instance([ - '\Author' => '\AuthorSchema', -], new EncoderOptions(JSON_PRETTY_PRINT, 'http://example.com/api/v1')); + Author::class => AuthorSchema::class, + ]) + ->withUrlPrefix('http://example.com/api/v1') + ->withEncodeOptions(JSON_PRETTY_PRINT); echo $encoder->encodeData($author) . PHP_EOL; ``` @@ -60,15 +62,22 @@ will output ```json { - "data": { - "type": "people", - "id": "123", - "attributes": { - "first_name": "John", - "last_name": "Dow" + "data" : { + "type" : "people", + "id" : "123", + "attributes" : { + "first-name": "John", + "last-name": "Doe" + }, + "relationships" : { + "comments" : { + "links": { + "related" : "http://example.com/api/v1/people/123/comments" + } + } }, - "links": { - "self": "http://example.com/api/v1/people/123" + "links" : { + "self" : "http://example.com/api/v1/people/123" } } } @@ -79,28 +88,42 @@ The ```AuthorSchema``` provides information about resource's attributes and migh ```php class AuthorSchema extends BaseSchema { - protected $resourceType = 'people'; + public function getType(): string + { + return 'people'; + } public function getId($author): ?string { - /** @var Author $author */ return $author->authorId; } - public function getAttributes($author, array $fieldKeysFilter = null): ?array + public function getAttributes($author): iterable { - /** @var Author $author */ return [ - 'first_name' => $author->firstName, - 'last_name' => $author->lastName, + 'first-name' => $author->firstName, + 'last-name' => $author->lastName, + ]; + } + + public function getRelationships($author): iterable + { + return [ + 'comments' => [ + self::RELATIONSHIP_LINKS_SELF => false, + self::RELATIONSHIP_LINKS_RELATED => true, + + // Data include supported as other cool features + // self::RELATIONSHIP_DATA => $author->comments, + ], ]; } } ``` -The first ```EncoderOptions``` parameter ```JSON_PRETTY_PRINT``` is a PHP predefined [JSON constant](http://php.net/manual/en/json.constants.php). +Parameter ```http://example.com/api/v1``` is a URL prefix that will be applied to all encoded links unless they have a flag set telling not to add any prefixes. -The second ```EncoderOptions``` parameter ```http://example.com/api/v1``` is a URL prefix that will be applied to all encoded links unless they have ```$treatAsHref``` flag set to ```true```. +Parameter ```JSON_PRETTY_PRINT``` is a PHP predefined [JSON constant](http://php.net/manual/en/json.constants.php). A sample program with encoding of multiple, nested, filtered objects and more is [here](sample). @@ -108,7 +131,7 @@ A sample program with encoding of multiple, nested, filtered objects and more is ## Versions -Current version is 2.x (PHP 7.1+) for older PHP (PHP 5.5 - 7.0, HHVM) please use version 1.x. +Current version is 3.x (PHP 7.1+) for older PHP (PHP 5.5 - 7.0, HHVM) please use version 1.x. ## Questions? diff --git a/composer.json b/composer.json index 39b0c251..2573652f 100644 --- a/composer.json +++ b/composer.json @@ -21,15 +21,13 @@ } ], "require": { - "php": ">=7.1.0", - "psr/log": "^1.0" + "php": ">=7.1.0" }, "require-dev": { "phpunit/phpunit": "^7.0", "mockery/mockery": "^1.0", "scrutinizer/ocular": "^1.4", "squizlabs/php_codesniffer": "^2.9", - "monolog/monolog": "^1.23", "phpmd/phpmd": "^2.6" }, "minimum-stability": "stable", @@ -37,7 +35,7 @@ "psr-4": { "Neomerx\\JsonApi\\": "src/" }, - "files": ["src/I18n/translate.php"] + "files": ["src/I18n/format.php"] }, "autoload-dev": { "psr-4": { @@ -48,10 +46,8 @@ "scripts": { "test": ["@test-unit", "@test-cs", "@test-md"], "test-unit": "./vendor/phpunit/phpunit/phpunit --coverage-text", - "test-unit-with-coverage": "phpdbg -qrr ./vendor/bin/phpunit --coverage-text", + "test-unit-phpdbg": "phpdbg -qrr ./vendor/bin/phpunit --coverage-text", "test-cs": "./vendor/bin/phpcs -p -s --standard=PSR2 ./src ./tests", - "test-md": "./vendor/bin/phpmd ./src text codesize,controversial,cleancode,design,unusedcode,naming", - - "perf-test": "docker-compose run --rm cli_7_1_php php /app/sample/sample.php -t=10000" + "test-md": "./vendor/bin/phpmd ./src text codesize,controversial,cleancode,design,unusedcode,naming" } } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index fd1a2413..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Usefull links -# ============= -# https://hub.docker.com/_/php/ -# https://docs.docker.com/compose/overview/ -# https://docs.docker.com/compose/compose-file/ -# -# Usefull commands -# ================ -# -# Connect into command line of container -# $ docker-compose up -d && docker exec -it cli_json_api /bin/bash -# Tip: `Ctrl+p` + `Ctrl+q` + `Enter` (to exit container bash) -# -# Remove containers -# $ docker-compose down -# -# In container -# ------------ -# Run test suit -# $ cd /app && ./vendor/bin/phpunit -# Performance test -# $ cd /app/sample/ && time php sample.php -t=10000 - -cli_7_1_php: - image: php:7.1-cli - container_name: cli_php_json_api - volumes: - - .:/app - working_dir: /app - tty: true - -cli_7_2_php: - image: php:7.2-cli - container_name: cli_php_json_api - volumes: - - .:/app - working_dir: /app - tty: true - -cli_hhvm: - image: webdevops/hhvm - container_name: cli_hhvm_json_api - volumes: - - .:/app - working_dir: /app - tty: true diff --git a/perf/Dockerfile b/perf/Dockerfile new file mode 100644 index 00000000..3c69b7bd --- /dev/null +++ b/perf/Dockerfile @@ -0,0 +1,13 @@ +FROM php:7.3-cli + +ARG DEBIAN_FRONTEND=noninteractive + +RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \ + && mkdir -p /tmp/blackfire \ + && curl -A "Docker" -L -s https://blackfire.io/api/v1/releases/probe/php/linux/amd64/$version | tar zxp -C /tmp/blackfire \ + && curl -A "Docker" -L -s https://blackfire.io/api/v1/releases/client/linux_static/amd64 | tar zxp -C /tmp/blackfire \ + && mv /tmp/blackfire/blackfire /usr/bin/blackfire \ + && mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get('extension_dir');")/blackfire.so \ + && printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" > /usr/local/etc/php/conf.d/blackfire.ini \ + && rm -Rf /tmp/blackfire \ + && apt-get clean; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* diff --git a/perf/blackfire.io.env.sample b/perf/blackfire.io.env.sample new file mode 100644 index 00000000..f7324a24 --- /dev/null +++ b/perf/blackfire.io.env.sample @@ -0,0 +1,7 @@ +# +# Credential should be taken from https://blackfire.io/my/settings/credentials +# +BLACKFIRE_CLIENT_ID= +BLACKFIRE_CLIENT_TOKEN= +BLACKFIRE_SERVER_ID= +BLACKFIRE_SERVER_TOKEN= diff --git a/perf/docker-compose.yml b/perf/docker-compose.yml new file mode 100644 index 00000000..d8789adb --- /dev/null +++ b/perf/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.7' + +services: + cli_php: + build: + context: ./ + dockerfile: Dockerfile + container_name: cli_php_json_api_blackfire + env_file: blackfire.io.env + volumes: + - type: bind + source: ./../ + target: /app + working_dir: /app + tty: true diff --git a/perf/readme.md b/perf/readme.md new file mode 100644 index 00000000..f217c3f7 --- /dev/null +++ b/perf/readme.md @@ -0,0 +1,28 @@ +# Overview + +This is performance test suite for the library that uses [Blackfire](https://blackfire.io) _Performance Management Solution_. + +## Prerequisites +- Install [docker](https://docs.docker.com/install/#supported-platforms); +- Install [docker-compose](https://docs.docker.com/compose/install/). + +## Installation + +- Copy `blackfire.io.env.sample` to `blackfire.io.env`; +- Put your Client ID, Client Token, Server ID and Server Token to `blackfire.io.env` from [Blackfire.io credentials page](https://blackfire.io/my/settings/credentials) (registration needed). + +## Profile Performance + +```bash +$ docker-compose run --rm cli_php blackfire run php -d zend.assertions=-1 /app/sample/sample.php -t=100 +``` + +The output will contain basic performance info and a URL with detailed profiling info [such as this one](https://blackfire.io/profiles/207fb294-d851-48ad-a31c-db29478172e3/graph). + +> Note: The **first** run will download necessary docker images which takes some time. The subsequent runs will not require such downloads and be faster. + +The created container can be removed from the local machine with + +```bash +$ docker rmi perf_cli_php +``` diff --git a/sample/Application/EncodeSamples.php b/sample/Application/EncodeSamples.php index 93c596b5..59075d3a 100644 --- a/sample/Application/EncodeSamples.php +++ b/sample/Application/EncodeSamples.php @@ -1,7 +1,7 @@ - AuthorSchema::class, - ], new EncoderOptions(JSON_PRETTY_PRINT, 'http://example.com/api/v1')); + ])->withEncodeOptions(JSON_PRETTY_PRINT); - $result = $encoder->encodeData($author); + $result = $encoder->withUrlPrefix('http://example.com/api/v1')->encodeData($author); return $result; } @@ -58,7 +56,7 @@ public function getBasicExample() * * @return string */ - public function getIncludedObjectsExample() + public function getIncludedObjectsExample(): string { $author = Author::instance('123', 'John', 'Dow'); $comments = [ @@ -73,9 +71,16 @@ public function getIncludedObjectsExample() Comment::class => CommentSchema::class, Post::class => PostSchema::class, Site::class => SiteSchema::class - ], new EncoderOptions(JSON_PRETTY_PRINT, 'http://example.com')); + ])->withEncodeOptions(JSON_PRETTY_PRINT); - $result = $encoder->encodeData($site); + $result = $encoder + ->withUrlPrefix('http://example.com') + ->withIncludedPaths([ + 'posts', + 'posts.author', + 'posts.comments', + ]) + ->encodeData($site); return $result; } @@ -85,7 +90,7 @@ public function getIncludedObjectsExample() * * @return string */ - public function getSparseAndFieldSetsExample() + public function getSparseAndFieldSetsExample(): string { $author = Author::instance('123', 'John', 'Dow'); $comments = [ @@ -95,25 +100,27 @@ public function getSparseAndFieldSetsExample() $post = Post::instance('321', 'Included objects', 'Yes, it is supported', $author, $comments); $site = Site::instance('1', 'JSON API Samples', [$post]); - $options = new EncodingParameters([ - // Paths to be included. Note 'posts.comments' will not be shown. - 'posts.author' - ], [ - // Attributes and relationships that should be shown - 'sites' => ['name', 'posts'], - 'posts' => ['author'], - 'people' => ['first_name'], - ]); - SiteSchema::$isShowCustomLinks = false; $encoder = Encoder::instance([ Author::class => AuthorSchema::class, Comment::class => CommentSchema::class, Post::class => PostSchema::class, Site::class => SiteSchema::class - ], new EncoderOptions(JSON_PRETTY_PRINT)); - - $result = $encoder->encodeData($site, $options); + ])->withEncodeOptions(JSON_PRETTY_PRINT); + + $result = $encoder + ->withIncludedPaths([ + // Paths to be included. Note 'posts.comments' will not be shown. + 'posts', + 'posts.author', + ]) + ->withFieldSets([ + // Attributes and relationships that should be shown + 'sites' => ['name', 'posts'], + 'posts' => ['author'], + 'people' => ['first_name'], + ]) + ->encodeData($site); return $result; } @@ -123,7 +130,7 @@ public function getSparseAndFieldSetsExample() * * @return string */ - public function getTopLevelMetaAndLinksExample() + public function getTopLevelMetaAndLinksExample(): string { $author = Author::instance('123', 'John', 'Dow'); $meta = [ @@ -135,10 +142,10 @@ public function getTopLevelMetaAndLinksExample() ] ]; $links = [ - Link::FIRST => new Link('http://example.com/people?first', null, true), - Link::LAST => new Link('http://example.com/people?last', null, true), - Link::PREV => new Link('http://example.com/people?prev', null, true), - Link::NEXT => new Link('http://example.com/people?next', null, true), + Link::FIRST => new Link(false,'http://example.com/people?first', false), + Link::LAST => new Link(false,'http://example.com/people?last', false), + Link::PREV => new Link(false,'http://example.com/people?prev', false), + Link::NEXT => new Link(false,'http://example.com/people?next', false), ]; $encoder = Encoder::instance([ @@ -146,9 +153,13 @@ public function getTopLevelMetaAndLinksExample() Comment::class => CommentSchema::class, Post::class => PostSchema::class, Site::class => SiteSchema::class - ], new EncoderOptions(JSON_PRETTY_PRINT, 'http://example.com')); + ])->withEncodeOptions(JSON_PRETTY_PRINT); - $result = $encoder->withLinks($links)->withMeta($meta)->encodeData($author); + $result = $encoder + ->withLinks($links) + ->withMeta($meta) + ->withUrlPrefix('http://example.com') + ->encodeData($author); return $result; } @@ -158,13 +169,13 @@ public function getTopLevelMetaAndLinksExample() * * @return array */ - public function getDynamicSchemaExample() + public function getDynamicSchemaExample(): array { $site = Site::instance('1', 'JSON API Samples', []); $encoder = Encoder::instance([ Site::class => SiteSchema::class, - ], new EncoderOptions(JSON_PRETTY_PRINT)); + ])->withEncodeOptions(JSON_PRETTY_PRINT); SiteSchema::$isShowCustomLinks = false; $noLinksResult = $encoder->encodeData($site); @@ -188,10 +199,6 @@ public function getDynamicSchemaExample() public function runPerformanceTestForSmallNestedResources(int $iterations): array { $closure = function () use ($iterations) { - $options = new EncodingParameters( - ['posts.author'], - ['sites' => ['name'], 'people' => ['first_name']] - ); $encoder = Encoder::instance([ Author::class => AuthorSchema::class, Comment::class => CommentSchema::class, @@ -211,13 +218,15 @@ public function runPerformanceTestForSmallNestedResources(int $iterations): arra $site = Site::instance('1', 'JSON API Samples' . $rand, [$post]); $encoder - ->withLinks([Link::SELF => new Link('http://example.com/sites/1?' . $rand, null, true)]) + ->withLinks([Link::SELF => new Link(false,'http://example.com/sites/1?' . $rand, false)]) ->withMeta(['some' => ['meta' => 'information' . $rand]]) - ->encodeData($site, $options); + ->withIncludedPaths(['posts.author']) + ->withFieldSets(['sites' => ['name'], 'people' => ['first_name']]) + ->encodeData($site); } }; - $timeSpent = 0; + $timeSpent = 0.0; $bytesUsed = 0; $this->getTime($closure, $timeSpent, $bytesUsed); @@ -249,10 +258,6 @@ public function runPerformanceTestForBigCollection(int $numberOfItems): array $sites[] = $site; } - $options = new EncodingParameters( - ['posts.author', 'posts.comments'], - ['sites' => ['name'], 'people' => ['first_name']] - ); $encoder = Encoder::instance([ Author::class => AuthorSchema::class, Comment::class => CommentSchema::class, @@ -260,10 +265,13 @@ public function runPerformanceTestForBigCollection(int $numberOfItems): array Site::class => SiteSchema::class ]); - $encoder->encodeData($sites, $options); + $encoder + ->withIncludedPaths(['posts.author', 'posts.comments']) + ->withFieldSets(['sites' => ['name'], 'people' => ['first_name']]) + ->encodeData($sites); }; - $timeSpent = 0; + $timeSpent = 0.0; $bytesUsed = 0; $this->getTime($closure, $timeSpent, $bytesUsed); diff --git a/sample/Models/Author.php b/sample/Models/Author.php index 8fe9d68f..6c4e3eb3 100644 --- a/sample/Models/Author.php +++ b/sample/Models/Author.php @@ -1,7 +1,7 @@ -authorId; + return (string)$author->authorId; } /** * @inheritdoc */ - public function getAttributes($author, array $fieldKeysFilter = null): ? array + public function getAttributes($author): iterable { /** @var Author $author */ return [ @@ -49,4 +52,12 @@ public function getAttributes($author, array $fieldKeysFilter = null): ? array 'last_name' => $author->lastName, ]; } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + return []; + } } diff --git a/sample/Schemas/CommentSchema.php b/sample/Schemas/CommentSchema.php index 458488f1..c570269d 100644 --- a/sample/Schemas/CommentSchema.php +++ b/sample/Schemas/CommentSchema.php @@ -1,7 +1,7 @@ -commentId; + assert($comment instanceof Comment); + + return (string)$comment->commentId; } /** * @inheritdoc */ - public function getAttributes($comment, array $fieldKeysFilter = null): ?array + public function getAttributes($comment): iterable { - /** @var Comment $comment */ + assert($comment instanceof Comment); + return [ 'body' => $comment->body, ]; @@ -57,11 +57,16 @@ public function getAttributes($comment, array $fieldKeysFilter = null): ?array /** * @inheritdoc */ - public function getRelationships($comment, bool $isPrimary, array $includeRelationships): ?array + public function getRelationships($comment): iterable { - /** @var Comment $comment */ + assert($comment instanceof Comment); + return [ - 'author' => [self::DATA => $comment->author], + 'author' => [ + self::RELATIONSHIP_DATA => $comment->author, + self::RELATIONSHIP_LINKS_SELF => false, + self::RELATIONSHIP_LINKS_RELATED => false, + ], ]; } } diff --git a/sample/Schemas/PostSchema.php b/sample/Schemas/PostSchema.php index a77429e2..84477f27 100644 --- a/sample/Schemas/PostSchema.php +++ b/sample/Schemas/PostSchema.php @@ -1,7 +1,7 @@ -postId; + assert($post instanceof Post); + + return (string)$post->postId; } /** * @inheritdoc */ - public function getAttributes($post, array $fieldKeysFilter = null): ?array + public function getAttributes($post, array $fieldKeysFilter = null): iterable { - /** @var Post $post */ + assert($post instanceof Post); + return [ 'title' => $post->title, 'body' => $post->body, @@ -53,12 +58,21 @@ public function getAttributes($post, array $fieldKeysFilter = null): ?array /** * @inheritdoc */ - public function getRelationships($post, bool $isPrimary, array $includeRelationships): ?array + public function getRelationships($post): iterable { - /** @var Post $post */ + assert($post instanceof Post); + return [ - 'author' => [self::DATA => $post->author], - 'comments' => [self::DATA => $post->comments], + 'author' => [ + self::RELATIONSHIP_DATA => $post->author, + self::RELATIONSHIP_LINKS_SELF => false, + self::RELATIONSHIP_LINKS_RELATED => false, + ], + 'comments' => [ + self::RELATIONSHIP_DATA => $post->comments, + self::RELATIONSHIP_LINKS_SELF => false, + self::RELATIONSHIP_LINKS_RELATED => false, + ], ]; } } diff --git a/sample/Schemas/SiteSchema.php b/sample/Schemas/SiteSchema.php index 020b9800..3ea72af5 100644 --- a/sample/Schemas/SiteSchema.php +++ b/sample/Schemas/SiteSchema.php @@ -1,7 +1,7 @@ -siteId; + assert($site instanceof Site); + + return (string)$site->siteId; } /** * @inheritdoc */ - public function getAttributes($site, array $fieldKeysFilter = null): ?array + public function getAttributes($site, array $fieldKeysFilter = null): iterable { - /** @var Site $site */ + assert($site instanceof Site); + return [ 'name' => $site->name, ]; @@ -58,32 +63,22 @@ public function getAttributes($site, array $fieldKeysFilter = null): ?array /** * @inheritdoc */ - public function getRelationships($site, bool $isPrimary, array $includeRelationships): ?array + public function getRelationships($site): iterable { - /** @var Site $site */ + assert($site instanceof Site); $links = static::$isShowCustomLinks === false ? [] : [ - 'some-sublink' => new Link($this->getSelfSubUrl($site) . '/resource-sublink'), - 'external-link' => new Link('www.example.com', null, true), + 'some-sublink' => new Link(true, $this->getSelfSubUrl($site) . '/resource-sublink', false), + 'external-link' => new Link(false,'www.example.com', false), ]; return [ 'posts' => [ - self::DATA => $site->posts, - self::LINKS => $links, + self::RELATIONSHIP_DATA => $site->posts, + self::RELATIONSHIP_LINKS => $links, + self::RELATIONSHIP_LINKS_SELF => true, + self::RELATIONSHIP_LINKS_RELATED => false, ], ]; } - - /** - * @inheritdoc - */ - public function getIncludePaths(): array - { - return [ - 'posts', - 'posts.author', - 'posts.comments', - ]; - } } diff --git a/sample/composer.json b/sample/composer.json index 330fa56a..dfe22c38 100644 --- a/sample/composer.json +++ b/sample/composer.json @@ -17,5 +17,10 @@ "require": { "php": ">=7.1.0", "psr/log": "^1.0" + }, + "scripts": { + "perf-test-php-7-1": "docker-compose run --rm cli_7_1_php php -d zend.assertions=-1 /app/sample/sample.php -t=10000", + "perf-test-php-7-2": "docker-compose run --rm cli_7_2_php php -d zend.assertions=-1 /app/sample/sample.php -t=10000", + "perf-test-php-7-3": "docker-compose run --rm cli_7_3_php php -d zend.assertions=-1 /app/sample/sample.php -t=10000" } } diff --git a/sample/sample.php b/sample/sample.php index 446bb344..5e56d480 100644 --- a/sample/sample.php +++ b/sample/sample.php @@ -1,7 +1,7 @@ -samples->getBasicExample() . PHP_EOL; @@ -51,8 +51,10 @@ private function showBasicExample() /** * Shows how objects are put to 'included'. + * + * @return void */ - private function showIncludedObjectsExample() + private function showIncludedObjectsExample(): void { echo 'Neomerx JSON API sample application (included objects)' . PHP_EOL; echo $this->samples->getIncludedObjectsExample() . PHP_EOL; @@ -60,8 +62,10 @@ private function showIncludedObjectsExample() /** * Shows sparse and field set filters. + * + * @return void */ - private function showSparseAndFieldSetsExample() + private function showSparseAndFieldSetsExample(): void { echo 'Neomerx JSON API sample application (sparse and field sets)' . PHP_EOL; echo $this->samples->getSparseAndFieldSetsExample() . PHP_EOL; @@ -69,8 +73,10 @@ private function showSparseAndFieldSetsExample() /** * Shows sparse and field set filters. + * + * @return void */ - private function showTopLevelMetaAndLinksExample() + private function showTopLevelMetaAndLinksExample(): void { echo 'Neomerx JSON API sample application (top level links and meta information)' . PHP_EOL; echo $this->samples->getTopLevelMetaAndLinksExample() . PHP_EOL; @@ -78,8 +84,10 @@ private function showTopLevelMetaAndLinksExample() /** * Shows how schema could change dynamically. + * + * @return void */ - private function dynamicSchemaExample() + private function dynamicSchemaExample(): void { echo 'Neomerx JSON API sample application (dynamic schema)' . PHP_EOL; @@ -92,8 +100,10 @@ private function dynamicSchemaExample() * Run performance test for encoding many times a relatively small but nested resources. * * @param int $num + * + * @return void */ - private function runPerformanceTestForSmallNestedResources($num) + private function runPerformanceTestForSmallNestedResources(int $num): void { echo "Neomerx JSON API performance test ($num iterations for small resources)... "; [$time, $bytes] = $this->samples->runPerformanceTestForSmallNestedResources($num); @@ -105,8 +115,10 @@ private function runPerformanceTestForSmallNestedResources($num) * Run performance test for encoding once a big and nested resource. * * @param int $num + * + * @return void */ - private function runPerformanceTestForBigCollection($num) + private function runPerformanceTestForBigCollection(int $num): void { echo "Neomerx JSON API performance test (1 iteration for $num resources)... "; [$time, $bytes] = $this->samples->runPerformanceTestForBigCollection($num); @@ -116,8 +128,10 @@ private function runPerformanceTestForBigCollection($num) /** * Main entry point. + * + * @return void */ - public function main() + public function main(): void { $args = getopt('t::'); diff --git a/spec/current.txt b/spec/current.txt new file mode 100644 index 00000000..c7ca6fd3 --- /dev/null +++ b/spec/current.txt @@ -0,0 +1,1631 @@ +Specification v1.1 (Still in Development) +Status + +This page will always present the most recent text for JSON:API v1.1. Version 1.1 is a release candidate. As such, the content on this page is unlikely to change. However, some changes may still occur if implementation experience proves that they are necessary before this version is finalized. + +This version is expected to be finalized and released on January 31, 2019 (provided there are two compliant implementations by that date; if not the release will wait until such implementations exist to prove its viability). + +If you have concerns about the changes in this draft, catch an error in the specification’s text, or write an implementation, please let us know by opening an issue or pull request at our GitHub repository. + +You can also propose additions to JSON:API in our discussion forum. Keep in mind, though, that all new versions of JSON:API must be backwards compatible using a never remove, only add strategy. +Introduction + +JSON:API is a specification for how a client should request that resources be fetched or modified, and how a server should respond to those requests. JSON:API can also be easily extended with profiles. + +JSON:API is designed to minimize both the number of requests and the amount of data transmitted between clients and servers. This efficiency is achieved without compromising readability, flexibility, or discoverability. + +JSON:API requires use of the JSON:API media type (application/vnd.api+json) for exchanging data. +Conventions + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. +Content Negotiation +Universal Responsibilities + +The JSON:API media type is application/vnd.api+json. Clients and servers MUST send all JSON:API data using this media type in the Content-Type header. + +Further, the JSON:API media type MUST always be specified with either no media type parameters or with only the profile parameter. This applies to both the Content-Type and Accept headers. + + Note: A media type parameter is an extra piece of information that can accompany a media type. For example, in the header Content-Type: text/html; charset="utf-8", the media type is text/html and charset is a parameter. + +The profile parameter is used to support profiles. +Client Responsibilities + +Clients that include the JSON:API media type in their Accept header MUST specify the media type there at least once without any media type parameters. + +When processing a JSON:API response document, clients MUST ignore any parameters other than profile in the server’s Content-Type header. +Server Responsibilities + +Servers MUST respond with a 415 Unsupported Media Type status code if a request specifies the header Content-Type: application/vnd.api+json with any media type parameters other than profile. + +Servers MUST respond with a 406 Not Acceptable status code if a request’s Accept header contains the JSON:API media type and all instances of that media type are modified with media type parameters. + + Note: These content negotiation requirements exist to allow future versions of this specification to add other media type parameters for extension negotiation and versioning. + +Document Structure + +This section describes the structure of a JSON:API document, which is identified by the media type application/vnd.api+json. JSON:API documents are defined in JavaScript Object Notation (JSON) [RFC8259]. + +Although the same media type is used for both request and response documents, certain aspects are only applicable to one or the other. These differences are called out below. + +Unless otherwise noted, objects defined by this specification MUST NOT contain any additional members. Client and server implementations MUST ignore members not recognized by this specification. + + Note: These conditions allow this specification to evolve through additive changes. + +Top Level + +A JSON object MUST be at the root of every JSON:API request and response containing data. This object defines a document’s “top level”. + +A document MUST contain at least one of the following top-level members: + + data: the document’s “primary data” + errors: an array of error objects + meta: a meta object that contains non-standard meta-information. + +The members data and errors MUST NOT coexist in the same document. + +A document MAY contain any of these top-level members: + + jsonapi: an object describing the server’s implementation + links: a links object related to the primary data. + included: an array of resource objects that are related to the primary data and/or each other (“included resources”). + +If a document does not contain a top-level data key, the included member MUST NOT be present either. + +The top-level links object MAY contain the following members: + + self: the link that generated the current response document. + related: a related resource link when the primary data represents a resource relationship. + profile: an array of links, each specifying a profile in use in the document. + pagination links for the primary data. + +The document’s “primary data” is a representation of the resource or collection of resources targeted by a request. + +Primary data MUST be either: + + a single resource object, a single resource identifier object, or null, for requests that target single resources + an array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections + +For example, the following primary data is a single resource object: + +{ + "data": { + "type": "articles", + "id": "1", + "attributes": { + // ... this article's attributes + }, + "relationships": { + // ... this article's relationships + } + } +} + +The following primary data is a single resource identifier object that references the same resource: + +{ + "data": { + "type": "articles", + "id": "1" + } +} + +A logical collection of resources MUST be represented as an array, even if it only contains one item or is empty. +Resource Objects + +“Resource objects” appear in a JSON:API document to represent resources. + +A resource object MUST contain at least the following top-level members: + + id + type + +Exception: The id member is not required when the resource object originates at the client and represents a new resource to be created on the server. + +In addition, a resource object MAY contain any of these top-level members: + + attributes: an attributes object representing some of the resource’s data. + relationships: a relationships object describing relationships between the resource and other JSON:API resources. + links: a links object containing links related to the resource. + meta: a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship. + +Here’s how an article (i.e. a resource of type “articles”) might appear in a document: + +// ... +{ + "type": "articles", + "id": "1", + "attributes": { + "title": "Rails is Omakase" + }, + "relationships": { + "author": { + "links": { + "self": "/articles/1/relationships/author", + "related": "/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + } + } +} +// ... + +Identification + +Every resource object MUST contain an id member and a type member. The values of the id and type members MUST be strings. + +Within a given API, each resource object’s type and id pair MUST identify a single, unique resource. (The set of URIs controlled by a server, or multiple servers acting as one, constitute an API.) + +The type member is used to describe resource objects that share common attributes and relationships. + +The values of type members MUST adhere to the same constraints as member names. + + Note: This spec is agnostic about inflection rules, so the value of type can be either plural or singular. However, the same value should be used consistently throughout an implementation. + +Fields + +A resource object’s attributes and its relationships are collectively called its “fields”. + +Fields for a resource object MUST share a common namespace with each other and with type and id. In other words, a resource can not have an attribute and relationship with the same name, nor can it have an attribute or relationship named type or id. +Attributes + +The value of the attributes key MUST be an object (an “attributes object”). Members of the attributes object (“attributes”) represent information about the resource object in which it’s defined. + +Attributes may contain any valid JSON value. + +Complex data structures involving JSON objects and arrays are allowed as attribute values. However, any object that constitutes or is contained in an attribute MUST NOT contain a relationships or links member, as those members are reserved by this specification for future use. + +Although has-one foreign keys (e.g. author_id) are often stored internally alongside other information to be represented in a resource object, these keys SHOULD NOT appear as attributes. + + Note: See fields and member names for more restrictions on this container. + +Relationships + +The value of the relationships key MUST be an object (a “relationships object”). Members of the relationships object (“relationships”) represent references from the resource object in which it’s defined to other resource objects. + +Relationships may be to-one or to-many. + +A “relationship object” MUST contain at least one of the following: + + links: a links object containing at least one of the following: + self: a link for the relationship itself (a “relationship link”). This link allows the client to directly manipulate the relationship. For example, removing an author through an article’s relationship URL would disconnect the person from the article without deleting the people resource itself. When fetched successfully, this link returns the linkage for the related resources as its primary data. (See Fetching Relationships.) + related: a related resource link + data: resource linkage + meta: a meta object that contains non-standard meta-information about the relationship. + +A relationship object that represents a to-many relationship MAY also contain pagination links under the links member, as described below. Any pagination links in a relationship object MUST paginate the relationship data, not the related resources. + + Note: See fields and member names for more restrictions on this container. + +Related Resource Links + +A “related resource link” provides access to resource objects linked in a relationship. When fetched, the related resource object(s) are returned as the response’s primary data. + +For example, an article’s comments relationship could specify a link that returns a collection of comment resource objects when retrieved through a GET request. + +If present, a related resource link MUST reference a valid URL, even if the relationship isn’t currently associated with any target resources. Additionally, a related resource link MUST NOT change because its relationship’s content changes. +Resource Linkage + +Resource linkage in a compound document allows a client to link together all of the included resource objects without having to GET any URLs via links. + +Resource linkage MUST be represented as one of the following: + + null for empty to-one relationships. + an empty array ([]) for empty to-many relationships. + a single resource identifier object for non-empty to-one relationships. + an array of resource identifier objects for non-empty to-many relationships. + + Note: The spec does not impart meaning to order of resource identifier objects in linkage arrays of to-many relationships, although implementations may do that. Arrays of resource identifier objects may represent ordered or unordered relationships, and both types can be mixed in one response object. + +For example, the following article is associated with an author: + +// ... +{ + "type": "articles", + "id": "1", + "attributes": { + "title": "Rails is Omakase" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/articles/1" + } +} +// ... + +The author relationship includes a link for the relationship itself (which allows the client to change the related author directly), a related resource link to fetch the resource objects, and linkage information. +Resource Links + +The optional links member within each resource object contains links related to the resource. + +If present, this links object MAY contain a self link that identifies the resource represented by the resource object. + +// ... +{ + "type": "articles", + "id": "1", + "attributes": { + "title": "Rails is Omakase" + }, + "links": { + "self": "http://example.com/articles/1" + } +} +// ... + +A server MUST respond to a GET request to the specified URL with a response that includes the resource as the primary data. +Resource Identifier Objects + +A “resource identifier object” is an object that identifies an individual resource. + +A “resource identifier object” MUST contain type and id members. + +A “resource identifier object” MAY also include a meta member, whose value is a meta object that contains non-standard meta-information. +Compound Documents + +To reduce the number of HTTP requests, servers MAY allow responses that include related resources along with the requested primary resources. Such responses are called “compound documents”. + +In a compound document, all included resources MUST be represented as an array of resource objects in a top-level included member. + +Compound documents require “full linkage”, meaning that every included resource MUST be identified by at least one resource identifier object in the same document. These resource identifier objects could either be primary data or represent resource linkage contained within primary or included resources. + +The only exception to the full linkage requirement is when relationship fields that would otherwise contain linkage data are excluded via sparse fieldsets. + + Note: Full linkage ensures that included resources are related to either the primary data (which could be resource objects or resource identifier objects) or to each other. + +A complete example document with multiple included relationships: + +{ + "data": [{ + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" + }, + "links": { + "self": "http://example.com/articles/1" + }, + "relationships": { + "author": { + "links": { + "self": "http://example.com/articles/1/relationships/author", + "related": "http://example.com/articles/1/author" + }, + "data": { "type": "people", "id": "9" } + }, + "comments": { + "links": { + "self": "http://example.com/articles/1/relationships/comments", + "related": "http://example.com/articles/1/comments" + }, + "data": [ + { "type": "comments", "id": "5" }, + { "type": "comments", "id": "12" } + ] + } + } + }], + "included": [{ + "type": "people", + "id": "9", + "attributes": { + "firstName": "Dan", + "lastName": "Gebhardt", + "twitter": "dgeb" + }, + "links": { + "self": "http://example.com/people/9" + } + }, { + "type": "comments", + "id": "5", + "attributes": { + "body": "First!" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "2" } + } + }, + "links": { + "self": "http://example.com/comments/5" + } + }, { + "type": "comments", + "id": "12", + "attributes": { + "body": "I like XML better" + }, + "relationships": { + "author": { + "data": { "type": "people", "id": "9" } + } + }, + "links": { + "self": "http://example.com/comments/12" + } + }] +} + +A compound document MUST NOT include more than one resource object for each type and id pair. + + Note: In a single document, you can think of the type and id as a composite key that uniquely references resource objects in another part of the document. + + Note: This approach ensures that a single canonical resource object is returned with each response, even when the same resource is referenced multiple times. + +Meta Information + +Where specified, a meta member can be used to include non-standard meta-information. The value of each meta member MUST be an object (a “meta object”). + +Any members MAY be specified within meta objects. + +For example: + +{ + "meta": { + "copyright": "Copyright 2015 Example Corp.", + "authors": [ + "Yehuda Katz", + "Steve Klabnik", + "Dan Gebhardt", + "Tyler Kellen" + ] + }, + "data": { + // ... + } +} + +Links + +Where specified, a links member can be used to represent links. The value of this member MUST be an object (a “links object”). + +Within this object, a link MUST be represented as either: + + a string containing the link’s URI. + an object (“link object”) which can contain the following members: + href: a string containing the link’s URI. + meta: a meta object containing non-standard meta-information about the link. + Any link-specific target attributes described below. + +Except for the profile key in the top-level links object and the type key in an error object’s links object, each key present in a links object MUST have a single link as its value. The aforementioned profile and type keys, if present, MUST hold an array of links. + +In the example below, the self link is simply a URI string, whereas the related link uses the object form to provide meta information about a related resource collection: + +"links": { + "self": "http://example.com/articles/1", + "related": { + "href": "http://example.com/articles/1/comments", + "meta": { + "count": 10 + } + } +} + +Profile Links + +Like all links, a link in an array of profile links can be represented with a link object. In that case, the link object MAY contain an aliases member listing any profile aliases. + +Here, the profile key specifies an array of profile links, including one that includes a profile alias: + +"links": { + "profile": [ + "http://example.com/profiles/flexible-pagination", + { + "href": "http://example.com/profiles/resource-versioning", + "aliases": { + "version": "v" + } + } + ] +} + + Note: Additional link types, similar to profile links, may be specified in the future. + +JSON:API Object + +A JSON:API document MAY include information about its implementation under a top level jsonapi member. If present, the value of the jsonapi member MUST be an object (a “jsonapi object”). + +The jsonapi object MAY contain any of the following members: + + version - whose value is a string indicating the highest JSON:API version supported. + meta - a meta object that contains non-standard meta-information. + +A simple example appears below: + +{ + "jsonapi": { + "version": "1.1" + } +} + +If the version member is not present, clients should assume the server implements at least version 1.0 of the specification. + + Note: Because JSON:API is committed to making additive changes only, the version string primarily indicates which new features a server may support. + +Member Names + +All member names used in a JSON:API document MUST be treated as case sensitive by clients and servers, and they MUST meet all of the following conditions: + + Member names MUST contain at least one character. + Member names MUST contain only the allowed characters listed below. + Member names MUST start and end with a “globally allowed character”, as defined below. + +To enable an easy mapping of member names to URLs, it is RECOMMENDED that member names use only non-reserved, URL safe characters specified in RFC 3986. +Allowed Characters + +The following “globally allowed characters” MAY be used anywhere in a member name: + + U+0061 to U+007A, “a-z” + U+0041 to U+005A, “A-Z” + U+0030 to U+0039, “0-9” + U+0080 and above (non-ASCII Unicode characters; not recommended, not URL safe) + +Additionally, the following characters are allowed in member names, except as the first or last character: + + U+002D HYPHEN-MINUS, “-“ + U+005F LOW LINE, “_” + U+0020 SPACE, “ “ (not recommended, not URL safe) + +Reserved Characters + +The following characters MUST NOT be used in member names: + + U+002B PLUS SIGN, “+” (has overloaded meaning in URL query strings) + U+002C COMMA, “,” (used as a separator between relationship paths) + U+002E PERIOD, “.” (used as a separator within relationship paths) + U+005B LEFT SQUARE BRACKET, “[” (used in sparse fieldsets) + U+005D RIGHT SQUARE BRACKET, “]” (used in sparse fieldsets) + U+0021 EXCLAMATION MARK, “!” + U+0022 QUOTATION MARK, ‘”’ + U+0023 NUMBER SIGN, “#” + U+0024 DOLLAR SIGN, “$” + U+0025 PERCENT SIGN, “%” + U+0026 AMPERSAND, “&” + U+0027 APOSTROPHE, “’” + U+0028 LEFT PARENTHESIS, “(“ + U+0029 RIGHT PARENTHESIS, “)” + U+002A ASTERISK, “*” + U+002F SOLIDUS, “/” + U+003A COLON, “:” + U+003B SEMICOLON, “;” + U+003C LESS-THAN SIGN, “<” + U+003D EQUALS SIGN, “=” + U+003E GREATER-THAN SIGN, “>” + U+003F QUESTION MARK, “?” + U+0040 COMMERCIAL AT, “@” + U+005C REVERSE SOLIDUS, “\” + U+005E CIRCUMFLEX ACCENT, “^” + U+0060 GRAVE ACCENT, “`” + U+007B LEFT CURLY BRACKET, “{“ + U+007C VERTICAL LINE, “|” + U+007D RIGHT CURLY BRACKET, “}” + U+007E TILDE, “~” + U+007F DELETE + U+0000 to U+001F (C0 Controls) + +@-Members + +Member names MAY also begin with an at sign (U+0040 COMMERCIAL AT, “@”). Members named this way are called “@-Members”. @-Members MAY appear anywhere in a JSON:API document. + +However, JSON:API processors MUST completely ignore @-Members (i.e. not treat them as JSON:API data). + +Moreover, the existence of @-Members MUST be ignored when interpreting all JSON:API definitions and processing instructions given outside of this subsection. For example, an attribute is defined above as any member of the attributes object. However, because @-Members must be ignored when interpreting that definition, an @-Member that occurs in an attributes object is not an attribute. + + Note: Among other things, “@” members can be used to add JSON-LD data to a JSON:API document. Such documents should be served with an extra header to convey to JSON-LD clients that they contain JSON-LD data. + +Fetching Data + +Data, including resources and relationships, can be fetched by sending a GET request to an endpoint. + +Responses can be further refined with the optional features described below. +Fetching Resources + +A server MUST support fetching resource data for every URL provided as: + + a self link as part of the top-level links object + a self link as part of a resource-level links object + a related link as part of a relationship-level links object + +For example, the following request fetches a collection of articles: + +GET /articles HTTP/1.1 +Accept: application/vnd.api+json + +The following request fetches an article: + +GET /articles/1 HTTP/1.1 +Accept: application/vnd.api+json + +And the following request fetches an article’s author: + +GET /articles/1/author HTTP/1.1 +Accept: application/vnd.api+json + +Responses +200 OK + +A server MUST respond to a successful request to fetch an individual resource or resource collection with a 200 OK response. + +A server MUST respond to a successful request to fetch a resource collection with an array of resource objects or an empty array ([]) as the response document’s primary data. + +For example, a GET request to a collection of articles could return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "http://example.com/articles" + }, + "data": [{ + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" + } + }, { + "type": "articles", + "id": "2", + "attributes": { + "title": "Rails is Omakase" + } + }] +} + +A similar response representing an empty collection would be: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "http://example.com/articles" + }, + "data": [] +} + +A server MUST respond to a successful request to fetch an individual resource with a resource object or null provided as the response document’s primary data. + +null is only an appropriate response when the requested URL is one that might correspond to a single resource, but doesn’t currently. + + Note: Consider, for example, a request to fetch a to-one related resource link. This request would respond with null when the relationship is empty (such that the link is corresponding to no resources) but with the single related resource’s resource object otherwise. + +For example, a GET request to an individual article could return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "http://example.com/articles/1" + }, + "data": { + "type": "articles", + "id": "1", + "attributes": { + "title": "JSON:API paints my bikeshed!" + }, + "relationships": { + "author": { + "links": { + "related": "http://example.com/articles/1/author" + } + } + } + } +} + +If the above article’s author is missing, then a GET request to that related resource would return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "http://example.com/articles/1/author" + }, + "data": null +} + +404 Not Found + +A server MUST respond with 404 Not Found when processing a request to fetch a single resource that does not exist, except when the request warrants a 200 OK response with null as the primary data (as described above). +Other Responses + +A server MAY respond with other HTTP status codes. + +A server MAY include error details with error responses. + +A server MUST prepare responses, and a client MUST interpret responses, in accordance with HTTP semantics. +Fetching Relationships + +A server MUST support fetching relationship data for every relationship URL provided as a self link as part of a relationship’s links object. + +For example, the following request fetches data about an article’s comments: + +GET /articles/1/relationships/comments HTTP/1.1 +Accept: application/vnd.api+json + +And the following request fetches data about an article’s author: + +GET /articles/1/relationships/author HTTP/1.1 +Accept: application/vnd.api+json + +Responses +200 OK + +A server MUST respond to a successful request to fetch a relationship with a 200 OK response. + +The primary data in the response document MUST match the appropriate value for resource linkage, as described above for relationship objects. + +The top-level links object MAY contain self and related links, as described above for relationship objects. + +For example, a GET request to a URL from a to-one relationship link could return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "/articles/1/relationships/author", + "related": "/articles/1/author" + }, + "data": { + "type": "people", + "id": "12" + } +} + +If the above relationship is empty, then a GET request to the same URL would return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "/articles/1/relationships/author", + "related": "/articles/1/author" + }, + "data": null +} + +A GET request to a URL from a to-many relationship link could return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "/articles/1/relationships/tags", + "related": "/articles/1/tags" + }, + "data": [ + { "type": "tags", "id": "2" }, + { "type": "tags", "id": "3" } + ] +} + +If the above relationship is empty, then a GET request to the same URL would return: + +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "links": { + "self": "/articles/1/relationships/tags", + "related": "/articles/1/tags" + }, + "data": [] +} + +404 Not Found + +A server MUST return 404 Not Found when processing a request to fetch a relationship link URL that does not exist. + + Note: This can happen when the parent resource of the relationship does not exist. For example, when /articles/1 does not exist, request to /articles/1/relationships/tags returns 404 Not Found. + +If a relationship link URL exists but the relationship is empty, then 200 OK MUST be returned, as described above. +Other Responses + +A server MAY respond with other HTTP status codes. + +A server MAY include error details with error responses. + +A server MUST prepare responses, and a client MUST interpret responses, in accordance with HTTP semantics. +Inclusion of Related Resources + +An endpoint MAY return resources related to the primary data by default. + +An endpoint MAY also support an include query parameter to allow the client to customize which related resources should be returned. + +If an endpoint does not support the include parameter, it MUST respond with 400 Bad Request to any requests that include it. + +If an endpoint supports the include parameter and a client supplies it: + + The server’s response MUST be a compound document with an included key — even if that included key holds an empty array (because the requested relationships are empty). + The server MUST NOT include unrequested resource objects in the included section of the compound document. + +The value of the include parameter MUST be a comma-separated (U+002C COMMA, “,”) list of relationship paths. A relationship path is a dot-separated (U+002E FULL-STOP, “.”) list of relationship names. + +If a server is unable to identify a relationship path or does not support inclusion of resources from a path, it MUST respond with 400 Bad Request. + + Note: For example, a relationship path could be comments.author, where comments is a relationship listed under a articles resource object, and author is a relationship listed under a comments resource object. + +For instance, comments could be requested with an article: + +GET /articles/1?include=comments HTTP/1.1 +Accept: application/vnd.api+json + +In order to request resources related to other resources, a dot-separated path for each relationship name can be specified: + +GET /articles/1?include=comments.author HTTP/1.1 +Accept: application/vnd.api+json + + Note: Because compound documents require full linkage (except when relationship linkage is excluded by sparse fieldsets), intermediate resources in a multi-part path must be returned along with the leaf nodes. For example, a response to a request for comments.author should include comments as well as the author of each of those comments. + + Note: A server may choose to expose a deeply nested relationship such as comments.author as a direct relationship with an alternative name such as commentAuthors. This would allow a client to request /articles/1?include=commentAuthors instead of /articles/1?include=comments.author. By exposing the nested relationship with an alternative name, the server can still provide full linkage in compound documents without including potentially unwanted intermediate resources. + +Multiple related resources can be requested in a comma-separated list: + +GET /articles/1?include=author,comments.author HTTP/1.1 +Accept: application/vnd.api+json + +Furthermore, related resources can be requested from a relationship endpoint: + +GET /articles/1/relationships/comments?include=comments.author HTTP/1.1 +Accept: application/vnd.api+json + +In this case, the primary data would be a collection of resource identifier objects that represent linkage to comments for an article, while the full comments and comment authors would be returned as included data. + + Note: This section applies to any endpoint that responds with primary data, regardless of the request type. For instance, a server could support the inclusion of related resources along with a POST request to create a resource or relationship. + +Sparse Fieldsets + +A client MAY request that an endpoint return only specific fields in the response on a per-type basis by including a fields[TYPE] query parameter. + +The value of any fields[TYPE] parameter MUST be a comma-separated (U+002C COMMA, “,”) list that refers to the name(s) of the fields to be returned. + +If a client requests a restricted set of fields for a given resource type, an endpoint MUST NOT include additional fields in resource objects of that type in its response. + +GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1 +Accept: application/vnd.api+json + + Note: The above example URI shows unencoded [ and ] characters simply for readability. In practice, these characters should be percent-encoded. See “Square Brackets in Parameter Names”. + + Note: This section applies to any endpoint that responds with resources as primary or included data, regardless of the request type. For instance, a server could support sparse fieldsets along with a POST request to create a resource. + +Sorting + +A server MAY choose to support requests to sort resource collections according to one or more criteria (“sort fields”). + + Note: Although recommended, sort fields do not necessarily need to correspond to resource attribute and relationship names. + + Note: It is recommended that dot-separated (U+002E FULL-STOP, “.”) sort fields be used to request sorting based upon relationship attributes. For example, a sort field of author.name could be used to request that the primary data be sorted based upon the name attribute of the author relationship. + +An endpoint MAY support requests to sort the primary data with a sort query parameter. The value for sort MUST represent sort fields. + +GET /people?sort=age HTTP/1.1 +Accept: application/vnd.api+json + +An endpoint MAY support multiple sort fields by allowing comma-separated (U+002C COMMA, “,”) sort fields. Sort fields SHOULD be applied in the order specified. + +GET /people?sort=age,name HTTP/1.1 +Accept: application/vnd.api+json + +The sort order for each sort field MUST be ascending unless it is prefixed with a minus (U+002D HYPHEN-MINUS, “-“), in which case it MUST be descending. + +GET /articles?sort=-created,title HTTP/1.1 +Accept: application/vnd.api+json + +The above example should return the newest articles first. Any articles created on the same date will then be sorted by their title in ascending alphabetical order. + +If the server does not support sorting as specified in the query parameter sort, it MUST return 400 Bad Request. + +If sorting is supported by the server and requested by the client via query parameter sort, the server MUST return elements of the top-level data array of the response ordered according to the criteria specified. The server MAY apply default sorting rules to top-level data if request parameter sort is not specified. + + Note: This section applies to any endpoint that responds with a resource collection as primary data, regardless of the request type. + +Pagination + +A server MAY choose to limit the number of resources returned in a response to a subset (“page”) of the whole set available. + +A server MAY provide links to traverse a paginated data set (“pagination links”). + +Pagination links MUST appear in the links object that corresponds to a collection. To paginate the primary data, supply pagination links in the top-level links object. To paginate an included collection returned in a compound document, supply pagination links in the corresponding links object. + +The following keys MUST be used for pagination links: + + first: the first page of data + last: the last page of data + prev: the previous page of data + next: the next page of data + +Keys MUST either be omitted or have a null value to indicate that a particular link is unavailable. + +Concepts of order, as expressed in the naming of pagination links, MUST remain consistent with JSON:API’s sorting rules. + +The page query parameter family is reserved for pagination. Servers and clients SHOULD use these parameters for pagination operations. + + Note: JSON API is agnostic about the pagination strategy used by a server, but the page query parameter family can be used regardless of the strategy employed. For example, a page-based strategy might use query parameters such as page[number] and page[size], while a cursor-based strategy might use page[cursor]. + + Note: This section applies to any endpoint that responds with a resource collection as primary data, regardless of the request type. + +Filtering + +The filter query parameter family is reserved for filtering data. Servers and clients SHOULD use these parameters for filtering operations. + + Note: JSON API is agnostic about the strategies supported by a server. + +Creating, Updating and Deleting Resources + +A server MAY allow resources of a given type to be created. It MAY also allow existing resources to be modified or deleted. + +A request MUST completely succeed or fail (in a single “transaction”). No partial updates are allowed. + + Note: The type member is required in every resource object throughout requests and responses in JSON:API. There are some cases, such as when POSTing to an endpoint representing heterogenous data, when the type could not be inferred from the endpoint. However, picking and choosing when it is required would be confusing; it would be hard to remember when it was required and when it was not. Therefore, to improve consistency and minimize confusion, type is always required. + +Creating Resources + +A resource can be created by sending a POST request to a URL that represents a collection of resources. The request MUST include a single resource object as primary data. The resource object MUST contain at least a type member. + +For instance, a new photo might be created with the following request: + +POST /photos HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { + "type": "photos", + "attributes": { + "title": "Ember Hamster", + "src": "http://example.com/images/productivity.png" + }, + "relationships": { + "photographer": { + "data": { "type": "people", "id": "9" } + } + } + } +} + +If a relationship is provided in the relationships member of the resource object, its value MUST be a relationship object with a data member. The value of this key represents the linkage the new resource is to have. +Client-Generated IDs + +A server MAY accept a client-generated ID along with a request to create a resource. An ID MUST be specified with an id key, the value of which MUST be a universally unique identifier. The client SHOULD use a properly generated and formatted UUID as described in RFC 4122 [RFC4122]. + + NOTE: In some use-cases, such as importing data from another source, it may be possible to use something other than a UUID that is still guaranteed to be globally unique. Do not use anything other than a UUID unless you are 100% confident that the strategy you are using indeed generates globally unique identifiers. + +For example: + +POST /photos HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { + "type": "photos", + "id": "550e8400-e29b-41d4-a716-446655440000", + "attributes": { + "title": "Ember Hamster", + "src": "http://example.com/images/productivity.png" + } + } +} + +A server MUST return 403 Forbidden in response to an unsupported request to create a resource with a client-generated ID. +Responses +201 Created + +If a POST request did not include a Client-Generated ID and the requested resource has been created successfully, the server MUST return a 201 Created status code. + +The response SHOULD include a Location header identifying the location of the newly created resource, in order to comply with RFC 7231. + +The response MUST also include a document that contains the primary resource created. + +If the resource object returned by the response contains a self key in its links member and a Location header is provided, the value of the self member MUST match the value of the Location header. + +HTTP/1.1 201 Created +Location: http://example.com/photos/550e8400-e29b-41d4-a716-446655440000 +Content-Type: application/vnd.api+json + +{ + "data": { + "type": "photos", + "id": "550e8400-e29b-41d4-a716-446655440000", + "attributes": { + "title": "Ember Hamster", + "src": "http://example.com/images/productivity.png" + }, + "links": { + "self": "http://example.com/photos/550e8400-e29b-41d4-a716-446655440000" + } + } +} + +202 Accepted + +If a request to create a resource has been accepted for processing, but the processing has not been completed by the time the server responds, the server MUST return a 202 Accepted status code. +204 No Content + +If a POST request did include a Client-Generated ID and the requested resource has been created successfully, the server MUST return either a 201 Created status code and response document (as described above) or a 204 No Content status code with no response document. + + Note: If a 204 response is received the client should consider the resource object sent in the request to be accepted by the server, as if the server had returned it back in a 201 response. + +403 Forbidden + +A server MAY return 403 Forbidden in response to an unsupported request to create a resource. +404 Not Found + +A server MUST return 404 Not Found when processing a request that references a related resource that does not exist. +409 Conflict + +A server MUST return 409 Conflict when processing a POST request to create a resource with a client-generated ID that already exists. + +A server MUST return 409 Conflict when processing a POST request in which the resource object’s type is not among the type(s) that constitute the collection represented by the endpoint. + +A server SHOULD include error details and provide enough information to recognize the source of the conflict. +Other Responses + +A server MAY respond with other HTTP status codes. + +A server MAY include error details with error responses. + +A server MUST prepare responses, and a client MUST interpret responses, in accordance with HTTP semantics. +Updating Resources + +A resource can be updated by sending a PATCH request to the URL that represents the resource. + +The URL for a resource can be obtained in the self link of the resource object. Alternatively, when a GET request returns a single resource object as primary data, the same request URL can be used for updates. + +The PATCH request MUST include a single resource object as primary data. The resource object MUST contain type and id members. + +For example: + +PATCH /articles/1 HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { + "type": "articles", + "id": "1", + "attributes": { + "title": "To TDD or Not" + } + } +} + +Updating a Resource’s Attributes + +Any or all of a resource’s attributes MAY be included in the resource object included in a PATCH request. + +If a request does not include all of the attributes for a resource, the server MUST interpret the missing attributes as if they were included with their current values. The server MUST NOT interpret missing attributes as null values. + +For example, the following PATCH request is interpreted as a request to update only the title and text attributes of an article: + +PATCH /articles/1 HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { + "type": "articles", + "id": "1", + "attributes": { + "title": "To TDD or Not", + "text": "TLDR; It's complicated... but check your test coverage regardless." + } + } +} + +Updating a Resource’s Relationships + +Any or all of a resource’s relationships MAY be included in the resource object included in a PATCH request. + +If a request does not include all of the relationships for a resource, the server MUST interpret the missing relationships as if they were included with their current values. It MUST NOT interpret them as null or empty values. + +If a relationship is provided in the relationships member of a resource object in a PATCH request, its value MUST be a relationship object with a data member. The relationship’s value will be replaced with the value specified in this member. + +For instance, the following PATCH request will update the author relationship of an article: + +PATCH /articles/1 HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { + "type": "articles", + "id": "1", + "relationships": { + "author": { + "data": { "type": "people", "id": "1" } + } + } + } +} + +Likewise, the following PATCH request performs a complete replacement of the tags for an article: + +PATCH /articles/1 HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { + "type": "articles", + "id": "1", + "relationships": { + "tags": { + "data": [ + { "type": "tags", "id": "2" }, + { "type": "tags", "id": "3" } + ] + } + } + } +} + +A server MAY reject an attempt to do a full replacement of a to-many relationship. In such a case, the server MUST reject the entire update, and return a 403 Forbidden response. + + Note: Since full replacement may be a very dangerous operation, a server may choose to disallow it. For example, a server may reject full replacement if it has not provided the client with the full list of associated objects, and does not want to allow deletion of records the client has not seen. + +Responses +202 Accepted + +If an update request has been accepted for processing, but the processing has not been completed by the time the server responds, the server MUST return a 202 Accepted status code. +200 OK + +If a server accepts an update but also changes the resource(s) in ways other than those specified by the request (for example, updating the updated-at attribute or a computed sha), it MUST return a 200 OK response. The response document MUST include a representation of the updated resource(s) as if a GET request was made to the request URL. + +A server MUST return a 200 OK status code if an update is successful, the client’s current attributes remain up to date, and the server responds only with top-level meta data. In this case the server MUST NOT include a representation of the updated resource(s). +204 No Content + +If an update is successful and the server doesn’t update any attributes besides those provided, the server MUST return either a 200 OK status code and response document (as described above) or a 204 No Content status code with no response document. +403 Forbidden + +A server MUST return 403 Forbidden in response to an unsupported request to update a resource or relationship. +404 Not Found + +A server MUST return 404 Not Found when processing a request to modify a resource that does not exist. + +A server MUST return 404 Not Found when processing a request that references a related resource that does not exist. +409 Conflict + +A server MAY return 409 Conflict when processing a PATCH request to update a resource if that update would violate other server-enforced constraints (such as a uniqueness constraint on a property other than id). + +A server MUST return 409 Conflict when processing a PATCH request in which the resource object’s type and id do not match the server’s endpoint. + +A server SHOULD include error details and provide enough information to recognize the source of the conflict. +Other Responses + +A server MAY respond with other HTTP status codes. + +A server MAY include error details with error responses. + +A server MUST prepare responses, and a client MUST interpret responses, in accordance with HTTP semantics. +Updating Relationships + +Although relationships can be modified along with resources (as described above), JSON:API also supports updating of relationships independently at URLs from relationship links. + + Note: Relationships are updated without exposing the underlying server semantics, such as foreign keys. Furthermore, relationships can be updated without necessarily affecting the related resources. For example, if an article has many authors, it is possible to remove one of the authors from the article without deleting the person itself. Similarly, if an article has many tags, it is possible to add or remove tags. Under the hood on the server, the first of these examples might be implemented with a foreign key, while the second could be implemented with a join table, but the JSON:API protocol would be the same in both cases. + + Note: A server may choose to delete the underlying resource if a relationship is deleted (as a garbage collection measure). + +Updating To-One Relationships + +A server MUST respond to PATCH requests to a URL from a to-one relationship link as described below. + +The PATCH request MUST include a top-level member named data containing one of: + + a resource identifier object corresponding to the new related resource. + null, to remove the relationship. + +For example, the following request updates the author of an article: + +PATCH /articles/1/relationships/author HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": { "type": "people", "id": "12" } +} + +And the following request clears the author of the same article: + +PATCH /articles/1/relationships/author HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": null +} + +If the relationship is updated successfully then the server MUST return a successful response. +Updating To-Many Relationships + +A server MUST respond to PATCH, POST, and DELETE requests to a URL from a to-many relationship link as described below. + +For all request types, the body MUST contain a data member whose value is an empty array or an array of resource identifier objects. + +If a client makes a PATCH request to a URL from a to-many relationship link, the server MUST either completely replace every member of the relationship, return an appropriate error response if some resources can not be found or accessed, or return a 403 Forbidden response if complete replacement is not allowed by the server. + +For example, the following request replaces every tag for an article: + +PATCH /articles/1/relationships/tags HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": [ + { "type": "tags", "id": "2" }, + { "type": "tags", "id": "3" } + ] +} + +And the following request clears every tag for an article: + +PATCH /articles/1/relationships/tags HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": [] +} + +If a client makes a POST request to a URL from a relationship link, the server MUST add the specified members to the relationship unless they are already present. If a given type and id is already in the relationship, the server MUST NOT add it again. + + Note: This matches the semantics of databases that use foreign keys for has-many relationships. Document-based storage should check the has-many relationship before appending to avoid duplicates. + +If all of the specified resources can be added to, or are already present in, the relationship then the server MUST return a successful response. + + Note: This approach ensures that a request is successful if the server’s state matches the requested state, and helps avoid pointless race conditions caused by multiple clients making the same changes to a relationship. + +In the following example, the comment with ID 123 is added to the list of comments for the article with ID 1: + +POST /articles/1/relationships/comments HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": [ + { "type": "comments", "id": "123" } + ] +} + +If the client makes a DELETE request to a URL from a relationship link the server MUST delete the specified members from the relationship or return a 403 Forbidden response. If all of the specified resources are able to be removed from, or are already missing from, the relationship then the server MUST return a successful response. + + Note: As described above for POST requests, this approach helps avoid pointless race conditions between multiple clients making the same changes. + +Relationship members are specified in the same way as in the POST request. + +In the following example, comments with IDs of 12 and 13 are removed from the list of comments for the article with ID 1: + +DELETE /articles/1/relationships/comments HTTP/1.1 +Content-Type: application/vnd.api+json +Accept: application/vnd.api+json + +{ + "data": [ + { "type": "comments", "id": "12" }, + { "type": "comments", "id": "13" } + ] +} + + Note: RFC 7231 specifies that a DELETE request may include a body, but that a server may reject the request. This spec defines the semantics of a server, and we are defining its semantics for JSON:API. + +Responses +202 Accepted + +If a relationship update request has been accepted for processing, but the processing has not been completed by the time the server responds, the server MUST return a 202 Accepted status code. +204 No Content + +A server MUST return a 204 No Content status code if an update is successful and the representation of the resource in the request matches the result. + + Note: This is the appropriate response to a POST request sent to a URL from a to-many relationship link when that relationship already exists. It is also the appropriate response to a DELETE request sent to a URL from a to-many relationship link when that relationship does not exist. + +200 OK + +If a server accepts an update but also changes the targeted relationship(s) in other ways than those specified by the request, it MUST return a 200 OK response. The response document MUST include a representation of the updated relationship(s). + +A server MUST return a 200 OK status code if an update is successful, the client’s current data remain up to date, and the server responds only with top-level meta data. In this case the server MUST NOT include a representation of the updated relationship(s). +403 Forbidden + +A server MUST return 403 Forbidden in response to an unsupported request to update a relationship. +Other Responses + +A server MAY respond with other HTTP status codes. + +A server MAY include error details with error responses. + +A server MUST prepare responses, and a client MUST interpret responses, in accordance with HTTP semantics. +Deleting Resources + +An individual resource can be deleted by making a DELETE request to the resource’s URL: + +DELETE /photos/1 HTTP/1.1 +Accept: application/vnd.api+json + +Responses +202 Accepted + +If a deletion request has been accepted for processing, but the processing has not been completed by the time the server responds, the server MUST return a 202 Accepted status code. +204 No Content + +A server MUST return a 204 No Content status code if a deletion request is successful and no content is returned. +200 OK + +A server MUST return a 200 OK status code if a deletion request is successful and the server responds with only top-level meta data. +404 NOT FOUND + +A server SHOULD return a 404 Not Found status code if a deletion request fails due to the resource not existing. +Other Responses + +A server MAY respond with other HTTP status codes. + +A server MAY include error details with error responses. + +A server MUST prepare responses, and a client MUST interpret responses, in accordance with HTTP semantics. +Query Parameters +Query Parameter Families + +Although “query parameter” is a common term in everyday web development, it is not a well-standardized concept. Therefore, JSON:API provides its own definition of a query parameter. + +For the most part, JSON:API’s definition coincides with colloquial usage, and its details can be safely ignored. However, one important consequence of this definition is that a URL like the following is considered to have two distinct query parameters: + +/?page[offset]=0&page[limit]=10 + +The two parameters are named page[offset] and page[limit]; there is no single page parameter. + +In practice, however, parameters like page[offset] and page[limit] are usually defined and processed together, and it’s convenient to refer to them collectively. Therefore, JSON:API introduces the concept of a query parameter family. + +A “query parameter family” is the set of all query parameters whose name starts with a “base name”, followed by zero or more instances of empty square brackets (i.e. []) or square-bracketed legal member names. The family is referred to by its base name. + +For example, the filter query parameter family includes parameters named: filter, filter[x], filter[], filter[x][], filter[][], filter[x][y], etc. However, filter[_] is not a valid parameter name in the family, because _ is not a valid member name. +Implementation-Specific Query Parameters + +Implementations MAY support custom query parameters. However, the names of these query parameters MUST come from a family whose base name is a legal member name and also contains at least one non a-z character (i.e., outside U+0061 to U+007A). + +It is RECOMMENDED that a capital letter (e.g. camelCasing) be used to satisfy the above requirement. + +If a server encounters a query parameter that does not follow the naming conventions above, and the server does not know how to process it as a query parameter from this specification, it MUST return 400 Bad Request. + + Note: By forbidding the use of query parameters that contain only the characters [a-z], JSON:API is reserving the ability to standardize additional query parameters later without conflicting with existing implementations. + +Profiles + +JSON:API supports the use of “profiles” as a way to indicate additional semantics that apply to a JSON:API request/document, without altering the basic semantics described in this specification. + +A profile is a separate specification defining these additional semantics. + +RFC 6906 covers the nature of profile identification: + + Profiles are identified by URI… The presence of a specific URI has to be sufficient for a client to assert that a resource representation conforms to a profile [regardless of any content that may or may not be available at that URI]. + +However, to aid human understanding, visiting a profile’s URI SHOULD return documentation of the profile. + +The following example profile reserves a timestamps member in the meta object of every resource object: + +# Timestamps profile + +## Introduction + +This page specifies a profile for the `application/vnd.api+json` media type, +as described in the [JSON:API specification](http://jsonapi.org/format/). + +This profile allows every resource in a JSON:API document to represent +significant timestamps in a consistent way. + +## Document Structure + +Every resource object **MAY** include a `timestamps` member in its associated +`meta` object. If this member is present, its value **MUST** be an object that +**MAY** contain any of the following members: + +* `created` +* `updated` + +The value of each member **MUST** comply with the variant of ISO 8601 used by +JavaScript's `JSON.stringify` method to format Javascript `Date` objects. + +## Keywords + +This profile defines the following keywords: + +* `timestamps` + +profile Media Type Parameter + +The profile media type parameter is used to describe the application of one or more profiles to a JSON:API document. The value of the profile parameter MUST equal a space-separated (U+0020 SPACE, “ “) list of profile URIs. + + Note: When serializing the profile media type parameter, the HTTP specification requires that its value be surrounded by quotation marks (U+0022 QUOTATION MARK, “"”) if it contains more than one URI. + +A client MAY use the profile media type parameter in conjunction with the JSON:API media type in an Accept header to request, but not require, that the server apply one or more profiles to the response document. When such a request is received, a server SHOULD attempt to apply the requested profiles to its response. + +For example, in the following request, the client asks that the server apply the http://example.com/last-modified profile if it is able to. + +Accept: application/vnd.api+json;profile="http://example.com/last-modified", application/vnd.api+json + + Note: The second instance of the JSON:API media type in the example above is required under the client’s content negotiation responsibilities. It is used to support old servers that don’t understand the profile parameter. + +Servers MAY add profiles to a JSON:API document even if the client has not requested them. The recipient of a document MUST ignore any profiles in that document that it does not understand. The only exception to this is profiles whose support is required using the profile query parameter, as described later. +Sending Profiled Documents + +Clients and servers MUST include the profile media type parameter in conjunction with the JSON:API media type in a Content-Type header to indicate that they have applied one or more profiles to a JSON:API document. + +Likewise, clients and servers applying profiles to a JSON:API document MUST include a top-level links object with a profile key, and that profile key MUST include a link to the URI of each profile that has been applied. + +When an older JSON:API server that doesn’t support the profile media type parameter receives a document with one or more profiles, it will respond with a 415 Unsupported Media Type error. + +After attempting to rule out other possible causes of this error, a client that receives a 415 Unsupported Media Type SHOULD remove the profiles it has applied to the document and retry its request without the profile media type parameter. If this resolves the error, the client SHOULD NOT attempt to apply profiles in subsequent interactions with the same API. + + The most likely other causes of a 415 error are that the server doesn’t support JSON:API at all or that the client has failed to provide a required profile. + +profile Query Parameter + +A client MAY use the profile query parameter to require the server to apply one or more profiles when processing the request. The value of the profile query parameter MUST equal a URI-encoded whitespace-separated list of profile URIs. + +If a server receives a request requiring the application of a profile or combination of profiles that it can not apply, it MUST respond with a 400 Bad Request status code. The response MUST contain an error object that identifies the profile query parameter as the source and has the following URI as (one of) its types: + +https://jsonapi.org/errors/profile-not-supported + + Note: When a client lists a profile in the Accept header, it’s asking the server to compute its response as normal, but then send the response document with some extra information, as described in the requested profile. By contrast, when a client lists a profile in the profile query parameter, it’s asking the server to process the incoming request according to the rules of the profile. This can fundamentally change the meaning of the server’s response. + +Omitting the profile Query Parameter + +Requiring the client to specify the profile query parameter would be cumbersome. Accordingly, JSON:API defines a way that server’s may infer its value in many cases. + +To do so, a server MAY define an internal mapping from query parameter names to profile URIs. The profile URI for a query parameter name in this mapping MUST NOT change over time. + + Note: the server may choose to map all query parameter names from the same family to one profile URI. Or, it may choose to map only specific query parameter names. + +If a requested URL does not contain the profile query parameter and does contain one or more query parameters in the server’s internal mapping, the server may act as though the request URL contained a profile query parameter whose value was the URI-encoded space-separated list of each unique profile URI found in the server’s internal mapping for the query parameters in use on the request. + +For example, the server might support a profile that defines a meaning for the values of the page[cursor] query parameter. Then, it could define its internal param name to profile URI mapping like so: + +{ "page[cursor]": "https://example.com/pagination-profile" } + +Accordingly, a request for: + +https://example.com/?page[cursor]=xyz + +would be interpreted by the server as: + +https://example.com/?page[cursor]=xyz&profile=https://example.com/pagination-profile + +Profile Keywords and Aliases + +A profile SHOULD explicitly declare “keywords” for any elements that it introduces to the document structure. If a profile does not explicitly declare a keyword for an element, then the name of the element itself (i.e., its key in the document) is considered to be its keyword. All profile keywords MUST meet this specification’s requirements for member names. + +For the purposes of aliasing, a profile’s elements are defined shallowly. In other words, if a profile introduces an object-valued document member, that member is an element (and so subject to aliasing), but any keys in it are not themselves elements. Likewise, if the profile defines an array-valued element, the keys in nested objects within that array are not elements. + +The following example profile defines a single keyword, version: + +# Resource versioning profile + +## Introduction + +This page specifies a profile for the `application/vnd.api+json` media type, +as described in the [JSON:API specification](http://jsonapi.org/format/). + +This profile ensures that every resource represented in a JSON:API document +includes a version. + +## Document Structure + +Every resource **MUST** include a `meta` object containing a `version` member. +The value of this member **MUST** be a string that represents a unique version +for that resource. + +## Keywords + +This profile defines the following keywords: + +* `version` + +This profile might be applied as follows: + +{ + "data": { + "type": "contacts", + "id": "345", + "meta": { + "version": "2018-04-14-879976658" + }, + "attributes": { + "name": "Ethan" + } + }, + "links": { + "profile": ["http://example.com/profiles/resource-versioning"] + } +} + +Documents that apply a particular profile MAY represent each keyword with an alternatively named member, or “alias”. An alias fully assumes any meaning specified for a keyword, which no longer retains that meaning. Any aliases associated with a profile MUST be represented in the profile’s corresponding aliases object within its link object. The key of each alias MUST be a keyword from the profile, and the value MUST be an alias that applies to this particular representation. This aliasing mechanism allows profiles to be applied in a way that is both consistent with the rest of the representation and does not conflict with other profiles. + +For instance, the following document provides an alias for version: v. Interpreters of this representation should treat the key v as if it were the key version described in the profile: + +{ + "data": { + "type": "contacts", + "id": "345", + "meta": { + "v": "2018-04-14-879976658" + }, + "attributes": { + "name": "Ethan" + } + }, + "links": { + "profile": [{ + "href": "http://example.com/profiles/resource-versioning", + "aliases": { + "version": "v" + } + }] + } +} + +Processing Profiled Documents/Requests + +When a profile is applied to a request and/or document, the value used for each of the profile’s document members or query parameters is said to be “a recognized value” if that value, including all parts of it, has a legal, defined meaning according to the latest revision of the profile that the application is aware of. + + Note: The set of recognized values is also/more technically known as the defined text set. + +For example, the hypothetical timestamps profile specifies the timestamps element, and the meaning for two keys within it – created and updated. Therefore, in the following use of the profile, the value for the timestamps element would be a recognized value: + +{ + "type": "contacts", + "id": "345", + "meta": { + "timestamps": { "created": "2018-08-29T18:38:17.567Z" } + } + //... +} + +However, in the following case, the value for timestamps is not a recognized value because one of the keys in it, createdUnixEpoch, doesn’t have a meaning assigned to it in the timestamps profile: + +{ + "type": "contacts", + "id": "345", + "meta": { + "timestamps": { + "createdUnixEpoch": 1535567910201, + "created": "2018-08-29T18:38:17.567Z" + } + } + //... + } + +Likewise, if a profile defines an element and enumerates true and false as legal values with a specific meaning, then a string appearing as that element’s value would be an unrecognized value. + + Note: unrecognized values are not necessarily invalid or erroneous values. For example, the timestamps profile might be revised later to actually define a “createdUnixEpoch” key. This key would be unrecognized by all applications that existed at the time it was defined, but not by ones created/deployed later. + +Each profile MAY define its own rules for how applications should proceed when encountering unrecognized values. + +If a profile does not define its own rules for handling unrecognized values, the following rule applies by default: + + If the value of a profile-defined query parameter is unrecognized, the server MUST fail the request and respond with a 400 Bad Request and an error object indicating the problematic parameter. + + Otherwise, if the unrecognized value is a JSON object in the request/response document, and the only thing that makes it unrecognized is that it contains one or more keys that have no meaning assigned to them (in the latest revision of the profile that the application is aware of), then the application MUST simply ignore those unknown keys and continue processing the profile. + + In all other cases, the application MUST assume that the profile has been applied erroneously and MUST totally ignore the profile (i.e., process the request as if the profile were not there). + +In the case of our example timestamps profile, it does not define its own rules, so the above defaults would apply. + +Under the second of these default rules, the unrecognized value we saw above (with the createdUnixEpoch key) would be processed as though the createdUnixEpoch key simply weren’t present, and the application would still be able to use the data in the created key. + +However, if the user instead provided the following value, the whole timestamps profile would need to be ignored: + +{ + //... + "timestamps": { + "updated": "Wed Aug 29 2018 15:00:05 GMT-0400", + "created": "2018-08-29T18:38:17.567Z" + } +} + +Ignoring the profile in this case is required by the third default rule, because the value for the updated key is not recognized under the profile’s requirement that the updated key hold a string of the form produced by JSON.stringify. +Authoring Profiles + +A profile MAY assign meaning to elements of the document structure whose use is left up to each implementation, such as resource fields or members of meta objects. A profile MUST NOT define/assign a meaning to document members in areas of the document reserved for future use by the JSON:API specification. + +For example, it would be illegal for a profile to define a new key in a document’s top-level object, or in a links object, as JSON API implementations are not allowed to add custom keys in those areas. + +Likewise, a profile MAY assign a meaning to query parameters or parameter values whose details are left up to each implementation, such as filter and all parameters that contain a non a-z character. However, profiles MUST NOT assign a meaning to query parameters that are reserved. + +The meaning of an element or query parameter defined by a profile MUST NOT vary based on the presence or absence of other profiles. + +The scope of a profile MUST be clearly delineated. The elements and query parameters specified by a profile, and their meanings, MUST NOT change over time or else the profile MUST be considered a new profile with a new URI. + + Note: When a profile changes its URI, a huge amount of interoperability is lost. Users that reference the new URI will not have their messages understood by implementations still aware only of the old URI, and vice-versa. Accordingly, it’s important to design your profile so that it can evolve without its URI needing to change. See “Revising a Profile” for details. + +Finally, a profile MUST NOT: + + assume that, if it is supported, then other specific profiles will be supported as well. + + define fixed endpoints, HTTP headers, or header values. + + alter the JSON structure of any concept defined in this specification, including to allow a superset of JSON structures. + + If you create your own profile, you are strongly encouraged to register it with the JSON API profile registry, so that others can find and reuse it. + +Revising a Profile + +Profiles MAY be revised over time, e.g., to add new capabilities. However, any such changes MUST be backwards and forwards compatible (“compatible evolution”), in order to not break existing users of the profile. + +For example, the hypothetical timestamps profile could not introduce a new, required deleted member within the timestamps object, as that would be incompatible with existing deployments of the profile, which would not include this new member. + +The timestamps profile also could not evolve to define a new element as a sibling of the timestamps key, as that would be incompatible with the rule that “The elements… specified by a profile… MUST NOT change over time.” + + The practical issue with adding a sibling element is that another profile in use on the document might already define a sibling element of the same name, and existing documents would not have any aliases defined to resolve this conflict. + +However, the timestamps profile could evolve to allow other optional members, such as deleted, in the timestamps object. This is possible because the timestamps object is already a reserved element of the profile, and the profile is subject to the default rule that new (previously unrecognized) keys will simply be ignored by existing applications. +Designing Profiles to Evolve Over Time + +Fundamentally, for a profile to be able to change in a compatible way over time, it must define – from the beginning – a rule describing how an application that is only familiar with the original version of the profile should process documents/requests that use features from an updated version of the profile. + +One major approach is to simply have applications ignore (at least some types of) unrecognized data. This allows the profile to define new, optional features; old applications will continue to work, but simply won’t process/”see” these new capabilities. + +This is essentially the strategy that JSON:API itself uses when it says that: + + Client and server implementations MUST ignore members not recognized by this specification. + +Other protocols use analogous strategies. E.g., in HTTP, unknown headers are simply ignored; they don’t crash the processing of the request/response. + +As a profile author, you may define your own rules for how applications should process uses of the profile that contain unrecognized data, or you may simply allow the default rules described in the “Processing Profiled Documents/Requests” to take effect. + +If you choose to use the default rules, you SHOULD reserve an object-valued element anywhere you expect to potentially add new features over time. +Errors +Processing Errors + +A server MAY choose to stop processing as soon as a problem is encountered, or it MAY continue processing and encounter multiple problems. For instance, a server might process multiple attributes and then return multiple validation problems in a single response. + +When a server encounters multiple problems for a single request, the most generally applicable HTTP error code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate for multiple 4xx errors or 500 Internal Server Error might be appropriate for multiple 5xx errors. +Error Objects + +Error objects provide additional information about problems encountered while performing an operation. Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document. + +An error object MAY have the following members: + + id: a unique identifier for this particular occurrence of the problem. + links: a links object containing the following members: + about: a link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error. + type: an array of links that identify the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error. + status: the HTTP status code applicable to this problem, expressed as a string value. + code: an application-specific error code, expressed as a string value. + title: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. + detail: a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. + source: an object containing references to the source of the error, optionally including any of the following members: + pointer: a JSON Pointer [RFC6901] to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer. + parameter: a string indicating which URI query parameter caused the error. + meta: a meta object containing non-standard meta-information about the error. + +Appendix +Query Parameters Details +Parsing/Serialization + +A query parameter is a name–value pair extracted from, or serialized into, a URI’s query string. + +To extract the query parameters from a URI, an implementation MUST run the URI’s query string, excluding the leading question mark, through the application/x-www-form-urlencoded parsing algorithm, with one exception: JSON:API allows the specification that defines a query parameter’s usage to provide its own rules for parsing the parameter’s value from the value bytes identified in steps 3.2 and and 3.3 of the application/x-www-form-urlencoded parsing algorithm. The resulting value might not be a string. + + Note: In general, the query string parsing built in to servers and browsers will match the process specified above, so most implementations do not need to worry about this. + + The application/x-www-form-urlencoded format is referenced because it is the basis for the a=b&c=d style that almost all query strings use today. + + However, application/x-www-form-urlencoded parsing contains the bizarre historical artifact that + characters must be treated as spaces, and it requires that all values be percent-decoded during parsing, which makes it impossible to use RFC 3986 delimiter characters as delimiters. These issues motivate the exception that JSON:API defines above. + +Similarly, to serialize a query parameter into a URI, an implementation MUST use the the application/x-www-form-urlencoded serializer, with the corresponding exception that a parameter’s value — but not its name — may be serialized differently than that algorithm requires, provided the serialization does not interfere with the ability to parse back the resulting URI. +Square Brackets in Parameter Names + +With query parameter families, JSON:API allows for query parameters whose names contain square brackets (i.e., U+005B “[” and U+005D “]”). + +According to the query parameter serialization rules above, a compliant implementation will percent-encode these square brackets. However, some URI producers — namely browsers — do not always encode them. Servers SHOULD accept requests in which these square brackets are left unencoded in a query parameter’s name. If a server does accept these requests, it MUST treat the request as equivalent to one in which the square brackets were percent-encoded. diff --git a/spec/readme.md b/spec/readme.md new file mode 100644 index 00000000..32048761 --- /dev/null +++ b/spec/readme.md @@ -0,0 +1 @@ +This folder contain the latest JSON API specification version used in development. The main purpose is tracking specification changes with `diff` command to simplify keeping the library up to date. diff --git a/src/Contracts/Document/DocumentFactoryInterface.php b/src/Contracts/Document/DocumentFactoryInterface.php deleted file mode 100644 index 9b944c3d..00000000 --- a/src/Contracts/Document/DocumentFactoryInterface.php +++ /dev/null @@ -1,55 +0,0 @@ -|null $links - * - * @return void - */ - public function setDocumentLinks(array $links): void; - - /** - * Set arbitrary meta-information about primary data to top-level 'meta' section. - * - * @param mixed $meta - * - * @return void - */ - public function setMetaToDocument($meta): void; - - /** - * Add resource to 'data' section. - * - * @param ResourceObjectInterface $resource - * - * @return void - */ - public function addToData(ResourceObjectInterface $resource): void; - - /** - * Set empty array to 'data' section. - * - * @return void - */ - public function setEmptyData(): void; - - /** - * Set null to 'data' section. - * - * @return void - */ - public function setNullData(): void; - - /** - * Add a relationship to resource in 'data' section. - * - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relationship - * @param ResourceObjectInterface $resource - * - * @return void - */ - public function addRelationshipToData( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship, - ResourceObjectInterface $resource - ): void; - - /** - * Add an empty relationship to resource in 'data' section. - * - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relationship - * - * @return void - */ - public function addEmptyRelationshipToData( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void; - - /** - * Add a null relationship to resource in 'data' section. - * - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relationship - * - * @return void - */ - public function addNullRelationshipToData( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void; - - /** - * Add resource to 'included' section. - * - * @param ResourceObjectInterface $resource - * - * @return void - */ - public function addToIncluded(ResourceObjectInterface $resource): void; - - /** - * Add a relationship to resource in 'included' section. - * - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relationship - * @param ResourceObjectInterface $resource - * - * @return void - */ - public function addRelationshipToIncluded( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship, - ResourceObjectInterface $resource - ): void; - - /** - * Add an empty relationship to resource in 'included' section. - * - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relationship - * - * @return void - */ - public function addEmptyRelationshipToIncluded( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void; - - /** - * Add a null relationship to resource in 'included' section. - * - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relationship - * - * @return void - */ - public function addNullRelationshipToIncluded( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void; - - /** - * Mark resource as completed (no new relations/links/etc will be added to the resource anymore). - * - * @param ResourceObjectInterface $resource - * - * @return void - */ - public function setResourceCompleted(ResourceObjectInterface $resource): void; - - /** - * Add information to 'errors' top-level section. - * - * If you add error information no other elements will be in output document. - * - * @param ErrorInterface $error - * - * @return void - */ - public function addError(ErrorInterface $error): void; - - /** - * Add information to 'errors' top-level section. - * - * If you add errors information no other elements will be in output document. - * - * @param ErrorInterface[]|ErrorCollection|iterable $errors - * - * @return void - */ - public function addErrors(iterable $errors): void; - - /** - * Add JSON API version information. - * - * @link http://jsonapi.org/format/#document-jsonapi-object - * - * @param string $version - * @param mixed|null $meta - * - * @return void - */ - public function addJsonApiVersion(string $version, $meta = null); - - /** - * Set a prefix that will be applied to all URLs in the document except marked as href. - * - * @see LinkInterface - * - * @param string $prefix - * - * @return void - */ - public function setUrlPrefix(string $prefix): void; - - /** - * Remove 'data' top-level section. - * - * @return void - */ - public function unsetData(): void; - - /** - * Get document as array. - * - * @return array - */ - public function getDocument(): array; -} diff --git a/src/Contracts/Encoder/EncoderInterface.php b/src/Contracts/Encoder/EncoderInterface.php index f4596453..ba990333 100644 --- a/src/Contracts/Encoder/EncoderInterface.php +++ b/src/Contracts/Encoder/EncoderInterface.php @@ -1,7 +1,9 @@ - format. + * Limit fields in the output result. + * + * Format + * [ + * 'type1' => ['attribute1', 'attribute2', 'relationship1', ...] + * 'type2' => [] // no fields in output, only type and id. + * + * // 'type3' is not on the list so all its attributes and relationships will be in output. + * ] + * + * @param array $fieldSets + * + * @return self + */ + public function withFieldSets(array $fieldSets): self; + + /** + * Set JSON encode options. + * + * @link http://php.net/manual/en/function.json-encode.php + * + * @param int $options + * + * @return self + */ + public function withEncodeOptions(int $options): self; + + /** + * Set JSON encode depth. + * + * @link http://php.net/manual/en/function.json-encode.php + * + * @param int $depth + * + * @return self + */ + public function withEncodeDepth(int $depth): self; + + /** + * Add links that will be encoded with data. Links must be in `$name => $link, ...` format. * * @param array $links * + * @see LinkInterface + * * @return self */ public function withLinks(array $links): self; + /** + * Add profile links that will be encoded with data. Links must be in `$link1, $link2, ...` format. + * + * @param iterable $links + * + * @see LinkWithAliasesInterface + * + * @return self + */ + public function withProfile(iterable $links): self; + /** * Add meta information that will be encoded with data. If 'null' meta will not appear in a document. * @@ -48,77 +125,68 @@ public function withLinks(array $links): self; public function withMeta($meta): self; /** - * If called JSON API version information with optional meta will be added to a document. + * If called JSON API version information will be added to a document. * - * @param mixed|null $version + * @param string $version * * @return self * * @see http://jsonapi.org/format/#document-jsonapi-object */ - public function withJsonApiVersion($version = null): self; + public function withJsonApiVersion(string $version): self; + + /** + * If called JSON API version meta will be added to a document. + * + * @param mixed $meta + * + * @return self + * + * @see http://jsonapi.org/format/#document-jsonapi-object + */ + public function withJsonApiMeta($meta): self; /** * Add 'self' Link to top-level document's 'links' section for relationship specified. * - * @param object $resource - * @param string $relationshipName - * @param null|mixed $meta - * @param bool $treatAsHref + * @param object $resource + * @param string $relationshipName * * @see http://jsonapi.org/format/#fetching-relationships * * @return self - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ - public function withRelationshipSelfLink( - $resource, - string $relationshipName, - $meta = null, - bool $treatAsHref = false - ): self; + public function withRelationshipSelfLink($resource, string $relationshipName): self; /** * Add 'related' Link to top-level document's 'links' section for relationship specified. * - * @param object $resource - * @param string $relationshipName - * @param null|mixed $meta - * @param bool $treatAsHref + * @param object $resource + * @param string $relationshipName * * @see http://jsonapi.org/format/#fetching-relationships * * @return self - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ - public function withRelationshipRelatedLink( - $resource, - string $relationshipName, - $meta = null, - bool $treatAsHref = false - ): self; + public function withRelationshipRelatedLink($resource, string $relationshipName): self; /** * Encode input as JSON API string. * - * @param object|array|Iterator|null $data Data to encode. - * @param EncodingParametersInterface|null $parameters Encoding parameters. + * @param object|iterable|null $data Data to encode. * * @return string */ - public function encodeData($data, EncodingParametersInterface $parameters = null): string; + public function encodeData($data): string; /** * Encode input as JSON API string with a list of resource identifiers. * - * @param object|array|Iterator|null $data Data to encode. - * @param EncodingParametersInterface|null $parameters Encoding parameters. + * @param object|iterable|null $data Data to encode. * * @return string */ - public function encodeIdentifiers($data, EncodingParametersInterface $parameters = null): string; + public function encodeIdentifiers($data): string; /** * Encode error as JSON API string. @@ -132,11 +200,13 @@ public function encodeError(ErrorInterface $error): string; /** * Encode errors as JSON API string. * - * @param ErrorInterface[]|ErrorCollection|iterable $errors + * @see ErrorInterface + * + * @param iterable $errors * * @return string */ - public function encodeErrors($errors): string; + public function encodeErrors(iterable $errors): string; /** * Encode input meta as JSON API string. diff --git a/src/Contracts/Encoder/Handlers/HandlerFactoryInterface.php b/src/Contracts/Encoder/Handlers/HandlerFactoryInterface.php deleted file mode 100644 index e4b47815..00000000 --- a/src/Contracts/Encoder/Handlers/HandlerFactoryInterface.php +++ /dev/null @@ -1,39 +0,0 @@ -|null - */ - public function getFieldSet(string $type): ?array; -} diff --git a/src/Contracts/Encoder/Parser/ParserReplyHandlerInterface.php b/src/Contracts/Encoder/Parser/ParserReplyHandlerInterface.php deleted file mode 100644 index 20aeb65d..00000000 --- a/src/Contracts/Encoder/Parser/ParserReplyHandlerInterface.php +++ /dev/null @@ -1,32 +0,0 @@ -|null $parameters + * + * @return MediaTypeInterface + */ + public function createMediaType(string $type, string $subType, array $parameters = null): MediaTypeInterface; + + /** + * Create media type for Accept HTTP header. + * + * @param int $position + * @param string $type + * @param string $subType + * @param array|null $parameters + * @param float $quality + * + * @return AcceptMediaTypeInterface + */ + public function createAcceptMediaType( + int $position, + string $type, + string $subType, + array $parameters = null, + float $quality = 1.0 + ): AcceptMediaTypeInterface; } diff --git a/src/Contracts/Http/Headers/AcceptMediaTypeInterface.php b/src/Contracts/Http/Headers/AcceptMediaTypeInterface.php index 795dd736..1c2d0587 100644 --- a/src/Contracts/Http/Headers/AcceptMediaTypeInterface.php +++ b/src/Contracts/Http/Headers/AcceptMediaTypeInterface.php @@ -1,7 +1,9 @@ -|null $parameters - * - * @return MediaTypeInterface - */ - public function createMediaType(string $type, string $subType, array $parameters = null): MediaTypeInterface; - - /** - * Create parameters. - * - * @param string[]|null $includePaths - * @param array|null $fieldSets - * - * @return EncodingParametersInterface - */ - public function createQueryParameters( - array $includePaths = null, - array $fieldSets = null - ): EncodingParametersInterface; - - /** - * Create parameters parser. - * - * @return HeaderParametersParserInterface - */ - public function createHeaderParametersParser(): HeaderParametersParserInterface; - - /** - * Create media type for Accept HTTP header. - * - * @param int $position - * @param string $type - * @param string $subType - * @param array|null $parameters - * @param float $quality - * - * @return AcceptMediaTypeInterface - */ - public function createAcceptMediaType( - int $position, - string $type, - string $subType, - array $parameters = null, - float $quality = 1.0 - ): AcceptMediaTypeInterface; -} diff --git a/src/Contracts/Http/Query/BaseQueryParserInterface.php b/src/Contracts/Http/Query/BaseQueryParserInterface.php index 13926d81..f3fc9d14 100644 --- a/src/Contracts/Http/Query/BaseQueryParserInterface.php +++ b/src/Contracts/Http/Query/BaseQueryParserInterface.php @@ -1,7 +1,9 @@ -logger === null ?: $this->logger->log($level, $message, $context); - } + public function isNull(): bool; } diff --git a/src/Contracts/Encoder/Stack/StackReadOnlyInterface.php b/src/Contracts/Parser/IdentifierInterface.php similarity index 52% rename from src/Contracts/Encoder/Stack/StackReadOnlyInterface.php rename to src/Contracts/Parser/IdentifierInterface.php index 2c8167f9..8cfc2c3e 100644 --- a/src/Contracts/Encoder/Stack/StackReadOnlyInterface.php +++ b/src/Contracts/Parser/IdentifierInterface.php @@ -1,7 +1,9 @@ -idx = $idx; - } + public function getPosition(): PositionInterface; } diff --git a/src/Contracts/Encoder/Stack/StackFactoryInterface.php b/src/Contracts/Parser/ParserInterface.php similarity index 58% rename from src/Contracts/Encoder/Stack/StackFactoryInterface.php rename to src/Contracts/Parser/ParserInterface.php index f238105b..03291be5 100644 --- a/src/Contracts/Encoder/Stack/StackFactoryInterface.php +++ b/src/Contracts/Parser/ParserInterface.php @@ -1,7 +1,9 @@ - + * @return bool */ - public function getLinks(): array; + public function hasLinks(): bool; /** - * Get meta. + * Get relationship links. * - * @return mixed + * @see LinkInterface + * + * @return iterable */ - public function getMeta(); + public function getLinks(): iterable; /** - * If 'data' should be shown. + * If relationship has meta. * * @return bool */ - public function isShowData(): bool; + public function hasMeta(): bool; /** - * If relationship is from root (non existing root element). + * Get relationship meta. * - * @return bool + * @return mixed */ - public function isRoot(): bool; + public function getMeta(); } diff --git a/src/Contracts/Parser/ResourceInterface.php b/src/Contracts/Parser/ResourceInterface.php new file mode 100644 index 00000000..4b773010 --- /dev/null +++ b/src/Contracts/Parser/ResourceInterface.php @@ -0,0 +1,71 @@ + + * @return null|iterable */ - public function getLinks(): ?array; + public function getLinks(): ?iterable; + + /** + * Get links that may lead to further details about the problem. + * + * @see BaseLinkInterface + * + * @return null|iterable + */ + public function getTypeLinks(): ?iterable; /** * Get the HTTP status code applicable to this problem, expressed as a string value. @@ -85,10 +98,17 @@ public function getDetail(): ?string; */ public function getSource(): ?array; + /** + * If error has meta information. + * + * @return bool + */ + public function hasMeta(): bool; + /** * Get error meta information. * - * @return mixed|null + * @return mixed */ public function getMeta(); } diff --git a/src/Contracts/Encoder/Parameters/EncodingParametersInterface.php b/src/Contracts/Schema/IdentifierInterface.php similarity index 56% rename from src/Contracts/Encoder/Parameters/EncodingParametersInterface.php rename to src/Contracts/Schema/IdentifierInterface.php index ffeab965..ffa6e334 100644 --- a/src/Contracts/Encoder/Parameters/EncodingParametersInterface.php +++ b/src/Contracts/Schema/IdentifierInterface.php @@ -1,7 +1,9 @@ -|null $attributeKeysFilter - * - * @return ResourceObjectInterface - */ - public function createResourceObject( - SchemaInterface $schema, - $resource, - bool $isInArray, - array $fieldKeysFilter = null - ): ResourceObjectInterface; - - /** - * Create relationship object. - * - * @param string|null $name - * @param object|array|null $data - * @param array $links - * @param mixed $meta - * @param bool $isShowData - * @param bool $isRoot - * - * @return RelationshipObjectInterface - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function createRelationshipObject( - ?string $name, - $data, - array $links, - $meta, - bool $isShowData, - bool $isRoot - ): RelationshipObjectInterface; - - /** - * Create link. - * - * @param string $subHref - * @param array|object|null $meta - * @param bool $treatAsHref If $subHref is a full URL and must not be concatenated with other URLs. - * - * @return LinkInterface - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function createLink(string $subHref, $meta = null, bool $treatAsHref = false): LinkInterface; - - /** - * Create an adapter for schema that will provide data to encode them as resource identifiers. - * - * @param SchemaInterface $schema - * - * @return SchemaInterface - */ - public function createResourceIdentifierSchemaAdapter(SchemaInterface $schema): SchemaInterface; - - /** - * Create schema for identity objects. - * - * @param ContainerInterface $container - * @param string $classType - * @param Closure $identityClosure function($resource) : string - * - * @return SchemaInterface - */ - public function createIdentitySchema( - ContainerInterface $container, - string $classType, - Closure $identityClosure - ): SchemaInterface; -} diff --git a/src/Contracts/Schema/SchemaInterface.php b/src/Contracts/Schema/SchemaInterface.php index 7567b635..2648bb24 100644 --- a/src/Contracts/Schema/SchemaInterface.php +++ b/src/Contracts/Schema/SchemaInterface.php @@ -1,7 +1,9 @@ -|null $attributeKeysFilter + * @see LinkInterface * - * @return ResourceObjectInterface + * @return iterable */ - public function createResourceObject( - $resource, - bool $isOriginallyArrayed, - array $fieldKeysFilter = null - ): ResourceObjectInterface; + public function getLinks($resource): iterable; /** - * Get resource's relationship objects. + * Get 'self' URL link to resource relationship. * - * @param object $resource - * @param bool $isPrimary - * @param array $includeRelationships + * @param mixed $resource + * @param string $name * - * @return iterable RelationshipObjectInterface[] + * @return LinkInterface */ - public function getRelationshipObjectIterator($resource, bool $isPrimary, array $includeRelationships): iterable; + public function getRelationshipSelfLink($resource, string $name): LinkInterface; /** - * Get links related to resource. + * Get 'related' URL link to resource relationship. * - * @param mixed $resource + * @param mixed $resource + * @param string $name * - * @return array Array key is link name and value is LinkInterface. + * @return LinkInterface */ - public function getResourceLinks($resource): array; + public function getRelationshipRelatedLink($resource, string $name): LinkInterface; /** - * Get links related to resource when it is in 'included' section. + * If resource has meta when it is considered as a resource identifier (e.g. in a relationship). * * @param mixed $resource * - * @return array Array key is link name and value is LinkInterface. - */ - public function getIncludedResourceLinks($resource): array; - - /** - * If resource attributes should be shown when the resource is within 'included'. - * * @return bool */ - public function isShowAttributesInIncluded(): bool; - - /** - * Get schema default include paths. - * - * @return string[] - */ - public function getIncludePaths(): array; + public function hasIdentifierMeta($resource): bool; /** - * Get meta when resource is primary (top level 'data' section). + * Get resource meta when it is considered as a resource identifier (e.g. in a relationship). * - * @param object $resource + * @param mixed $resource * * @return mixed */ - public function getPrimaryMeta($resource); + public function getIdentifierMeta($resource); /** - * Get meta when resource is within included resources. + * If resource has meta when it is considered as a resource (e.g. in a main data or included sections). * - * @param object $resource + * @param mixed $resource * - * @return mixed + * @return bool */ - public function getInclusionMeta($resource); + public function hasResourceMeta($resource): bool; /** - * Get get relationships meta when the resource is primary. + * Get resource meta when it is considered as a resource (e.g. in a main data or included sections). * - * @param object $resource + * @param mixed $resource * * @return mixed */ - public function getRelationshipsPrimaryMeta($resource); + public function getResourceMeta($resource); /** - * Get get relationships meta when the resource is within included. + * If `self` links should be added in relationships by default. * - * @param object $resource - * - * @return mixed + * @return bool */ - public function getRelationshipsInclusionMeta($resource); + public function isAddSelfLinkInRelationshipByDefault(): bool; /** - * Get meta when resource is within relationship of a primary resource. + * If `related` links should be added in relationships by default. * - * @param object $resource - * - * @return mixed + * @return bool */ - public function getLinkageMeta($resource); + public function isAddRelatedLinkInRelationshipByDefault(): bool; } diff --git a/src/Document/Document.php b/src/Document/Document.php deleted file mode 100644 index a9dc6a07..00000000 --- a/src/Document/Document.php +++ /dev/null @@ -1,423 +0,0 @@ -presenter = new ElementPresenter($this); - } - - /** - * @inheritdoc - */ - public function setDocumentLinks(array $links): void - { - $this->links = $this->presenter->getLinksRepresentation($this->urlPrefix, $links); - } - - /** - * @inheritdoc - */ - public function setMetaToDocument($meta): void - { - (is_object($meta) === true || is_array($meta) === true) ?: Exceptions::throwInvalidArgument('meta', $meta); - $this->meta = $meta; - } - - /** - * @inheritdoc - */ - public function addToIncluded(ResourceObjectInterface $resource): void - { - $idx = $resource->getId(); - $type = $resource->getType(); - if (isset($this->hasBeenMetAlready[$type][$idx]) === false) { - $this->bufferForIncluded[$type][$idx] = $this->presenter->convertIncludedResourceToArray($resource); - $this->hasBeenMetAlready[$type][$idx] = true; - } - } - - /** - * @inheritdoc - */ - public function addToData(ResourceObjectInterface $resource): void - { - // check if 'not-arrayed' data were added you cannot add to 'non-array' data section anymore - ($this->isDataArrayed === true || $this->isDataArrayed === null) ?: Exceptions::throwLogicException(); - - $this->isDataArrayed !== null ?: $this->isDataArrayed = $resource->isInArray(); - - // check all resources have the same isInArray flag - ($this->isDataArrayed === $resource->isInArray()) ?: Exceptions::throwLogicException(); - - $idx = $resource->getId(); - $type = $resource->getType(); - - isset($this->bufferForData[$type][$idx]) === false ?: Exceptions::throwLogicException(); - - $this->bufferForData[$type][$idx] = $this->presenter->convertDataResourceToArray($resource, true); - $this->hasBeenMetAlready[$type][$idx] = true; - - // check if resource has already been added to included - // (for example as related resource of one of the previous main resources) - if (isset($this->includedResources[$type][$idx]) === true) { - $includedIndex = $this->includedResources[$type][$idx]; - - // remove duplicate from 'included' (leave only in main resources) - unset($this->included[$includedIndex]); - } - } - - /** - * @inheritdoc - */ - public function setEmptyData(): void - { - $this->data = []; - } - - /** - * @inheritdoc - */ - public function setNullData(): void - { - $this->data = null; - } - - /** - * @inheritdoc - */ - public function addRelationshipToData( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship, - ResourceObjectInterface $resource - ): void { - $this->presenter->addRelationshipTo($this->bufferForData, $parent, $relationship, $resource); - } - - /** - * @inheritdoc - */ - public function addRelationshipToIncluded( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship, - ResourceObjectInterface $resource - ): void { - $this->presenter->addRelationshipTo($this->bufferForIncluded, $parent, $relationship, $resource); - } - - /** - * @inheritdoc - */ - public function addEmptyRelationshipToData( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void { - $this->presenter->setRelationshipTo($this->bufferForData, $parent, $relationship, []); - } - - /** - * @inheritdoc - */ - public function addNullRelationshipToData( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void { - $this->presenter->setRelationshipTo($this->bufferForData, $parent, $relationship, null); - } - - /** - * @inheritdoc - */ - public function addEmptyRelationshipToIncluded( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void { - $this->presenter->setRelationshipTo($this->bufferForIncluded, $parent, $relationship, []); - } - - /** - * @inheritdoc - */ - public function addNullRelationshipToIncluded( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relationship - ): void { - $this->presenter->setRelationshipTo($this->bufferForIncluded, $parent, $relationship, null); - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.ElseExpression) - */ - public function setResourceCompleted(ResourceObjectInterface $resource): void - { - $idx = $resource->getId(); - $type = $resource->getType(); - - $foundInData = isset($this->bufferForData[$type][$idx]); - $foundInIncluded = isset($this->bufferForIncluded[$type][$idx]); - - $addMeta = function (array $representation, Closure $getMetaClosure) { - if (empty($representation[self::KEYWORD_RELATIONSHIPS]) === true) { - // if no relationships have been added remove empty placeholder - unset($representation[self::KEYWORD_RELATIONSHIPS]); - } else { - // relationship might have meta - /** @noinspection PhpParamsInspection */ - $relShipsMeta = $getMetaClosure(); - if (empty($relShipsMeta) === false) { - $representation[self::KEYWORD_RELATIONSHIPS][self::KEYWORD_META] = $relShipsMeta; - } - } - - return $representation; - }; - - if ($foundInData === true) { - $representation = $this->bufferForData[$type][$idx]; - unset($this->bufferForData[$type][$idx]); - - $this->data[] = $addMeta($representation, function () use ($resource) { - return $resource->getRelationshipsPrimaryMeta(); - }); - } - - if ($foundInIncluded === true) { - $representation = $this->bufferForIncluded[$type][$idx]; - unset($this->bufferForIncluded[$type][$idx]); - - $this->included[] = $addMeta($representation, function () use ($resource) { - return $resource->getRelationshipsInclusionMeta(); - }); - // remember we added (type, id) at index - $this->includedResources[$type][$idx] = $this->includedIndex; - $this->includedIndex++; - } - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.ElseExpression) - */ - public function getDocument(): array - { - if ($this->errors !== null) { - return array_filter([ - self::KEYWORD_JSON_API => $this->version, - self::KEYWORD_META => $this->meta, - self::KEYWORD_ERRORS => $this->errors, - ], function ($value) { - return $value !== null; - }); - } - - $document = array_filter([ - self::KEYWORD_JSON_API => $this->version, - self::KEYWORD_META => $this->meta, - self::KEYWORD_LINKS => $this->links, - self::KEYWORD_DATA => true, // this field wont be filtered - self::KEYWORD_INCLUDED => empty($this->included) === true ? null : array_values($this->included), - ], function ($value) { - return $value !== null; - }); - - if ($this->showData === true) { - $isDataNotArray = ($this->isDataArrayed === false && empty($this->data) === false); - $document[self::KEYWORD_DATA] = ($isDataNotArray ? $this->data[0] : $this->data); - } else { - unset($document[self::KEYWORD_DATA]); - } - - return $document; - } - - /** - * @inheritdoc - */ - public function addJsonApiVersion(string $version, $meta = null) - { - $this->version = $meta === null ? - [self::KEYWORD_VERSION => $version] : [self::KEYWORD_VERSION => $version, self::KEYWORD_META => $meta]; - } - - /** - * @inheritdoc - */ - public function unsetData(): void - { - $this->showData = false; - } - - /** - * @inheritdoc - */ - public function addError(ErrorInterface $error): void - { - $errorId = (($errorId = $error->getId()) === null ? null : (string)$errorId); - - $representation = array_filter([ - self::KEYWORD_ERRORS_ID => $errorId, - self::KEYWORD_ERRORS_LINKS => $this->presenter - ->getLinksRepresentation($this->urlPrefix, $error->getLinks()), - self::KEYWORD_ERRORS_STATUS => $error->getStatus(), - self::KEYWORD_ERRORS_CODE => $error->getCode(), - self::KEYWORD_ERRORS_TITLE => $error->getTitle(), - self::KEYWORD_ERRORS_DETAIL => $error->getDetail(), - self::KEYWORD_ERRORS_SOURCE => $error->getSource(), - self::KEYWORD_ERRORS_META => $error->getMeta(), - ], function ($value) { - return $value !== null; - }); - - // There is a special case when error representation is an empty array - // Due to further json transform it must be an object otherwise it will be an empty array in json - $this->errors[] = empty($representation) === false ? $representation : (object)$representation; - } - - /** - * @inheritdoc - */ - public function addErrors(iterable $errors): void - { - empty($this->errors) === false ?: $this->errors = []; - - foreach ($errors as $error) { - $this->addError($error); - } - } - - /** - * @inheritdoc - */ - public function setUrlPrefix(string $prefix): void - { - $this->urlPrefix = $prefix; - } - - /** - * Get URL prefix. - * - * @return null|string - */ - public function getUrlPrefix(): ?string - { - return $this->urlPrefix; - } -} diff --git a/src/Document/Link.php b/src/Document/Link.php deleted file mode 100644 index 4eeea5dd..00000000 --- a/src/Document/Link.php +++ /dev/null @@ -1,108 +0,0 @@ -subHref = $subHref; - $this->meta = $meta; - $this->treatAsHref = $treatAsHref; - } - - /** - * @inheritdoc - */ - public function getSubHref(): string - { - return $this->subHref; - } - - /** - * @inheritdoc - */ - public function getMeta() - { - return $this->meta; - } - - /** - * @inheritdoc - */ - public function isTreatAsHref(): bool - { - return $this->treatAsHref; - } - - /** - * @inheritdoc - */ - public function hasMeta(): bool - { - return $this->getMeta() !== null; - } - - /** - * @inheritdoc - */ - public function getHref(string $prefix = null): string - { - return $this->isTreatAsHref() === true ? $this->getSubHref() : $prefix . $this->getSubHref(); - } - - /** - * @inheritdoc - */ - public function getHrefWithMeta(string $prefix = null): array - { - return [ - DocumentInterface::KEYWORD_HREF => $this->getHref($prefix), - DocumentInterface::KEYWORD_META => $this->getMeta(), - ]; - } -} diff --git a/src/Document/Presenters/ElementPresenter.php b/src/Document/Presenters/ElementPresenter.php deleted file mode 100644 index d4e8db57..00000000 --- a/src/Document/Presenters/ElementPresenter.php +++ /dev/null @@ -1,345 +0,0 @@ - - '\'%s\' is a reserved keyword and cannot be used as a relationship name in type \'%s\'', - self::MSG_INVALID_ATTRIBUTE => - '\'%s\' is a reserved keyword and cannot be used as attribute name in type \'%s\'', - ]; - - /** - * @var array - */ - private $messages; - - /** - * @param Document $document - * @param array $messages - */ - public function __construct(Document $document, $messages = self::MESSAGES) - { - $this->document = $document; - $this->messages = $messages; - } - - /** - * @param array $target - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relation - * @param mixed $value - * - * @return void - */ - public function setRelationshipTo( - array &$target, - ResourceObjectInterface $parent, - RelationshipObjectInterface $relation, - $value - ): void { - $parentId = $parent->getId(); - $parentType = $parent->getType(); - $name = $relation->getName(); - $parentExists = isset($target[$parentType][$parentId]); - - // parent object might be already fully parsed (with children) so - // - it won't exist in $target - // - it won't make any sense to parse it again (we'll got exactly the same result and it will be thrown away - // as duplicate relations/included resources are not allowed) - if ($parentExists === true) { - $isOk = isset($target[$parentType][$parentId][Document::KEYWORD_RELATIONSHIPS][$name]) === false; - $isOk ?: Exceptions::throwLogicException(); - - $representation = []; - - if ($relation->isShowData() === true) { - $representation[Document::KEYWORD_LINKAGE_DATA] = $value; - } - - $representation += $this->getRelationRepresentation($parent, $relation); - - $target[$parentType][$parentId][Document::KEYWORD_RELATIONSHIPS][$name] = $representation; - } - } - - /** - * @param array $target - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relation - * @param ResourceObjectInterface $resource - * - * @return void - * - * @SuppressWarnings(PHPMD.ElseExpression) - */ - public function addRelationshipTo( - array &$target, - ResourceObjectInterface $parent, - RelationshipObjectInterface $relation, - ResourceObjectInterface $resource - ): void { - $parentId = $parent->getId(); - $parentType = $parent->getType(); - $parentExists = isset($target[$parentType][$parentId]); - - // parent might be already added to included to it won't be in 'target' buffer - if ($parentExists === true) { - $parentAlias = &$target[$parentType][$parentId]; - - $name = $relation->getName(); - $alreadyGotRelation = isset($parentAlias[Document::KEYWORD_RELATIONSHIPS][$name]); - - $linkage = null; - if ($relation->isShowData() === true) { - $linkage = $this->getLinkageRepresentation($resource); - } - - if ($alreadyGotRelation === false) { - // ... add the first linkage - $representation = []; - if ($linkage !== null) { - if ($resource->isInArray() === true) { - // original data in array - $representation[Document::KEYWORD_LINKAGE_DATA][] = $linkage; - } else { - // original data not in array (just object) - $representation[Document::KEYWORD_LINKAGE_DATA] = $linkage; - } - } - $representation += $this->getRelationRepresentation($parent, $relation); - - $parentAlias[Document::KEYWORD_RELATIONSHIPS][$name] = $representation; - } elseif ($alreadyGotRelation === true && $linkage !== null) { - // Check data in '$name' relationship are marked as not arrayed otherwise - // it's fail to add multiple data instances - $resource->isInArray() === true ?: Exceptions::throwLogicException(); - - // ... or add another linkage - $parentAlias[Document::KEYWORD_RELATIONSHIPS][$name][Document::KEYWORD_LINKAGE_DATA][] = $linkage; - } - } - } - - /** - * Convert resource object for 'data' section to array. - * - * @param ResourceObjectInterface $resource - * @param bool $isShowAttributes - * - * @return array - */ - public function convertDataResourceToArray(ResourceObjectInterface $resource, bool $isShowAttributes): array - { - return $this->convertResourceToArray( - $resource, - $resource->getResourceLinks(), - $resource->getPrimaryMeta(), - $isShowAttributes - ); - } - - /** - * Convert resource object for 'included' section to array. - * - * @param ResourceObjectInterface $resource - * - * @return array - */ - public function convertIncludedResourceToArray(ResourceObjectInterface $resource): array - { - return $this->convertResourceToArray( - $resource, - $resource->getIncludedResourceLinks(), - $resource->getInclusionMeta(), - $resource->isShowAttributesInIncluded() - ); - } - - /** - * @param string|null $prefix - * @param array|null $links - * - * @return array|null - */ - public function getLinksRepresentation(string $prefix = null, array $links = null): ?array - { - $result = null; - if (empty($links) === false) { - foreach ($links as $name => $link) { - /** @var LinkInterface $link */ - $result[$name] = $this->getLinkRepresentation($prefix, $link); - } - } - - return $result; - } - - /** - * @param ResourceObjectInterface $resource - * - * @return array - */ - private function getLinkageRepresentation(ResourceObjectInterface $resource): array - { - $representation = [ - Document::KEYWORD_TYPE => $resource->getType(), - Document::KEYWORD_ID => $resource->getId(), - ]; - if (($meta = $resource->getLinkageMeta()) !== null) { - $representation[Document::KEYWORD_META] = $meta; - } - - return $representation; - } - - /** - * @param string|null $prefix - * @param LinkInterface $link - * - * @return array|null|string - */ - private function getLinkRepresentation(?string $prefix, LinkInterface $link) - { - return $link->hasMeta() === true ? $link->getHrefWithMeta($prefix) : $link->getHref($prefix); - } - - /** - * @param ResourceObjectInterface $parent - * @param RelationshipObjectInterface $relation - * - * @return array - */ - private function getRelationRepresentation( - ResourceObjectInterface $parent, - RelationshipObjectInterface $relation - ): array { - $isOk = ($relation->getName() !== Document::KEYWORD_SELF); - if ($isOk === false) { - $message = $this->messages[self::MSG_INVALID_RELATIONSHIP]; - throw new InvalidArgumentException(_($message, Document::KEYWORD_SELF, $parent->getType())); - } - - $representation = []; - - if (($meta = $relation->getMeta()) !== null) { - $representation[Document::KEYWORD_META] = $meta; - } - - $baseUrl = $this->document->getUrlPrefix(); - foreach ($relation->getLinks() as $name => $link) { - $representation[Document::KEYWORD_LINKS][$name] = $this->getLinkRepresentation($baseUrl, $link); - } - - return $representation; - } - - /** - * Convert resource object to array. - * - * @param ResourceObjectInterface $resource - * @param array $resourceLinks - * @param mixed $meta - * @param bool $isShowAttributes - * - * @return array - */ - private function convertResourceToArray( - ResourceObjectInterface $resource, - array $resourceLinks, - $meta, - bool $isShowAttributes - ): array { - $representation = [ - Document::KEYWORD_TYPE => $resource->getType(), - ]; - if (($resourceId = $resource->getId()) !== null) { - $representation[Document::KEYWORD_ID] = $resourceId; - } - - $attributes = $resource->getAttributes(); - - // "type" and "id" are reserved keywords and cannot be used as resource object attributes - $isOk = (isset($attributes[Document::KEYWORD_TYPE]) === false); - if ($isOk === false) { - $message = $this->messages[self::MSG_INVALID_ATTRIBUTE]; - throw new InvalidArgumentException(_($message, Document::KEYWORD_TYPE, $resource->getType())); - } - $isOk = (isset($attributes[Document::KEYWORD_ID]) === false); - if ($isOk === false) { - $message = $this->messages[self::MSG_INVALID_ATTRIBUTE]; - throw new InvalidArgumentException(_($message, Document::KEYWORD_ID, $resource->getType())); - } - - if ($isShowAttributes === true && empty($attributes) === false) { - $representation[Document::KEYWORD_ATTRIBUTES] = $attributes; - } - - // reserve placeholder for relationships, otherwise it would be added after - // links and meta which is not visually beautiful - $representation[Document::KEYWORD_RELATIONSHIPS] = null; - - if (empty($resourceLinks) === false) { - foreach ($resourceLinks as $linkName => $link) { - /** @var LinkInterface $link */ - $representation[Document::KEYWORD_LINKS][$linkName] = - $this->getLinkRepresentation($this->document->getUrlPrefix(), $link); - } - } - - if ($meta !== null) { - $representation[Document::KEYWORD_META] = $meta; - } - - return $representation; - } -} diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index 06656d83..64a7f2c5 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -1,7 +1,9 @@ - format. + * Default encode options. * - * @var array|null - */ - private $links; - - /** - * @var array|object|null + * @link http://php.net/manual/en/function.json-encode.php */ - private $meta; + const DEFAULT_JSON_ENCODE_OPTIONS = 0; /** - * @var bool - */ - private $isAddJsonApiVersion; - - /** - * @var mixed|null + * Default encode depth. + * + * @link http://php.net/manual/en/function.json-encode.php */ - protected $jsonApiVersionMeta; + const DEFAULT_JSON_ENCODE_DEPTH = 512; /** - * @param FactoryInterface $factory - * @param ContainerInterface $container - * @param EncoderOptions|null $encoderOptions + * @param FactoryInterface $factory + * @param SchemaContainerInterface $container */ public function __construct( FactoryInterface $factory, - ContainerInterface $container, - EncoderOptions $encoderOptions = null + SchemaContainerInterface $container ) { - $this->factory = $factory; - $this->container = $container; - $this->encoderOptions = $encoderOptions; - - $this->resetEncodeParameters(); + $this->setFactory($factory)->setContainer($container)->resetPropertiesToDefaults(); } /** - * @inheritdoc - */ - public function withLinks(array $links): EncoderInterface - { - $this->links = array_merge($this->links, $links); - - return $this; - } - - /** - * @inheritdoc + * Create encoder instance. + * + * @param array $schemas Schema providers. + * + * @return EncoderInterface */ - public function withMeta($meta): EncoderInterface + public static function instance(array $schemas = []): EncoderInterface { - $this->meta = $meta; + $factory = static::createFactory(); + $container = $factory->createSchemaContainer($schemas); + $encoder = $factory->createEncoder($container); - return $this; + return $encoder; } /** * @inheritdoc */ - public function withJsonApiVersion($version = null): EncoderInterface + public function encodeData($data): string { - $this->isAddJsonApiVersion = $version !== null; - $this->jsonApiVersionMeta = $version; - - return $this; - } - + // encode to json + $array = $this->encodeDataToArray($data); + $result = $this->encodeToJson($array); - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function withRelationshipSelfLink( - $resource, - string $relationshipName, - $meta = null, - bool $treatAsHref = false - ): EncoderInterface { - $link = $this->getContainer()->getSchema($resource) - ->getRelationshipSelfLink($resource, $relationshipName, $meta, $treatAsHref); - - return $this->withLinks([ - DocumentInterface::KEYWORD_SELF => $link, - ]); - } + $this->resetPropertiesToDefaults(); - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function withRelationshipRelatedLink( - $resource, - string $relationshipName, - $meta = null, - bool $treatAsHref = false - ): EncoderInterface { - $link = $this->getContainer()->getSchema($resource) - ->getRelationshipRelatedLink($resource, $relationshipName, $meta, $treatAsHref); - - return $this->withLinks([ - DocumentInterface::KEYWORD_RELATED => $link, - ]); + return $result; } /** * @inheritdoc */ - public function encodeData($data, EncodingParametersInterface $parameters = null): string + public function encodeIdentifiers($data): string { - $array = $this->encodeDataToArray($this->getContainer(), $data, $parameters); + // encode to json + $array = $this->encodeIdentifiersToArray($data); $result = $this->encodeToJson($array); + $this->resetPropertiesToDefaults(); + return $result; } /** * @inheritdoc */ - public function encodeIdentifiers($data, EncodingParametersInterface $parameters = null): string + public function encodeError(ErrorInterface $error): string { - $array = $this->encodeIdentifiersToArray($data, $parameters); + // encode to json + $array = $this->encodeErrorToArray($error); $result = $this->encodeToJson($array); + $this->resetPropertiesToDefaults(); + return $result; } /** * @inheritdoc */ - public function encodeError(ErrorInterface $error): string + public function encodeErrors(iterable $errors): string { - return $this->encodeToJson($this->encodeErrorToArray($error)); - } + // encode to json + $array = $this->encodeErrorsToArray($errors); + $result = $this->encodeToJson($array); - /** - * @inheritdoc - */ - public function encodeErrors($errors): string - { - return $this->encodeToJson($this->encodeErrorsToArray($errors)); + $this->resetPropertiesToDefaults(); + + return $result; } /** @@ -206,106 +156,140 @@ public function encodeErrors($errors): string */ public function encodeMeta($meta): string { - return $this->encodeToJson($this->encodeMetaToArray($meta)); - } - - /** - * @param ContainerInterface $container - * @param object|array|Iterator|null $data - * @param EncodingParametersInterface|null $parameters - * - * @return array - */ - protected function encodeDataToArray( - ContainerInterface $container, - $data, - EncodingParametersInterface $parameters = null - ): array { - $this->checkInputData($data); - - $docWriter = $this->getFactory()->createDocument(); - $paramAnalyzer = $this->createParametersAnalyzer($container, $parameters); - $parserManager = $this->getFactory()->createManager($paramAnalyzer); - $interpreter = $this->getFactory()->createReplyInterpreter($docWriter, $paramAnalyzer); - $parser = $this->getFactory()->createParser($container, $parserManager); - - $this->configureUrlPrefix($docWriter); - - foreach ($parser->parse($data) as $reply) { - $interpreter->handle($reply); - } - - $this->addTopLevelMeta($docWriter); - $this->addTopLevelLinks($docWriter); - $this->addTopLevelJsonApiVersion($docWriter); + // encode to json + $array = $this->encodeMetaToArray($meta); + $result = $this->encodeToJson($array); - $result = $docWriter->getDocument(); - $this->resetEncodeParameters(); + $this->resetPropertiesToDefaults(); return $result; } /** - * Encode array to JSON. - * - * @param array $document - * - * @return string + * @return FactoryInterface */ - protected function encodeToJson(array $document): string + protected static function createFactory(): FactoryInterface { - return $this->getEncoderOptions() === null ? - json_encode($document) : - json_encode($document, $this->getEncoderOptions()->getOptions(), $this->getEncoderOptions()->getDepth()); + return new Factory(); } /** - * Create encoder instance. + * @param object|iterable|null $data Data to encode. * - * @param array $schemas Schema providers. - * @param EncoderOptions|null $encodeOptions + * @return array * - * @return EncoderInterface + * @SuppressWarnings(PHPMD.ElseExpression) */ - public static function instance(array $schemas = [], EncoderOptions $encodeOptions = null): EncoderInterface + protected function encodeDataToArray($data): array { - $factory = static::createFactory(); - $container = $factory->createContainer($schemas); - $encoder = $factory->createEncoder($container, $encodeOptions); + if (is_array($data) === false && is_object($data) === false && $data !== null) { + throw new InvalidArgumentException(); + } - return $encoder; - } + $parser = $this->getFactory()->createParser($this->getSchemaContainer()); + $writer = $this->createDocumentWriter(); + $filter = $this->getFactory()->createFieldSetFilter($this->getFieldSets()); + + // write header + $this->writeHeader($writer); + + // write body + foreach ($parser->parse($data, $this->getIncludePaths()) as $item) { + if ($item instanceof ResourceInterface) { + if ($item->getPosition()->getLevel() > ParserInterface::ROOT_LEVEL) { + if ($filter->shouldOutputRelationship($item->getPosition()) === true) { + $writer->addResourceToIncluded($item, $filter); + } + } else { + $writer->addResourceToData($item, $filter); + } + } elseif ($item instanceof IdentifierInterface) { + assert($item->getPosition()->getLevel() <= ParserInterface::ROOT_LEVEL); + $writer->addIdentifierToData($item); + } else { + assert($item instanceof DocumentDataInterface); + assert($item->getPosition()->getLevel() === 0); + if ($item->isCollection() === true) { + $writer->setDataAsArray(); + } elseif ($item->isNull() === true) { + $writer->setNullToData(); + } + } + } - /** - * @return FactoryInterface - */ - protected static function createFactory(): FactoryInterface - { - return new Factory(); - } + // write footer + $this->writeFooter($writer); - /** - * @param mixed $data - */ - protected function checkInputData($data): void - { - if (is_array($data) === false && is_object($data) === false && $data !== null && !($data instanceof Iterator)) { - throw new InvalidArgumentException('data'); - } + $array = $writer->getDocument(); + + return $array; } /** - * @param object|array|Iterator|null $data - * @param EncodingParametersInterface|null $parameters + * @param object|iterable|null $data Data to encode. * * @return array + * + * @SuppressWarnings(PHPMD.ElseExpression) */ - protected function encodeIdentifiersToArray($data, EncodingParametersInterface $parameters = null): array + protected function encodeIdentifiersToArray($data): array { - $container = $this->getFactory()->createResourceIdentifierContainerAdapter($this->getContainer()); - $result = $this->encodeDataToArray($container, $data, $parameters); + $parser = $this->getFactory()->createParser($this->getSchemaContainer()); + $writer = $this->createDocumentWriter(); + $filter = $this->getFactory()->createFieldSetFilter($this->getFieldSets()); + + // write header + $this->writeHeader($writer); + + // write body + $includePaths = $this->getIncludePaths(); + $expectIncluded = empty($includePaths) === false; + + // https://github.com/neomerx/json-api/issues/218 + // + // if we expect included resources we have to include top level resources in `included` as well + // Spec: + // + // GET /articles/1/relationships/comments?include=comments.author HTTP/1.1 + // Accept: application/vnd.api+json + // + // In this case, the primary data would be a collection of resource identifier objects that + // represent linkage to comments for an article, while the full comments and comment authors + // would be returned as included data. + + foreach ($parser->parse($data, $includePaths) as $item) { + if ($item instanceof ResourceInterface) { + if ($item->getPosition()->getLevel() > ParserInterface::ROOT_LEVEL) { + assert($expectIncluded === true); + if ($filter->shouldOutputRelationship($item->getPosition()) === true) { + $writer->addResourceToIncluded($item, $filter); + } + } else { + $writer->addIdentifierToData($item); + if ($expectIncluded === true) { + $writer->addResourceToIncluded($item, $filter); + } + } + } elseif ($item instanceof IdentifierInterface) { + assert($item->getPosition()->getLevel() <= ParserInterface::ROOT_LEVEL); + $writer->addIdentifierToData($item); + } else { + assert($item instanceof DocumentDataInterface); + assert($item->getPosition()->getLevel() === 0); + if ($item->isCollection() === true) { + $writer->setDataAsArray(); + } elseif ($item->isNull() === true) { + $writer->setNullToData(); + } + } + } - return $result; + // write footer + $this->writeFooter($writer); + + $array = $writer->getDocument(); + + return $array; } /** @@ -315,13 +299,18 @@ protected function encodeIdentifiersToArray($data, EncodingParametersInterface $ */ protected function encodeErrorToArray(ErrorInterface $error): array { - $docWriter = $this->getFactory()->createDocument(); - $docWriter->addError($error); + $writer = $this->createErrorWriter(); + + // write header + $this->writeHeader($writer); + + // write body + $writer->addError($error); - $this->addTopLevelMeta($docWriter); - $this->addTopLevelJsonApiVersion($docWriter); + // write footer + $this->writeFooter($writer); - $array = $docWriter->getDocument(); + $array = $writer->getDocument(); return $array; } @@ -333,13 +322,22 @@ protected function encodeErrorToArray(ErrorInterface $error): array */ protected function encodeErrorsToArray(iterable $errors): array { - $docWriter = $this->getFactory()->createDocument(); - $docWriter->addErrors($errors); + $writer = $this->createErrorWriter(); - $this->addTopLevelMeta($docWriter); - $this->addTopLevelJsonApiVersion($docWriter); + // write header + $this->writeHeader($writer); - $array = $docWriter->getDocument(); + // write body + foreach ($errors as $error) { + assert($error instanceof ErrorInterface); + $writer->addError($error); + } + + // write footer + $this->writeFooter($writer); + + // encode to json + $array = $writer->getDocument(); return $array; } @@ -351,134 +349,107 @@ protected function encodeErrorsToArray(iterable $errors): array */ protected function encodeMetaToArray($meta): array { - $docWriter = $this->getFactory()->createDocument(); + $this->withMeta($meta); + + $writer = $this->getFactory()->createDocumentWriter(); - $docWriter->setMetaToDocument($meta); - $docWriter->unsetData(); - $array = $docWriter->getDocument(); + $writer->setUrlPrefix($this->getUrlPrefix()); + + // write header + $this->writeHeader($writer); + + // write footer + $this->writeFooter($writer); + + // encode to json + $array = $writer->getDocument(); return $array; } /** - * @param DocumentInterface $docWriter + * @param BaseWriterInterface $writer + * + * @return void */ - protected function addTopLevelMeta(DocumentInterface $docWriter): void + protected function writeHeader(BaseWriterInterface $writer): void { - if ($this->getMeta() !== null) { - $docWriter->setMetaToDocument($this->getMeta()); + if ($this->hasMeta() === true) { + $writer->setMeta($this->getMeta()); } - } - /** - * @param DocumentInterface $docWriter - */ - protected function addTopLevelLinks(DocumentInterface $docWriter): void - { - if (empty($this->getLinks()) === false) { - $docWriter->setDocumentLinks($this->getLinks()); + if ($this->hasJsonApiVersion() === true) { + $writer->setJsonApiVersion($this->getJsonApiVersion()); } - } - /** - * @param DocumentInterface $docWriter - */ - protected function addTopLevelJsonApiVersion(DocumentInterface $docWriter): void - { - if ($this->isWithJsonApiVersion() === true) { - $docWriter->addJsonApiVersion(self::JSON_API_VERSION, $this->getJsonApiVersionMeta()); + if ($this->hasJsonApiMeta() === true) { + $writer->setJsonApiMeta($this->getJsonApiMeta()); } - } - /** - * @return ContainerInterface - */ - protected function getContainer(): ContainerInterface - { - return $this->container; - } + if ($this->hasLinks() === true) { + $writer->setLinks($this->getLinks()); + } - /** - * @return FactoryInterface - */ - protected function getFactory(): FactoryInterface - { - return $this->factory; + if ($this->hasProfile() === true) { + $writer->setProfile($this->getProfile()); + } } /** - * @return EncoderOptions|null + * @param BaseWriterInterface $writer + * + * @return void */ - protected function getEncoderOptions(): ?EncoderOptions + protected function writeFooter(BaseWriterInterface $writer): void { - return $this->encoderOptions; + assert($writer !== null); } /** - * @return array|null + * Encode array to JSON. + * + * @param array $document + * + * @return string */ - protected function getLinks(): ?array + protected function encodeToJson(array $document): string { - return $this->links; + return json_encode($document, $this->getEncodeOptions(), $this->getEncodeDepth()); } /** - * @return array|null|object + * @return DocumentWriterInterface */ - public function getMeta() + private function createDocumentWriter(): DocumentWriterInterface { - return $this->meta; - } + $writer = $this->getFactory()->createDocumentWriter(); + $writer->setUrlPrefix($this->getUrlPrefix()); - /** - * @return bool - */ - protected function isWithJsonApiVersion(): bool - { - return $this->isAddJsonApiVersion; + return $writer; } /** - * @return mixed|null + * @return ErrorWriterInterface */ - protected function getJsonApiVersionMeta() + private function createErrorWriter(): ErrorWriterInterface { - return $this->jsonApiVersionMeta; - } - - /** - * @param ContainerInterface $container - * @param EncodingParametersInterface|null $parameters - * - * @return ParametersAnalyzerInterface - */ - private function createParametersAnalyzer( - ContainerInterface $container, - EncodingParametersInterface $parameters = null - ): ParametersAnalyzerInterface { - return $this->getFactory()->createParametersAnalyzer( - $parameters === null ? $this->getFactory()->createQueryParameters() : $parameters, - $container - ); - } + $writer = $this->getFactory()->createErrorWriter(); + $writer->setUrlPrefix($this->getUrlPrefix()); - /** - * Reset encode parameters. - */ - private function resetEncodeParameters(): void - { - $this->meta = null; - $this->links = []; - $this->isAddJsonApiVersion = false; - $this->jsonApiVersionMeta = null; + return $writer; } /** - * @param DocumentInterface $docWriter + * @return self */ - private function configureUrlPrefix(DocumentInterface $docWriter): void + private function resetPropertiesToDefaults(): self { - $this->getEncoderOptions() !== null && $this->getEncoderOptions()->getUrlPrefix() !== null ? - $docWriter->setUrlPrefix($this->getEncoderOptions()->getUrlPrefix()) : null; + return $this->reset( + static::DEFAULT_URL_PREFIX, + static::DEFAULT_INCLUDE_PATHS, + static::DEFAULT_FIELD_SET_FILTERS, + static::DEFAULT_JSON_ENCODE_OPTIONS, + static::DEFAULT_JSON_ENCODE_DEPTH + ); } } diff --git a/src/Encoder/EncoderOptions.php b/src/Encoder/EncoderOptions.php deleted file mode 100644 index b9a601fd..00000000 --- a/src/Encoder/EncoderOptions.php +++ /dev/null @@ -1,87 +0,0 @@ - 0 ?: Exceptions::throwInvalidArgument('depth', $depth); - - $this->options = $options; - $this->depth = $depth; - $this->urlPrefix = $urlPrefix; - } - - /** - * @link http://php.net/manual/en/function.json-encode.php - * - * @return int - */ - public function getOptions(): int - { - return $this->options; - } - - /** - * @link http://php.net/manual/en/function.json-encode.php - * - * @return int - */ - public function getDepth(): int - { - return $this->depth; - } - - /** - * @return null|string - */ - public function getUrlPrefix(): ?string - { - return $this->urlPrefix; - } -} diff --git a/src/Encoder/EncoderPropertiesTrait.php b/src/Encoder/EncoderPropertiesTrait.php new file mode 100644 index 00000000..1489fad2 --- /dev/null +++ b/src/Encoder/EncoderPropertiesTrait.php @@ -0,0 +1,480 @@ +links = null; + $this->profile = null; + $this->hasMeta = false; + $this->meta = null; + $this->jsonApiVersion = null; + $this->jsonApiMeta = null; + $this->hasJsonApiMeta = false; + + $this + ->withUrlPrefix($urlPrefix) + ->withIncludedPaths($includePaths) + ->withFieldSets($fieldSets) + ->withEncodeOptions($encodeOptions) + ->withEncodeDepth($encodeDepth); + + return $this; + } + + /** + * @return SchemaContainerInterface + */ + protected function getSchemaContainer(): SchemaContainerInterface + { + return $this->container; + } + + /** + * @param SchemaContainerInterface $container + * + * @return self + */ + public function setContainer(SchemaContainerInterface $container): self + { + $this->container = $container; + + return $this; + } + + /** + * @return FactoryInterface + */ + protected function getFactory(): FactoryInterface + { + return $this->factory; + } + + /** + * @param FactoryInterface $factory + * + * @return self + */ + public function setFactory(FactoryInterface $factory): self + { + $this->factory = $factory; + + return $this; + } + + /** + * @param string $prefix + * + * @return self|EncoderInterface + */ + public function withUrlPrefix(string $prefix): EncoderInterface + { + $this->urlPrefix = $prefix; + + return $this; + } + /** + * @return string + */ + protected function getUrlPrefix(): string + { + return $this->urlPrefix; + } + + /** + * @param iterable $paths + * + * @return self|EncoderInterface + */ + public function withIncludedPaths(iterable $paths): EncoderInterface + { + assert( + call_user_func( + function (array $paths): bool { + $pathsOk = true; + foreach ($paths as $path) { + $pathsOk = $pathsOk === true && is_string($path) === true && empty($path) === false; + } + + return $pathsOk; + }, + $paths + ) + ); + + $this->includePaths = $paths; + + return $this; + } + + /** + * @return array + */ + protected function getIncludePaths(): array + { + return $this->includePaths; + } + + /** + * @param array $fieldSets + * + * @return self|EncoderInterface + */ + public function withFieldSets(array $fieldSets): EncoderInterface + { + $this->fieldSets = $fieldSets; + + return $this; + } + + + /** + * @return array + */ + protected function getFieldSets(): array + { + return $this->fieldSets; + } + + /** + * @param int $options + * + * @return self|EncoderInterface + */ + public function withEncodeOptions(int $options): EncoderInterface + { + $this->encodeOptions = $options; + + return $this; + } + + /** + * @return int + */ + protected function getEncodeOptions(): int + { + return $this->encodeOptions; + } + + /** + * @param int $depth + * + * @return self|EncoderInterface + */ + public function withEncodeDepth(int $depth): EncoderInterface + { + assert($depth > 0); + + $this->encodeDepth = $depth; + + return $this; + } + + /** + * @return int + */ + protected function getEncodeDepth(): int + { + return $this->encodeDepth; + } + + /** + * @param iterable $links + * + * @return self|EncoderInterface + */ + public function withLinks(iterable $links): EncoderInterface + { + $this->links = $this->hasLinks() === true ? $this->mergeIterables($this->links, $links) : $links; + + return $this; + } + + /** + * @return bool + */ + protected function hasLinks(): bool + { + return $this->links !== null; + } + + /** + * @return iterable + */ + protected function getLinks(): iterable + { + return $this->links; + } + + /** + * @param iterable $links + * + * @return self|EncoderInterface + */ + public function withProfile(iterable $links): EncoderInterface + { + $this->profile = $links; + + return $this; + } + + /** + * @return bool + */ + protected function hasProfile(): bool + { + return $this->profile !== null; + } + + /** + * @return iterable + */ + protected function getProfile(): iterable + { + return $this->profile; + } + + /** + * @param mixed $meta + * + * @return self|EncoderInterface + */ + public function withMeta($meta): EncoderInterface + { + $this->meta = $meta; + $this->hasMeta = true; + + return $this; + } + + /** + * @return bool + */ + protected function hasMeta(): bool + { + return $this->hasMeta; + } + + /** + * @return mixed + */ + public function getMeta() + { + return $this->meta; + } + + /** + * @param string $version + * + * @return self|EncoderInterface + */ + public function withJsonApiVersion(string $version): EncoderInterface + { + $this->jsonApiVersion = $version; + + return $this; + } + + /** + * @return bool + */ + protected function hasJsonApiVersion(): bool + { + return $this->jsonApiVersion !== null; + } + + /** + * @return string + */ + protected function getJsonApiVersion(): string + { + return $this->jsonApiVersion; + } + + /** + * @param mixed $meta + * + * @return self|EncoderInterface + */ + public function withJsonApiMeta($meta): EncoderInterface + { + $this->jsonApiMeta = $meta; + $this->hasJsonApiMeta = true; + + return $this; + } + + /** + * @return bool + */ + protected function hasJsonApiMeta(): bool + { + return $this->hasJsonApiMeta; + } + + /** + * @return mixed + */ + protected function getJsonApiMeta() + { + return $this->jsonApiMeta; + } + + /** + * @param mixed $resource + * @param string $relationshipName + * + * @return self|EncoderInterface + */ + public function withRelationshipSelfLink($resource, string $relationshipName): EncoderInterface + { + $link = $this + ->getSchemaContainer()->getSchema($resource) + ->getRelationshipSelfLink($resource, $relationshipName); + + return $this->withLinks([ + LinkInterface::SELF => $link, + ]); + } + + /** + * @param mixed $resource + * @param string $relationshipName + * + * @return self|EncoderInterface + */ + public function withRelationshipRelatedLink($resource, string $relationshipName): EncoderInterface + { + $link = $this + ->getSchemaContainer()->getSchema($resource) + ->getRelationshipRelatedLink($resource, $relationshipName); + + return $this->withLinks([ + LinkInterface::RELATED => $link, + ]); + } + + /** + * @param iterable $iterable1 + * @param iterable $iterable2 + * + * @return iterable + */ + private function mergeIterables(iterable $iterable1, iterable $iterable2): iterable + { + yield from $iterable1; + yield from $iterable2; + } +} diff --git a/src/Encoder/Handlers/ReplyInterpreter.php b/src/Encoder/Handlers/ReplyInterpreter.php deleted file mode 100644 index 625734b0..00000000 --- a/src/Encoder/Handlers/ReplyInterpreter.php +++ /dev/null @@ -1,281 +0,0 @@ -document = $document; - $this->parameterAnalyzer = $parameterAnalyzer; - } - - /** - * @inheritdoc - */ - public function handle(ParserReplyInterface $reply): void - { - $current = $reply->getStack()->end(); - - if ($reply->getReplyType() === ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED) { - $this->setResourceCompleted($current); - - return; - } - - $previous = $reply->getStack()->penult(); - - switch ($current->getLevel()) { - case 1: - $this->addToData($reply, $current); - break; - case 2: - $rootType = $reply->getStack()->root()->getResource()->getType(); - $this->handleRelationships($rootType, $reply, $current, $previous); - break; - default: - $rootType = $reply->getStack()->root()->getResource()->getType(); - $this->handleIncluded($rootType, $reply, $current, $previous); - break; - } - } - - /** - * @param string $rootType - * @param ParserReplyInterface $reply - * @param Frame $current - * @param Frame $previous - */ - protected function handleRelationships( - string $rootType, - ParserReplyInterface $reply, - Frame $current, - Frame $previous - ): void { - $this->addToIncludedAndCheckIfParentIsTarget($rootType, $reply, $current, $previous); - - if ($this->isRelationshipInFieldSet($current, $previous) === true) { - $this->addRelationshipToData($reply, $current, $previous); - } - } - - /** - * @param string $rootType - * @param ParserReplyInterface $reply - * @param Frame $current - * @param Frame $previous - */ - protected function handleIncluded( - string $rootType, - ParserReplyInterface $reply, - Frame $current, - Frame $previous - ): void { - if ($this->addToIncludedAndCheckIfParentIsTarget($rootType, $reply, $current, $previous) === true && - $this->isRelationshipInFieldSet($current, $previous) === true - ) { - $this->addRelationshipToIncluded($reply, $current, $previous); - } - } - - /** - * @param string $rootType - * @param ParserReplyInterface $reply - * @param Frame $current - * @param Frame $previous - * - * @return bool - */ - private function addToIncludedAndCheckIfParentIsTarget( - string $rootType, - ParserReplyInterface $reply, - Frame $current, - Frame $previous - ): bool { - list($parentIsTarget, $currentIsTarget) = $this->getIfTargets($rootType, $current, $previous); - - if ($currentIsTarget === true) { - $this->addToIncluded($reply, $current); - } - - return $parentIsTarget; - } - - /** - * @param ParserReplyInterface $reply - * @param Frame $current - * - * @return void - */ - private function addToData(ParserReplyInterface $reply, Frame $current): void - { - switch ($reply->getReplyType()) { - case ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED: - $this->document->setNullData(); - break; - case ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED: - $this->document->setEmptyData(); - break; - case ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED: - $this->document->addToData($current->getResource()); - break; - } - } - - /** - * @param ParserReplyInterface $reply - * @param Frame $current - * - * @return void - */ - private function addToIncluded(ParserReplyInterface $reply, Frame $current): void - { - if ($reply->getReplyType() === ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED) { - $resourceObject = $current->getResource(); - $this->document->addToIncluded($resourceObject); - } - } - - /** - * @param Frame $current - * @param Frame $previous - * @param ParserReplyInterface $reply - * - * @return void - */ - private function addRelationshipToData(ParserReplyInterface $reply, Frame $current, Frame $previous): void - { - $relationship = $current->getRelationship(); - $parent = $previous->getResource(); - - switch ($reply->getReplyType()) { - case ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED: - $this->document->addNullRelationshipToData($parent, $relationship); - break; - case ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED: - $this->document->addEmptyRelationshipToData($parent, $relationship); - break; - case ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED: - $this->document->addRelationshipToData($parent, $relationship, $current->getResource()); - break; - } - } - - /** - * @param Frame $current - * @param Frame $previous - * @param ParserReplyInterface $reply - * - * @return void - */ - private function addRelationshipToIncluded(ParserReplyInterface $reply, Frame $current, Frame $previous): void - { - $relationship = $current->getRelationship(); - $parent = $previous->getResource(); - - switch ($reply->getReplyType()) { - case ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED: - $this->document->addNullRelationshipToIncluded($parent, $relationship); - break; - case ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED: - $this->document->addEmptyRelationshipToIncluded($parent, $relationship); - break; - case ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED: - $this->document->addRelationshipToIncluded($parent, $relationship, $current->getResource()); - break; - } - } - - /** - * @param Frame $current - * - * @return void - */ - private function setResourceCompleted(Frame $current): void - { - // Add resource if it is a main resource (even if it has no fields) or - // if field set allows any fields for this type (filter out resources with no attributes and relationships) - if ($current->getLevel() === 1 || - $this->parameterAnalyzer->hasSomeFields($current->getResource()->getType()) === true - ) { - $resourceObject = $current->getResource(); - $this->document->setResourceCompleted($resourceObject); - } - } - - /** - * @param string $rootType - * @param Frame $current - * @param Frame|null $previous - * - * @return bool[] - */ - private function getIfTargets(string $rootType, Frame $current, Frame $previous = null): array - { - $currentIsTarget = $this->parameterAnalyzer->isPathIncluded($current->getPath(), $rootType); - $parentIsTarget = ($previous === null || - $this->parameterAnalyzer->isPathIncluded($previous->getPath(), $rootType)); - - return [$parentIsTarget, $currentIsTarget]; - } - - /** - * If relationship from 'parent' to 'current' resource passes field set filter. - * - * @param Frame $current - * @param Frame $previous - * - * @return bool - */ - private function isRelationshipInFieldSet(Frame $current, Frame $previous): bool - { - $parentType = $previous->getResource()->getType(); - $parameters = $this->parameterAnalyzer->getParameters(); - if (($fieldSet = $parameters->getFieldSet($parentType)) === null) { - return true; - } - - return (in_array($current->getRelationship()->getName(), $fieldSet, true) === true); - } -} diff --git a/src/Encoder/Parameters/EncodingParameters.php b/src/Encoder/Parameters/EncodingParameters.php deleted file mode 100644 index 4affe089..00000000 --- a/src/Encoder/Parameters/EncodingParameters.php +++ /dev/null @@ -1,71 +0,0 @@ -fieldSets = $fieldSets; - $this->includePaths = $includePaths; - } - - /** - * @inheritdoc - */ - public function getIncludePaths(): ?array - { - return $this->includePaths; - } - - /** - * @inheritdoc - */ - public function getFieldSets(): ?array - { - return $this->fieldSets; - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.StaticAccess) - */ - public function getFieldSet(string $type): ?array - { - return (isset($this->fieldSets[$type]) === true ? $this->fieldSets[$type] : null); - } -} diff --git a/src/Encoder/Parameters/ParametersAnalyzer.php b/src/Encoder/Parameters/ParametersAnalyzer.php deleted file mode 100644 index ced034d1..00000000 --- a/src/Encoder/Parameters/ParametersAnalyzer.php +++ /dev/null @@ -1,229 +0,0 @@ -container = $container; - $this->parameters = $parameters; - } - - /** - * @inheritdoc - */ - public function getParameters(): EncodingParametersInterface - { - return $this->parameters; - } - - /** - * @inheritdoc - */ - public function isPathIncluded(?string $path, string $type): bool - { - // check if it's in cache - if (isset($this->includePathsCache[$type][$path]) === true) { - return $this->includePathsCache[$type][$path]; - } - - $includePaths = $this->getIncludePathsByType($type); - - $result = - $this->hasExactPathMatch($includePaths, $path) === true || - // RC4 spec changed requirements and intermediate paths should be included as well - $this->hasMatchWithIncludedPaths($includePaths, $path) === true; - - $this->includePathsCache[$type][$path] = $result; - - return $result; - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.ElseExpression) - */ - public function getIncludeRelationships(?string $path, string $type): array - { - // check if it's in cache - if (isset($this->includeRelationshipsCache[$type][$path]) === true) { - return $this->includeRelationshipsCache[$type][$path]; - } - - $includePaths = $this->getIncludePathsByType($type); - $pathBeginning = (string)$path; - $pathLength = strlen($pathBeginning); - - $result = []; - foreach ($includePaths as $curPath) { - if ($pathLength === 0) { - $relationshipName = $this->getRelationshipNameForTopResource($curPath); - } elseif (strpos($curPath, $pathBeginning . DocumentInterface::PATH_SEPARATOR) === 0) { - $relationshipName = $this->getRelationshipNameForResource($curPath, $pathLength); - } else { - $relationshipName = null; - } - - // add $relationshipName to $result if not yet there - if ($relationshipName !== null && isset($result[$relationshipName]) === false) { - $result[$relationshipName] = $relationshipName; - } - } - - $this->includeRelationshipsCache[$type][$path] = $result; - - return $result; - } - - /** - * @inheritdoc - */ - public function hasSomeFields(string $type): bool - { - $hasSomeFields = $this->getParameters()->getFieldSet($type) !== []; - - return $hasSomeFields; - } - - /** - * If path has exact match with one of the 'include' paths. - * - * @param string[] $paths - * @param string|null $path - * - * @return bool - */ - protected function hasExactPathMatch(array $paths, ?string $path): bool - { - $result = in_array($path, $paths, true); - - return $result; - } - - /** - * If path matches one of the included paths. - * - * @param string[] $paths - * @param string|null $path - * - * @return bool - */ - protected function hasMatchWithIncludedPaths(array $paths, ?string $path): bool - { - $hasMatch = false; - - if ($path !== null) { - foreach ($paths as $targetPath) { - if (strpos($targetPath, $path . DocumentInterface::PATH_SEPARATOR) === 0) { - $hasMatch = true; - break; - } - } - } - - return $hasMatch; - } - - /** - * @param string $type - * - * @return string[] - * - * @SuppressWarnings(PHPMD.ElseExpression) - */ - private function getIncludePathsByType(string $type): array - { - $includePaths = $this->getParameters()->getIncludePaths(); - - // if include paths are set in params use them otherwise use default include paths from schema - if ($includePaths !== null) { - return $includePaths; - } - - $schema = $this->container->getSchemaByResourceType($type); - $typePaths = $schema->getIncludePaths(); - - return $typePaths; - } - - /** - * @param string $curPath - * - * @return string - */ - private function getRelationshipNameForTopResource(string $curPath): string - { - $nextSeparatorPos = strpos($curPath, DocumentInterface::PATH_SEPARATOR); - $relationshipName = $nextSeparatorPos === false ? $curPath : substr($curPath, 0, $nextSeparatorPos); - - return $relationshipName; - } - - /** - * @param string $curPath - * @param int $pathLength - * - * @return string - */ - private function getRelationshipNameForResource(string $curPath, int $pathLength): string - { - $nextSeparatorPos = strpos($curPath, DocumentInterface::PATH_SEPARATOR, $pathLength + 1); - $relationshipName = $nextSeparatorPos === false ? - substr($curPath, $pathLength + 1) : - substr($curPath, $pathLength + 1, $nextSeparatorPos - $pathLength - 1); - - return $relationshipName; - } -} diff --git a/src/Encoder/Parser/BaseReply.php b/src/Encoder/Parser/BaseReply.php deleted file mode 100644 index b48ab7e4..00000000 --- a/src/Encoder/Parser/BaseReply.php +++ /dev/null @@ -1,62 +0,0 @@ -stack = $stack; - $this->replyType = $replyType; - } - - /** - * @inheritdoc - */ - public function getReplyType(): int - { - return $this->replyType; - } - - /** - * @inheritdoc - */ - public function getStack(): StackReadOnlyInterface - { - return $this->stack; - } -} diff --git a/src/Encoder/Parser/Parser.php b/src/Encoder/Parser/Parser.php deleted file mode 100644 index 4977d6d6..00000000 --- a/src/Encoder/Parser/Parser.php +++ /dev/null @@ -1,423 +0,0 @@ - - 'Getting Schema for a top-level resource of type `%s` failed. ' . - 'Please check you have added a Schema for this type.', - - self::MSG_GET_SCHEMA_FAILED_FOR_RESOURCE_AT_PATH => - 'Getting Schema for a resource of type `%s` at path `%s` failed. ' . - 'Please check you have added a Schema for this type.', - ]; - - /** - * @var ParserFactoryInterface - */ - protected $parserFactory; - - /** - * @var StackFactoryInterface - */ - protected $stackFactory; - - /** - * @var SchemaFactoryInterface - */ - protected $schemaFactory; - - /** - * @var StackInterface - */ - protected $stack; - - /** - * @var ParserManagerInterface - */ - protected $manager; - - /** - * @var ContainerInterface - */ - protected $container; - - /** - * @var array - */ - private $messages; - - /** - * @param ParserFactoryInterface $parserFactory - * @param StackFactoryInterface $stackFactory - * @param SchemaFactoryInterface $schemaFactory - * @param ContainerInterface $container - * @param ParserManagerInterface $manager - * @param array $messages - */ - public function __construct( - ParserFactoryInterface $parserFactory, - StackFactoryInterface $stackFactory, - SchemaFactoryInterface $schemaFactory, - ContainerInterface $container, - ParserManagerInterface $manager, - $messages = self::MESSAGES - ) { - $this->manager = $manager; - $this->container = $container; - $this->stackFactory = $stackFactory; - $this->parserFactory = $parserFactory; - $this->schemaFactory = $schemaFactory; - $this->messages = $messages; - } - - /** - * @inheritdoc - */ - public function parse($data): iterable - { - $this->stack = $this->stackFactory->createStack(); - $rootFrame = $this->stack->push(); - $rootFrame->setRelationship( - $this->schemaFactory->createRelationshipObject(null, $data, [], null, true, true) - ); - - foreach ($this->parseData() as $parseReply) { - yield $parseReply; - } - - $this->stack = null; - } - - /** - * @return iterable - * - * @SuppressWarnings(PHPMD.ElseExpression) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function parseData(): iterable - { - list($isNull, $isOriginallyArrayed, $traversableData) = $this->analyzeCurrentData(); - - /** @var bool $isNull */ - /** @var bool $isOriginallyArrayed */ - - if ($isNull === true) { - assert($traversableData === null); - yield $this->createReplyForEmptyData($traversableData); - } else { - $curFrame = $this->stack->end(); - - // if resource(s) is in primary data section (not in included) - $isPrimary = $curFrame->getLevel() < 2; - - // we need to know if there are no resources in traversable to report an empty array - // as `Traversable` do not have any methods and `empty` do not work on it we have - // to count actual resources. - $dataIsEmpty = true; - foreach ($traversableData as $resource) { - $dataIsEmpty = false; - - $schema = $this->getSchema($resource, $curFrame); - $fieldSet = $this->getFieldSet($schema->getResourceType()); - $resourceObject = $schema->createResourceObject($resource, $isOriginallyArrayed, $fieldSet); - $isCircular = $this->checkCircular($resourceObject); - - $this->stack->setCurrentResource($resourceObject); - yield $this->createReplyResourceStarted(); - - // duplicated are allowed in data however they shouldn't be in includes - if ($isCircular === true && $isPrimary === false) { - continue; - } - - if ($this->shouldParseRelationships() === true) { - $relationships = $this->getIncludeRelationships(); - $relObjectIterator = $schema->getRelationshipObjectIterator($resource, $isPrimary, $relationships); - foreach ($relObjectIterator as $relationship) { - /** @var RelationshipObjectInterface $relationship */ - $nextFrame = $this->stack->push(); - $nextFrame->setRelationship($relationship); - try { - if ($this->isRelationshipIncludedOrInFieldSet() === true) { - foreach ($this->parseData() as $parseResult) { - yield $parseResult; - } - } - } finally { - $this->stack->pop(); - } - } - } - - yield $this->createReplyResourceCompleted(); - } - - if ($dataIsEmpty === true) { - // it actually was empty traversable (such as an empty array) so report about it. - yield $this->createReplyForEmptyData([]); - } - } - } - - /** - * @return array - */ - protected function analyzeCurrentData(): array - { - $data = $this->getCurrentData(); - $result = $this->analyzeData($data); - - return $result; - } - - /** - * @return array|null|object - */ - protected function getCurrentData() - { - $relationship = $this->stack->end()->getRelationship(); - $data = $relationship->isShowData() === true ? $relationship->getData() : null; - - return $data; - } - - /** - * @param array|null|object $data - * - * @return array - * - * @SuppressWarnings(PHPMD.StaticAccess) - * @SuppressWarnings(PHPMD.ElseExpression) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function analyzeData($data): array - { - $isCollection = true; - $isNull = false; - $traversableData = null; - - $isOk = (is_array($data) === true || is_object($data) === true || $data === null); - $isOk ?: Exceptions::throwInvalidArgument('data', $data); - - if ($this->container->hasSchema($data) === true) { - $isCollection = false; - $traversableData = [$data]; - } elseif (is_array($data) === true) { - $traversableData = $data; - } elseif ($data instanceof Traversable) { - $traversableData = $data instanceof IteratorAggregate ? $data->getIterator() : $data; - } elseif (is_object($data) === true) { - // normally resources should be handled above but if Schema was not registered for $data we get here - $isCollection = false; - $traversableData = [$data]; - } elseif ($data === null) { - $isCollection = false; - $isNull = true; - } - - return [$isNull, $isCollection, $traversableData]; - } - - /** - * @param mixed $resource - * @param StackFrameReadOnlyInterface $frame - * - * @return SchemaInterface - * - * @SuppressWarnings(PHPMD.StaticAccess) - * @SuppressWarnings(PHPMD.ElseExpression) - */ - private function getSchema($resource, StackFrameReadOnlyInterface $frame): SchemaInterface - { - try { - $schema = $this->container->getSchema($resource); - } catch (InvalidArgumentException $exception) { - $path = $frame->getPath(); - $typeName = is_object($resource) === true ? get_class($resource) : gettype($resource); - if ($path === null) { - $message = _($this->messages[static::MSG_GET_SCHEMA_FAILED_FOR_RESOURCE_AT_ROOT], $typeName); - } else { - $message = _($this->messages[static::MSG_GET_SCHEMA_FAILED_FOR_RESOURCE_AT_PATH], $typeName, $path); - } - - throw new InvalidArgumentException($message, 0, $exception); - } - - return $schema; - } - - /** - * @param array|null $data - * - * @return ParserReplyInterface - * - * @SuppressWarnings(PHPMD.StaticAccess) - */ - private function createReplyForEmptyData(?array $data): ParserReplyInterface - { - ($data === null || (is_array($data) === true && empty($data) === true)) ?: Exceptions::throwLogicException(); - - $replyType = ($data === null ? ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED : - ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED); - - return $this->parserFactory->createEmptyReply($replyType, $this->stack); - } - - /** - * @return ParserReplyInterface - */ - private function createReplyResourceStarted(): ParserReplyInterface - { - return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED, $this->stack); - } - - /** - * @return ParserReplyInterface - */ - private function createReplyResourceCompleted(): ParserReplyInterface - { - return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED, $this->stack); - } - - /** - * @return bool - */ - private function shouldParseRelationships(): bool - { - return $this->manager->isShouldParseRelationships($this->stack); - } - - /** - * @return string[] - */ - private function getIncludeRelationships(): array - { - return $this->manager->getIncludeRelationships($this->stack); - } - - /** - * @return bool - */ - private function isRelationshipIncludedOrInFieldSet(): bool - { - return - $this->manager->isRelationshipInFieldSet($this->stack) === true || - $this->manager->isShouldParseRelationships($this->stack) === true; - } - - /** - * @param ResourceObjectInterface $resourceObject - * - * @return bool - */ - private function checkCircular(ResourceObjectInterface $resourceObject): bool - { - foreach ($this->stack as $frame) { - /** @var StackFrameReadOnlyInterface $frame */ - if (($stackResource = $frame->getResource()) !== null && - $stackResource->getId() === $resourceObject->getId() && - $stackResource->getType() === $resourceObject->getType()) { - return true; - } - } - - return false; - } - - /** - * @param string $resourceType - * - * @return array |null - */ - private function getFieldSet(string $resourceType): ?array - { - return $this->manager->getFieldSet($resourceType); - } -} diff --git a/src/Encoder/Parser/ParserEmptyReply.php b/src/Encoder/Parser/ParserEmptyReply.php deleted file mode 100644 index bfed0ed8..00000000 --- a/src/Encoder/Parser/ParserEmptyReply.php +++ /dev/null @@ -1,41 +0,0 @@ -fieldSetCache = []; - $this->parameterAnalyzer = $parameterAnalyzer; - } - - /** - * @inheritdoc - */ - public function isShouldParseRelationships(StackReadOnlyInterface $stack): bool - { - if ($stack->count() < 2) { - // top level, no resources ware started to parse yet - return true; - } - - // on the way to included paths - $currentPath = $stack->end()->getPath(); - $currentRootType = $stack->root()->getResource()->getType(); - - $shouldContinue = $this->parameterAnalyzer->isPathIncluded($currentPath, $currentRootType); - - return $shouldContinue; - } - - /** - * @inheritdoc - */ - public function getIncludeRelationships(StackReadOnlyInterface $stack): array - { - $currentPath = $stack->end()->getPath(); - $currentRootType = $stack->root()->getResource()->getType(); - $includePaths = $this->parameterAnalyzer->getIncludeRelationships($currentPath, $currentRootType); - - return $includePaths; - } - - /** - * @inheritdoc - */ - public function isRelationshipInFieldSet(StackReadOnlyInterface $stack): bool - { - $resourceType = $stack->penult()->getResource()->getType(); - $resourceFieldSet = $this->getFieldSet($resourceType); - - $inFieldSet = $resourceFieldSet === null ? true : array_key_exists( - $stack->end()->getRelationship()->getName(), - $resourceFieldSet - ); - - return $inFieldSet; - } - - /** - * @inheritdoc - */ - public function getFieldSet(string $type): ?array - { - if (array_key_exists($type, $this->fieldSetCache) === false) { - $fieldSet = $this->parameterAnalyzer->getParameters()->getFieldSet($type); - $this->fieldSetCache[$type] = $fieldSet === null ? null : array_flip(array_values($fieldSet)); - } - - return $this->fieldSetCache[$type]; - } -} diff --git a/src/Encoder/Parser/ParserReply.php b/src/Encoder/Parser/ParserReply.php deleted file mode 100644 index 6487161d..00000000 --- a/src/Encoder/Parser/ParserReply.php +++ /dev/null @@ -1,41 +0,0 @@ -encodeDataToArray($this->getContainer(), $data, $parameters); - } - - /** - * @param object|array|Iterator|null $data - * @param EncodingParametersInterface|null $parameters - * - * @return array - */ - public function serializeIdentifiers($data, EncodingParametersInterface $parameters = null): array - { - return $this->encodeIdentifiersToArray($data, $parameters); - } - - /** - * @param ErrorInterface $error - * - * @return array - */ - public function serializeError(ErrorInterface $error): array - { - return $this->encodeErrorToArray($error); - } - - /** - * @param ErrorInterface[]|ErrorCollection $errors - * - * @return array - */ - public function serializeErrors($errors): array - { - return $this->encodeErrorsToArray($errors); - } - - /** - * @param array|object $meta - * - * @return array - */ - public function serializeMeta($meta): array - { - return $this->encodeMetaToArray($meta); - } -} diff --git a/src/Encoder/Stack/Stack.php b/src/Encoder/Stack/Stack.php deleted file mode 100644 index 6e08d0fe..00000000 --- a/src/Encoder/Stack/Stack.php +++ /dev/null @@ -1,138 +0,0 @@ -factory = $factory; - $this->stack = []; - $this->size = 0; - } - - /** - * @inheritdoc - */ - public function push(): StackFrameInterface - { - $frame = $this->factory->createFrame($this->end()); - array_push($this->stack, $frame); - $this->size++; - return $frame; - } - - /** - * @inheritdoc - */ - public function pop(): void - { - array_pop($this->stack); - $this->size <= 0 ?: $this->size--; - } - - /** - * @inheritdoc - */ - public function root(): ?StackFrameReadOnlyInterface - { - return $this->size > 0 ? $this->stack[0] : null; - } - - /** - * @inheritdoc - */ - public function end(): ?StackFrameReadOnlyInterface - { - return $this->size > 0 ? $this->stack[$this->size - 1] : null; - } - - /** - * @inheritdoc - */ - public function penult(): ?StackFrameReadOnlyInterface - { - return $this->size > 1 ? $this->stack[$this->size - 2] : null; - } - - /** - * @inheritdoc - */ - public function count() - { - return $this->size; - } - - /** - * @inheritdoc - */ - public function getIterator() - { - return new ArrayIterator($this->stack); - } - - /** - * @inheritdoc - */ - public function setCurrentResource(ResourceObjectInterface $resource): void - { - /** @var StackFrameInterface $lastFrame */ - $lastFrame = end($this->stack); - $lastFrame->setResource($resource); - } - - /** - * @inheritdoc - */ - public function setCurrentRelationship(RelationshipObjectInterface $relationship): void - { - /** @var StackFrameInterface $lastFrame */ - $lastFrame = end($this->stack); - $lastFrame->setRelationship($relationship); - } -} diff --git a/src/Encoder/Stack/StackFrame.php b/src/Encoder/Stack/StackFrame.php deleted file mode 100644 index d37d7e1b..00000000 --- a/src/Encoder/Stack/StackFrame.php +++ /dev/null @@ -1,137 +0,0 @@ -getLevel() + 1; - - // debug check - $isOk = ($level <= 2 || ($previous !== null && $previous->getRelationship() !== null)); - $isOk ?: Exceptions::throwLogicException(); - - $this->level = $level; - $this->previous = $previous; - } - - /** - * @inheritdoc - */ - public function getLevel(): int - { - return $this->level; - } - - /** - * @inheritdoc - */ - public function setResource(ResourceObjectInterface $resource): void - { - $this->resource = $resource; - } - - /** - * @inheritdoc - */ - public function setRelationship(RelationshipObjectInterface $relationship): void - { - $this->relationship = $relationship; - - $this->setCurrentPath(); - } - - /** - * @inheritdoc - */ - public function getResource(): ?ResourceObjectInterface - { - return $this->resource; - } - - /** - * @inheritdoc - */ - public function getRelationship(): ?RelationshipObjectInterface - { - return $this->relationship; - } - - /** - * @inheritdoc - */ - public function getPath(): ?string - { - return $this->path; - } - - /** - * Set path to current frame. - * - * @SuppressWarnings(PHPMD.ElseExpression) - */ - private function setCurrentPath(): void - { - if ($this->previous === null || $this->previous->getPath() === null) { - $this->path = $this->relationship->getName(); - } else { - $this->path = - $this->previous->getPath() . DocumentInterface::PATH_SEPARATOR . $this->relationship->getName(); - } - } -} diff --git a/src/Exceptions/BaseJsonApiException.php b/src/Exceptions/BaseJsonApiException.php new file mode 100644 index 00000000..33bd6374 --- /dev/null +++ b/src/Exceptions/BaseJsonApiException.php @@ -0,0 +1,28 @@ +errors = new ErrorCollection(); - if ($errors instanceof ErrorCollection) { + $this->errors = clone $errors; + } elseif (is_iterable($errors) === true) { + $this->errors = new ErrorCollection(); $this->addErrors($errors); - } elseif (is_array($errors) === true) { - $this->addErrorsFromArray($errors); } else { // should be ErrorInterface + $this->errors = new ErrorCollection(); $this->addError($errors); } @@ -90,23 +92,11 @@ public function addError(ErrorInterface $error): void } /** - * @param ErrorCollection $errors - * - * @return void - */ - public function addErrors(ErrorCollection $errors): void - { - foreach ($errors as $error) { - $this->addError($error); - } - } - - /** - * @param ErrorInterface[] $errors + * @param iterable $errors * * @return void */ - public function addErrorsFromArray(array $errors): void + public function addErrors(iterable $errors): void { foreach ($errors as $error) { $this->addError($error); diff --git a/src/Exceptions/LogicException.php b/src/Exceptions/LogicException.php new file mode 100644 index 00000000..cdfa2fec --- /dev/null +++ b/src/Exceptions/LogicException.php @@ -0,0 +1,26 @@ +logger = new ProxyLogger(); + return new Encoder($this, $container); } /** * @inheritdoc */ - public function setLogger(LoggerInterface $logger): void + public function createSchemaContainer(iterable $schemas): SchemaContainerInterface { - $this->logger->setLogger($logger); + return new SchemaContainer($this, $schemas); } /** * @inheritdoc */ - public function createEncoder( - ContainerInterface $container, - EncoderOptions $encoderOptions = null - ): EncoderInterface { - $encoder = new Encoder($this, $container, $encoderOptions); - - $encoder->setLogger($this->logger); - - return $encoder; + public function createPosition( + int $level, + string $path, + ?string $parentType, + ?string $parentRelationship + ): PositionInterface { + return new class ($level, $path, $parentType, $parentRelationship) implements PositionInterface + { + /** + * @var int + */ + private $level; + + /** + * @var string + */ + private $path; + + /** + * @var null|string + */ + private $parentType; + + /** + * @var null|string + */ + private $parentRelationship; + + /** + * @param int $level + * @param string $path + * @param null|string $parentType + * @param null|string $parentRelationship + */ + public function __construct(int $level, string $path, ?string $parentType, ?string $parentRelationship) + { + $this->level = $level; + $this->path = $path; + $this->parentType = $parentType; + $this->parentRelationship = $parentRelationship; + } + + /** + * @inheritdoc + */ + public function getLevel(): int + { + return $this->level; + } + + /** + * @inheritdoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritdoc + */ + public function getParentType(): ?string + { + return $this->parentType; + } + + /** + * @inheritdoc + */ + public function getParentRelationship(): ?string + { + return $this->parentRelationship; + } + }; } /** * @inheritdoc */ - public function createDocument(): DocumentInterface + public function createParser(SchemaContainerInterface $container): ParserInterface { - $document = new Document(); - - $document->setLogger($this->logger); - - return $document; + return new Parser($this, $container); } /** * @inheritdoc */ - public function createError( - string $idx = null, - LinkInterface $aboutLink = null, - string $status = null, - string $code = null, - string $title = null, - string $detail = null, - array $source = null, - array $meta = null - ): ErrorInterface { - return new Error($idx, $aboutLink, $status, $code, $title, $detail, $source, $meta); + public function createDocumentWriter(): DocumentWriterInterface + { + return new DocumentWriter(); } /** * @inheritdoc */ - public function createReply(int $replyType, StackReadOnlyInterface $stack): ParserReplyInterface + public function createErrorWriter(): ErrorWriterInterface { - return new ParserReply($replyType, $stack); + return new ErrorWriter(); } /** * @inheritdoc */ - public function createEmptyReply(int $replyType, StackReadOnlyInterface $stack): ParserReplyInterface + public function createFieldSetFilter(array $fieldSets): FieldSetFilterInterface { - return new ParserEmptyReply($replyType, $stack); + return new FieldSetFilter($fieldSets); } /** * @inheritdoc */ - public function createParser(ContainerInterface $container, ParserManagerInterface $manager): ParserInterface - { - $parser = new Parser($this, $this, $this, $container, $manager); - - $parser->setLogger($this->logger); - - return $parser; + public function createParsedResource( + PositionInterface $position, + SchemaContainerInterface $container, + $data + ): ResourceInterface { + return new IdentifierAndResource($position, $this, $container, $data); } /** * @inheritdoc */ - public function createManager(ParametersAnalyzerInterface $parameterAnalyzer): ParserManagerInterface - { - $manager = new ParserManager($parameterAnalyzer); - - $manager->setLogger($this->logger); - - return $manager; + public function createParsedIdentifier( + PositionInterface $position, + SchemaIdentifierInterface $identifier + ): ParserIdentifierInterface { + return new class ($position, $identifier) implements ParserIdentifierInterface + { + /** + * @var PositionInterface + */ + private $position; + + /** + * @var SchemaIdentifierInterface + */ + private $identifier; + + /** + * @param PositionInterface $position + * @param SchemaIdentifierInterface $identifier + */ + public function __construct( + PositionInterface $position, + SchemaIdentifierInterface $identifier + ) { + $this->position = $position; + $this->identifier = $identifier; + + // for test coverage only + assert($this->getPosition() !== null); + } + + /** + * @inheritdoc + */ + public function getType(): string + { + return $this->identifier->getType(); + } + + /** + * @inheritdoc + */ + public function getId(): ?string + { + return $this->identifier->getId(); + } + + /** + * @inheritdoc + */ + public function hasIdentifierMeta(): bool + { + return $this->identifier->hasIdentifierMeta(); + } + + /** + * @inheritdoc + */ + public function getIdentifierMeta() + { + return $this->identifier->getIdentifierMeta(); + } + + /** + * @inheritdoc + */ + public function getPosition(): PositionInterface + { + return $this->position; + } + }; } /** * @inheritdoc */ - public function createFrame(StackFrameReadOnlyInterface $previous = null): StackFrameInterface + public function createLink(bool $isSubUrl, string $value, bool $hasMeta, $meta = null): LinkInterface { - return new StackFrame($previous); + return new Link($isSubUrl, $value, $hasMeta, $meta); } /** * @inheritdoc */ - public function createStack(): StackInterface - { - return new Stack($this); + public function createRelationship( + PositionInterface $position, + bool $hasData, + ?RelationshipDataInterface $data, + bool $hasLinks, + ?iterable $links, + bool $hasMeta, + $meta + ): RelationshipInterface { + return new class ( + $position, + $hasData, + $data, + $hasLinks, + $links, + $hasMeta, + $meta + ) implements RelationshipInterface + { + /** + * @var PositionInterface + */ + private $position; + + /** + * @var bool + */ + private $hasData; + + /** + * @var ?RelationshipDataInterface + */ + private $data; + + /** + * @var bool + */ + private $hasLinks; + + /** + * @var ?iterable + */ + private $links; + + /** + * @var bool + */ + private $hasMeta; + + /** + * @var mixed + */ + private $meta; + + /** + * @var bool + */ + private $metaIsCallable; + + /** + * @param PositionInterface $position + * @param bool $hasData + * @param RelationshipDataInterface|null $data + * @param bool $hasLinks + * @param iterable|null $links + * @param bool $hasMeta + * @param mixed $meta + */ + public function __construct( + PositionInterface $position, + bool $hasData, + ?RelationshipDataInterface $data, + bool $hasLinks, + ?iterable $links, + bool $hasMeta, + $meta + ) { + assert($position->getLevel() > ParserInterface::ROOT_LEVEL); + assert(empty($position->getPath()) === false); + assert(($hasData === false && $data === null) || ($hasData === true && $data !== null)); + assert(($hasLinks === false && $links === null) || ($hasLinks === true && $links !== null)); + + $this->position = $position; + $this->hasData = $hasData; + $this->data = $data; + $this->hasLinks = $hasLinks; + $this->links = $links; + $this->hasMeta = $hasMeta; + $this->meta = $meta; + $this->metaIsCallable = is_callable($meta); + } + + /** + * @inheritdoc + */ + public function getPosition(): PositionInterface + { + return $this->position; + } + + /** + * @inheritdoc + */ + public function hasData(): bool + { + return $this->hasData; + } + + /** + * @inheritdoc + */ + public function getData(): RelationshipDataInterface + { + assert($this->hasData()); + + return $this->data; + } + + /** + * @inheritdoc + */ + public function hasLinks(): bool + { + return $this->hasLinks; + } + + /** + * @inheritdoc + */ + public function getLinks(): iterable + { + assert($this->hasLinks()); + + return $this->links; + } + + /** + * @inheritdoc + */ + public function hasMeta(): bool + { + return $this->hasMeta; + } + + /** + * @inheritdoc + */ + public function getMeta() + { + assert($this->hasMeta()); + + if ($this->metaIsCallable === true) { + $this->meta = call_user_func($this->meta); + $this->metaIsCallable = false; + } + + return $this->meta; + } + }; } /** * @inheritdoc */ - public function createReplyInterpreter( - DocumentInterface $document, - ParametersAnalyzerInterface $parameterAnalyzer - ): ReplyInterpreterInterface { - $interpreter = new ReplyInterpreter($document, $parameterAnalyzer); - - $interpreter->setLogger($this->logger); - - return $interpreter; + public function createRelationshipDataIsResource( + SchemaContainerInterface $schemaContainer, + PositionInterface $position, + $resource + ): RelationshipDataInterface { + return new RelationshipDataIsResource($this, $schemaContainer, $position, $resource); } /** * @inheritdoc */ - public function createParametersAnalyzer( - EncodingParametersInterface $parameters, - ContainerInterface $container - ): ParametersAnalyzerInterface { - $analyzer = new ParametersAnalyzer($parameters, $container); - - $analyzer->setLogger($this->logger); - - return $analyzer; + public function createRelationshipDataIsIdentifier( + SchemaContainerInterface $schemaContainer, + PositionInterface $position, + SchemaIdentifierInterface $identifier + ): RelationshipDataInterface { + return new RelationshipDataIsIdentifier($this, $schemaContainer, $position, $identifier); } /** * @inheritdoc */ - public function createMediaType(string $type, string $subType, array $parameters = null): MediaTypeInterface - { - return new MediaType($type, $subType, $parameters); + public function createRelationshipDataIsCollection( + SchemaContainerInterface $schemaContainer, + PositionInterface $position, + iterable $resources + ): RelationshipDataInterface { + return new RelationshipDataIsCollection($this, $schemaContainer, $position, $resources); } /** * @inheritdoc */ - public function createQueryParameters( - array $includePaths = null, - array $fieldSets = null - ): EncodingParametersInterface { - return new EncodingParameters($includePaths, $fieldSets); + public function createRelationshipDataIsNull(): RelationshipDataInterface + { + return new RelationshipDataIsNull(); } /** * @inheritdoc */ - public function createHeaderParametersParser(): HeaderParametersParserInterface + public function createMediaType(string $type, string $subType, array $parameters = null): MediaTypeInterface { - $parser = new HeaderParametersParser($this); - - $parser->setLogger($this->logger); - - return $parser; + return new MediaType($type, $subType, $parameters); } /** @@ -262,79 +501,4 @@ public function createAcceptMediaType( ): AcceptMediaTypeInterface { return new AcceptMediaType($position, $type, $subType, $parameters, $quality); } - - /** - * @inheritdoc - */ - public function createContainer(array $providers = []): ContainerInterface - { - $container = new Container($this, $providers); - - $container->setLogger($this->logger); - - return $container; - } - - /** - * @inheritdoc - */ - public function createResourceObject( - SchemaInterface $schema, - $resource, - bool $isInArray, - array $fieldKeysFilter = null - ): ResourceObjectInterface { - return new ResourceObject($schema, $resource, $isInArray, $fieldKeysFilter); - } - - /** - * @inheritdoc - */ - public function createRelationshipObject( - ?string $name, - $data, - array $links, - $meta, - bool $isShowData, - bool $isRoot - ): RelationshipObjectInterface { - return new RelationshipObject($name, $data, $links, $meta, $isShowData, $isRoot); - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function createLink(string $subHref, $meta = null, bool $treatAsHref = false): LinkInterface - { - return new Link($subHref, $meta, $treatAsHref); - } - - /** - * @inheritdoc - */ - public function createResourceIdentifierSchemaAdapter(SchemaInterface $schema): SchemaInterface - { - return new ResourceIdentifierSchemaAdapter($this, $schema); - } - - /** - * @inheritdoc - */ - public function createResourceIdentifierContainerAdapter(ContainerInterface $container): ContainerInterface - { - return new ResourceIdentifierContainerAdapter($this, $container); - } - - /** - * @inheritdoc - */ - public function createIdentitySchema( - ContainerInterface $container, - string $classType, - Closure $identityClosure - ): SchemaInterface { - return new IdentitySchema($this, $container, $classType, $identityClosure); - } } diff --git a/src/Http/BaseResponses.php b/src/Http/BaseResponses.php index 445fb0be..7487e464 100644 --- a/src/Http/BaseResponses.php +++ b/src/Http/BaseResponses.php @@ -1,7 +1,9 @@ -getEncoder(); - $links === null ?: $encoder->withLinks($links); - $meta === null ?: $encoder->withMeta($meta); - $content = $encoder->encodeData($data, $this->getEncodingParameters()); + public function getContentResponse($data, int $statusCode = self::HTTP_OK, array $headers = []) + { + $content = $this->getEncoder()->encodeData($data); return $this->createJsonApiResponse($content, $statusCode, $headers, true); } @@ -93,13 +69,10 @@ public function getContentResponse( /** * @inheritdoc */ - public function getCreatedResponse($resource, array $links = null, $meta = null, array $headers = []) + public function getCreatedResponse($resource, string $url, array $headers = []) { - $encoder = $this->getEncoder(); - $links === null ?: $encoder->withLinks($links); - $meta === null ?: $encoder->withMeta($meta); - $content = $encoder->encodeData($resource, $this->getEncodingParameters()); - $headers[self::HEADER_LOCATION] = $this->getResourceLocationUrl($resource); + $content = $this->getEncoder()->encodeData($resource); + $headers[self::HEADER_LOCATION] = $url; return $this->createJsonApiResponse($content, self::HTTP_CREATED, $headers, true); } @@ -117,8 +90,7 @@ public function getCodeResponse(int $statusCode, array $headers = []) */ public function getMetaResponse($meta, int $statusCode = self::HTTP_OK, array $headers = []) { - $encoder = $this->getEncoder(); - $content = $encoder->encodeMeta($meta); + $content = $this->getEncoder()->encodeMeta($meta); return $this->createJsonApiResponse($content, $statusCode, $headers, true); } @@ -126,17 +98,9 @@ public function getMetaResponse($meta, int $statusCode = self::HTTP_OK, array $h /** * @inheritDoc */ - public function getIdentifiersResponse( - $data, - int $statusCode = self::HTTP_OK, - array $links = null, - $meta = null, - array $headers = [] - ) { - $encoder = $this->getEncoder(); - $links === null ?: $encoder->withLinks($links); - $meta === null ?: $encoder->withMeta($meta); - $content = $encoder->encodeIdentifiers($data, $this->getEncodingParameters()); + public function getIdentifiersResponse($data, int $statusCode = self::HTTP_OK, array $headers = []) + { + $content = $this->getEncoder()->encodeIdentifiers($data); return $this->createJsonApiResponse($content, $statusCode, $headers, true); } @@ -148,31 +112,17 @@ public function getIdentifiersResponse( */ public function getErrorResponse($errors, int $statusCode = self::HTTP_BAD_REQUEST, array $headers = []) { - if ($errors instanceof ErrorCollection || is_array($errors) === true) { - /** @var ErrorInterface[] $errors */ + if (is_iterable($errors) === true) { + /** @var iterable $errors */ $content = $this->getEncoder()->encodeErrors($errors); } else { - /** @var ErrorInterface $errors */ + assert($errors instanceof ErrorInterface); $content = $this->getEncoder()->encodeError($errors); } return $this->createJsonApiResponse($content, $statusCode, $headers, true); } - /** - * @param mixed $resource - * - * @return string - */ - protected function getResourceLocationUrl($resource): string - { - $resSubUrl = $this->getSchemaContainer()->getSchema($resource)->getSelfSubLink($resource)->getSubHref(); - $urlPrefix = $this->getUrlPrefix(); - $location = $urlPrefix . $resSubUrl; - - return $location; - } - /** * @param string|null $content * @param int $statusCode diff --git a/src/Http/Headers/AcceptMediaType.php b/src/Http/Headers/AcceptMediaType.php index d6c7755e..692c137d 100644 --- a/src/Http/Headers/AcceptMediaType.php +++ b/src/Http/Headers/AcceptMediaType.php @@ -1,7 +1,9 @@ -factory = $factory; } @@ -53,7 +51,8 @@ public function parseAcceptHeader(string $value): iterable } $ranges = preg_split("/,(?=([^\"]*\"[^\"]*\")*[^\"]*$)/", $value); - for ($idx = 0; $idx < count($ranges); ++$idx) { + $count = count($ranges); + for ($idx = 0; $idx < $count; ++$idx) { $fields = explode(';', $ranges[$idx]); if (strpos($fields[0], '/') === false) { diff --git a/src/Http/Headers/MediaType.php b/src/Http/Headers/MediaType.php index bde5e21a..73f8a323 100644 --- a/src/Http/Headers/MediaType.php +++ b/src/Http/Headers/MediaType.php @@ -1,7 +1,9 @@ -getSortsImpl($this->getParameters(), $this->getMessage(static::MSG_ERR_INVALID_PARAMETER)); } + /** + * @inheritdoc + */ + public function getProfileUrls(): iterable + { + return $this->getProfileUrlsImpl($this->getParameters(), $this->getMessage(static::MSG_ERR_INVALID_PARAMETER)); + } + /** * @param array $parameters * diff --git a/src/Http/Query/BaseQueryParserTrait.php b/src/Http/Query/BaseQueryParserTrait.php index 27ec3f24..165a1855 100644 --- a/src/Http/Query/BaseQueryParserTrait.php +++ b/src/Http/Query/BaseQueryParserTrait.php @@ -1,7 +1,9 @@ -splitStringAndCheckNoEmpties(P::PARAM_INCLUDE, $path, '.', $errorTitle) as $link) { - yield $link; - } - }; - $paramName = P::PARAM_INCLUDE; $includes = $parameters[P::PARAM_INCLUDE]; foreach ($this->splitCommaSeparatedStringAndCheckNoEmpties($paramName, $includes, $errorTitle) as $path) { - yield $path => $splitByDot($path); + yield $path => $this->splitStringAndCheckNoEmpties(P::PARAM_INCLUDE, $path, '.', $errorTitle); } } } @@ -101,6 +97,25 @@ protected function getSorts(array $parameters, string $errorTitle): iterable } } + /** + * @param array $parameters + * @param string $errorTitle + * + * @return iterable + */ + protected function getProfileUrls(array $parameters, string $errorTitle): iterable + { + if (array_key_exists(P::PARAM_PROFILE, $parameters) === true) { + $encodedUrls = $parameters[P::PARAM_PROFILE]; + $decodedUrls = urldecode($encodedUrls); + yield from $this->splitSpaceSeparatedStringAndCheckNoEmpties( + P::PARAM_PROFILE, + $decodedUrls, + $errorTitle + ); + } + } + /** * @param string $paramName * @param string|mixed $shouldBeString @@ -116,6 +131,21 @@ private function splitCommaSeparatedStringAndCheckNoEmpties( return $this->splitStringAndCheckNoEmpties($paramName, $shouldBeString, ',', $errorTitle); } + /** + * @param string $paramName + * @param string|mixed $shouldBeString + * @param string $errorTitle + * + * @return iterable + */ + private function splitSpaceSeparatedStringAndCheckNoEmpties( + string $paramName, + $shouldBeString, + string $errorTitle + ): iterable { + return $this->splitStringAndCheckNoEmpties($paramName, $shouldBeString, ' ', $errorTitle); + } + /** * @param string $paramName * @param string|mixed $shouldBeString @@ -136,7 +166,7 @@ private function splitStringAndCheckNoEmpties( foreach (explode($separator, $trimmed) as $value) { $trimmedValue = trim($value); - if (($trimmedValue) === '') { + if ($trimmedValue === '') { throw new JsonApiException($this->createParameterError($paramName, $errorTitle)); } @@ -153,7 +183,7 @@ private function splitStringAndCheckNoEmpties( private function createParameterError(string $paramName, string $errorTitle): ErrorInterface { $source = [Error::SOURCE_PARAMETER => $paramName]; - $error = new Error(null, null, null, null, $errorTitle, null, $source); + $error = new Error(null, null, null, null, null, $errorTitle, null, $source); return $error; } diff --git a/src/I18n/Messages.php b/src/I18n/Messages.php new file mode 100644 index 00000000..53ccb201 --- /dev/null +++ b/src/I18n/Messages.php @@ -0,0 +1,70 @@ +getSchema($data); + $this + ->setPosition($position) + ->setFactory($factory) + ->setSchemaContainer($container) + ->setSchema($schema) + ->setData($data); + } + + /** + * @inheritdoc + */ + public function getPosition(): PositionInterface + { + return $this->position; + } + + /** + * @inheritdoc + */ + public function getId(): ?string + { + return $this->index; + } + + /** + * @inheritdoc + */ + public function getType(): string + { + return $this->type; + } + + /** + * @inheritdoc + */ + public function hasIdentifierMeta(): bool + { + return $this->getSchema()->hasIdentifierMeta($this->getData()); + } + + /** + * @inheritdoc + */ + public function getIdentifierMeta() + { + return $this->getSchema()->getIdentifierMeta($this->getData()); + } + + /** + * @inheritdoc + */ + public function getAttributes(): iterable + { + return $this->getSchema()->getAttributes($this->getData()); + } + + /** + * @inheritdoc + */ + public function getRelationships(): iterable + { + if ($this->relationshipsCache !== null) { + yield from $this->relationshipsCache; + + return; + } + + $this->relationshipsCache = []; + + $currentPath = $this->getPosition()->getPath(); + $nextLevel = $this->getPosition()->getLevel() + 1; + $nextPathPrefix = empty($currentPath) === true ? '' : $currentPath . PositionInterface::PATH_SEPARATOR; + foreach ($this->getSchema()->getRelationships($this->getData()) as $name => $description) { + assert($this->assertRelationshipNameAndDescription($name, $description) === true); + + [$hasData, $relationshipData, $nextPosition] = $this->parseRelationshipData( + $this->getFactory(), + $this->getSchemaContainer(), + $this->getType(), + $name, + $description, + $nextLevel, + $nextPathPrefix + ); + + [$hasLinks, $links] = + $this->parseRelationshipLinks($this->getSchema(), $this->getData(), $name, $description); + + $hasMeta = array_key_exists(SchemaInterface::RELATIONSHIP_META, $description); + $meta = $hasMeta === true ? $description[SchemaInterface::RELATIONSHIP_META] : null; + + assert( + $hasData || $hasMeta || $hasLinks, + "Relationship `$name` for type `" . $this->getType() . + '` MUST contain at least one of the following: links, data or meta.' + ); + + $relationship = $this->getFactory()->createRelationship( + $nextPosition, + $hasData, + $relationshipData, + $hasLinks, + $links, + $hasMeta, + $meta + ); + + $this->relationshipsCache[$name] = $relationship; + + yield $name => $relationship; + } + } + + /** + * @inheritdoc + */ + public function hasLinks(): bool + { + $this->cacheLinks(); + + return empty($this->links) === false; + } + + /** + * @inheritdoc + */ + public function getLinks(): iterable + { + $this->cacheLinks(); + + return $this->links; + } + + /** + * @inheritdoc + */ + public function hasResourceMeta(): bool + { + return $this->getSchema()->hasResourceMeta($this->getData()); + } + + /** + * @inheritdoc + */ + public function getResourceMeta() + { + return $this->getSchema()->getResourceMeta($this->getData()); + } + + /** + * @inheritdoc + */ + protected function setPosition(PositionInterface $position): self + { + assert($position->getLevel() >= ParserInterface::ROOT_LEVEL); + + $this->position = $position; + + return $this; + } + + /** + * @return FactoryInterface + */ + protected function getFactory(): FactoryInterface + { + return $this->factory; + } + + /** + * @param FactoryInterface $factory + * + * @return self + */ + protected function setFactory(FactoryInterface $factory): self + { + $this->factory = $factory; + + return $this; + } + + /** + * @return SchemaContainerInterface + */ + protected function getSchemaContainer(): SchemaContainerInterface + { + return $this->schemaContainer; + } + + /** + * @param SchemaContainerInterface $container + * + * @return self + */ + protected function setSchemaContainer(SchemaContainerInterface $container): self + { + $this->schemaContainer = $container; + + return $this; + } + + /** + * @return SchemaInterface + */ + protected function getSchema(): SchemaInterface + { + return $this->schema; + } + + /** + * @param SchemaInterface $schema + * + * @return self + */ + protected function setSchema(SchemaInterface $schema): self + { + $this->schema = $schema; + + return $this; + } + + /** + * @return mixed + */ + protected function getData() + { + return $this->data; + } + + /** + * @param mixed $data + * + * @return self + */ + protected function setData($data): self + { + $this->data = $data; + $this->index = $this->getSchema()->getId($data); + $this->type = $this->getSchema()->getType(); + + return $this; + } + + /** + * Read and parse links from schema. + */ + private function cacheLinks(): void + { + if ($this->links === null) { + $this->links = []; + foreach ($this->getSchema()->getLinks($this->getData()) as $name => $link) { + assert(is_string($name) === true && empty($name) === false); + assert($link instanceof LinkInterface); + $this->links[$name] = $link; + } + } + } + + /** + * @param string $name + * @param array $description + * + * @return bool + */ + private function assertRelationshipNameAndDescription(string $name, array $description): bool + { + assert( + is_string($name) === true && empty($name) === false, + "Relationship names for type `" . $this->getType() . '` should be non-empty strings.' + ); + assert( + is_array($description) === true && empty($description) === false, + "Relationship `$name` for type `" . $this->getType() . '` should be a non-empty array.' + ); + + return true; + } +} diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php new file mode 100644 index 00000000..f38f4b0f --- /dev/null +++ b/src/Parser/Parser.php @@ -0,0 +1,505 @@ +resourcesTracker = []; + + $this->setFactory($factory)->setSchemaContainer($container); + } + + /** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.ElseExpression) + */ + public function parse($data, array $paths = []): iterable + { + assert(is_array($data) === true || is_object($data) === true || $data === null); + + $this->paths = $this->normalizePaths($paths); + + $rootPosition = $this->getFactory()->createPosition( + ParserInterface::ROOT_LEVEL, + ParserInterface::ROOT_PATH, + null, + null + ); + + if ($this->getSchemaContainer()->hasSchema($data) === true) { + yield $this->createDocumentDataIsResource($rootPosition); + yield from $this->parseAsResource($rootPosition, $data); + } elseif ($data instanceof SchemaIdentifierInterface) { + yield $this->createDocumentDataIsIdentifier($rootPosition); + yield $this->parseAsIdentifier($rootPosition, $data); + } elseif (is_array($data) === true) { + yield $this->createDocumentDataIsCollection($rootPosition); + yield from $this->parseAsResourcesOrIdentifiers($rootPosition, $data); + } elseif ($data instanceof Traversable) { + $data = $data instanceof IteratorAggregate ? $data->getIterator() : $data; + yield $this->createDocumentDataIsCollection($rootPosition); + yield from $this->parseAsResourcesOrIdentifiers($rootPosition, $data); + } elseif ($data === null) { + yield $this->createDocumentDataIsNull($rootPosition); + } else { + throw new InvalidArgumentException(_(static::MSG_NO_SCHEMA_FOUND, get_class($data))); + } + } + + /** + * @return SchemaContainerInterface + */ + protected function getSchemaContainer(): SchemaContainerInterface + { + return $this->schemaContainer; + } + + /** + * @param SchemaContainerInterface $container + * + * @return self + */ + protected function setSchemaContainer(SchemaContainerInterface $container): self + { + $this->schemaContainer = $container; + + return $this; + } + + /** + * @return FactoryInterface + */ + protected function getFactory(): FactoryInterface + { + return $this->factory; + } + + /** + * @param FactoryInterface $factory + * + * @return self + */ + protected function setFactory(FactoryInterface $factory): self + { + $this->factory = $factory; + + return $this; + } + + /** + * @param ResourceInterface $resource + * + * @return void + */ + private function rememberResource(ResourceInterface $resource): void + { + $this->resourcesTracker[$resource->getId()][$resource->getType()] = true; + } + + /** + * @param ResourceInterface $resource + * + * @return bool + */ + private function hasSeenResourceBefore(ResourceInterface $resource): bool + { + return isset($this->resourcesTracker[$resource->getId()][$resource->getType()]); + } + + /** + * @param PositionInterface $position + * @param iterable $dataOrIds + * + * @see ResourceInterface + * @see IdentifierInterface + * + * @return iterable + */ + private function parseAsResourcesOrIdentifiers( + PositionInterface $position, + iterable $dataOrIds + ): iterable { + foreach ($dataOrIds as $dataOrId) { + if ($this->getSchemaContainer()->hasSchema($dataOrId) === true) { + yield from $this->parseAsResource($position, $dataOrId); + + continue; + } + + assert($dataOrId instanceof SchemaIdentifierInterface); + yield $this->parseAsIdentifier($position, $dataOrId); + } + } + + /** + * @param PositionInterface $position + * @param mixed $data + * + * @see ResourceInterface + * + * @return iterable + * + */ + private function parseAsResource( + PositionInterface $position, + $data + ): iterable { + assert($this->getSchemaContainer()->hasSchema($data) === true); + + $resource = $this->getFactory()->createParsedResource( + $position, + $this->getSchemaContainer(), + $data + ); + + yield from $this->parseResource($resource); + } + + /** + * @param ResourceInterface $resource + * + * @return iterable + */ + private function parseResource(ResourceInterface $resource): iterable + { + $seenBefore = $this->hasSeenResourceBefore($resource); + + // top level resources should be yielded in any case as it could be an array of the resources + // for deeper levels it's not needed as they go to `included` section and it must have no more + // than one instance of the same resource. + + if ($resource->getPosition()->getLevel() <= ParserInterface::ROOT_LEVEL || $seenBefore === false) { + yield $resource; + } + + // parse relationships only for resources not seen before (prevents infinite loop for circular references) + if ($seenBefore === false) { + $this->rememberResource($resource); + + foreach ($resource->getRelationships() as $name => $relationship) { + assert(is_string($name)); + assert($relationship instanceof RelationshipInterface); + + $isShouldParse = $this->isPathRequested($relationship->getPosition()->getPath()); + + if ($relationship->hasData() === true && $isShouldParse === true) { + $relData = $relationship->getData(); + if ($relData->isResource() === true) { + yield from $this->parseResource($relData->getResource()); + + continue; + } elseif ($relData->isCollection() === true) { + foreach ($relData->getResources() as $relResource) { + assert($relResource instanceof ResourceInterface); + yield from $this->parseResource($relResource); + } + + continue; + } + + assert($relData->isNull() || $relData->isIdentifier()); + } + } + } + } + + /** + * @param PositionInterface $position + * @param SchemaIdentifierInterface $identifier + * + * @return IdentifierInterface + */ + private function parseAsIdentifier( + PositionInterface $position, + SchemaIdentifierInterface $identifier + ): IdentifierInterface { + return new class ($position, $identifier) implements IdentifierInterface + { + /** + * @var PositionInterface + */ + private $position; + + /** + * @var SchemaIdentifierInterface + */ + private $identifier; + + /** + * @param PositionInterface $position + * @param SchemaIdentifierInterface $identifier + */ + public function __construct(PositionInterface $position, SchemaIdentifierInterface $identifier) + { + $this->position = $position; + $this->identifier = $identifier; + } + + /** + * @inheritdoc + */ + public function getPosition(): PositionInterface + { + return $this->position; + } + + /** + * @inheritdoc + */ + public function getId(): ?string + { + return $this->getIdentifier()->getId(); + } + + /** + * @inheritdoc + */ + public function getType(): string + { + return $this->getIdentifier()->getType(); + } + + /** + * @inheritdoc + */ + public function hasIdentifierMeta(): bool + { + return $this->getIdentifier()->hasIdentifierMeta(); + } + + /** + * @inheritdoc + */ + public function getIdentifierMeta() + { + return $this->getIdentifier()->getIdentifierMeta(); + } + + /** + * @return SchemaIdentifierInterface + */ + private function getIdentifier(): SchemaIdentifierInterface + { + return $this->identifier; + } + }; + } + + /** + * @param PositionInterface $position + * + * @return DocumentDataInterface + */ + private function createDocumentDataIsCollection(PositionInterface $position): DocumentDataInterface + { + return $this->createParsedDocumentData($position, true, false); + } + + /** + * @param PositionInterface $position + * + * @return DocumentDataInterface + */ + private function createDocumentDataIsNull(PositionInterface $position): DocumentDataInterface + { + return $this->createParsedDocumentData($position, false, true); + } + + /** + * @param PositionInterface $position + * + * @return DocumentDataInterface + */ + private function createDocumentDataIsResource(PositionInterface $position): DocumentDataInterface + { + return $this->createParsedDocumentData($position, false, false); + } + + /** + * @param PositionInterface $position + * + * @return DocumentDataInterface + */ + private function createDocumentDataIsIdentifier(PositionInterface $position): DocumentDataInterface + { + return $this->createParsedDocumentData($position, false, false); + } + + /** + * @param PositionInterface $position + * @param bool $isCollection + * @param bool $isNull + * + * @return DocumentDataInterface + */ + private function createParsedDocumentData( + PositionInterface $position, + bool $isCollection, + bool $isNull + ): DocumentDataInterface { + return new class ( + $position, + $isCollection, + $isNull + ) implements DocumentDataInterface + { + /** + * @var PositionInterface + */ + private $position; + /** + * @var bool + */ + private $isCollection; + + /** + * @var bool + */ + private $isNull; + + /** + * @param PositionInterface $position + * @param bool $isCollection + * @param bool $isNull + */ + public function __construct( + PositionInterface $position, + bool $isCollection, + bool $isNull + ) { + $this->position = $position; + $this->isCollection = $isCollection; + $this->isNull = $isNull; + } + + /** + * @inheritdoc + */ + public function getPosition(): PositionInterface + { + return $this->position; + } + + /** + * @inheritdoc + */ + public function isCollection(): bool + { + return $this->isCollection; + } + + /** + * @inheritdoc + */ + public function isNull(): bool + { + return $this->isNull; + } + }; + } + + /** + * @param string $path + * + * @return bool + */ + private function isPathRequested(string $path): bool + { + return array_key_exists($path, $this->paths); + } + + /** + * @param iterable $paths + * + * @return array + */ + private function normalizePaths(iterable $paths): array + { + $separator = DocumentInterface::PATH_SEPARATOR; + + // convert paths like a.b.c to paths that actually should be used a, a.b, a.b.c + $normalizedPaths = []; + foreach ($paths as $path) { + $curPath = ''; + foreach (explode($separator, $path) as $pathPart) { + $curPath = empty($curPath) === true ? $pathPart : $curPath . $separator . $pathPart; + $normalizedPaths[$curPath] = true; + } + } + + return $normalizedPaths; + } +} diff --git a/src/Parser/RelationshipData/BaseRelationshipData.php b/src/Parser/RelationshipData/BaseRelationshipData.php new file mode 100644 index 00000000..55478866 --- /dev/null +++ b/src/Parser/RelationshipData/BaseRelationshipData.php @@ -0,0 +1,92 @@ +factory = $factory; + $this->schemaContainer = $schemaContainer; + $this->position = $position; + } + + /** + * @param mixed $resource + * + * @return ResourceInterface + */ + protected function createParsedResource($resource): ResourceInterface + { + assert( + $this->schemaContainer->hasSchema($resource), + 'No Schema found for resource `' . get_class($resource) . '`.' + ); + + return $this->factory->createParsedResource( + $this->position, + $this->schemaContainer, + $resource + ); + } + + /** + * @param SchemaIdentifierInterface $identifier + * + * @return ResourceInterface + */ + protected function createParsedIdentifier(SchemaIdentifierInterface $identifier): ParserIdentifierInterface + { + return $this->factory->createParsedIdentifier($this->position, $identifier); + } +} diff --git a/src/Parser/RelationshipData/ParseRelationshipDataTrait.php b/src/Parser/RelationshipData/ParseRelationshipDataTrait.php new file mode 100644 index 00000000..43ff1037 --- /dev/null +++ b/src/Parser/RelationshipData/ParseRelationshipDataTrait.php @@ -0,0 +1,125 @@ +createPosition( + $nextLevel, + $nextPathPrefix . $name, + $parentType, + $name + ); + + $relationshipData = $hasData === true ? $this->parseData( + $factory, + $container, + $nextPosition, + $description[SchemaInterface::RELATIONSHIP_DATA] + ) : null; + + return [$hasData, $relationshipData, $nextPosition]; + } + + /** + * @param FactoryInterface $factory + * @param SchemaContainerInterface $container + * @param PositionInterface $position + * @param mixed $data + * + * @return RelationshipDataInterface + */ + private function parseData( + FactoryInterface $factory, + SchemaContainerInterface $container, + PositionInterface $position, + $data + ): RelationshipDataInterface { + // support if data is callable (e.g. a closure used to postpone actual data reading) + if (is_callable($data) === true) { + $data = call_user_func($data); + } + + if ($container->hasSchema($data) === true) { + return $factory->createRelationshipDataIsResource($container, $position, $data); + } elseif ($data instanceof IdentifierInterface) { + return $factory->createRelationshipDataIsIdentifier($container, $position, $data); + } elseif (is_array($data) === true) { + return $factory->createRelationshipDataIsCollection($container, $position, $data); + } elseif ($data instanceof Traversable) { + return $factory->createRelationshipDataIsCollection( + $container, + $position, + $data instanceof IteratorAggregate ? $data->getIterator() : $data + ); + } elseif ($data === null) { + return $factory->createRelationshipDataIsNull(); + } + + throw new InvalidArgumentException( + _(IdentifierAndResource::MSG_NO_SCHEMA_FOUND, get_class($data), $position->getPath()) + ); + } +} diff --git a/src/Parser/RelationshipData/ParseRelationshipLinksTrait.php b/src/Parser/RelationshipData/ParseRelationshipLinksTrait.php new file mode 100644 index 00000000..7aeb64b6 --- /dev/null +++ b/src/Parser/RelationshipData/ParseRelationshipLinksTrait.php @@ -0,0 +1,129 @@ +isAddSelfLinkInRelationshipByDefault(); + $addRelatedLink = $description[SchemaInterface::RELATIONSHIP_LINKS_RELATED] ?? + $parentSchema->isAddRelatedLinkInRelationshipByDefault(); + assert(is_bool($addSelfLink) === true || $addSelfLink instanceof LinkInterface); + assert(is_bool($addRelatedLink) === true || $addRelatedLink instanceof LinkInterface); + + $schemaLinks = array_key_exists(SchemaInterface::RELATIONSHIP_LINKS, $description) === true ? + $description[SchemaInterface::RELATIONSHIP_LINKS] : []; + assert(is_array($schemaLinks)); + + // if `self` or `related` link was given as LinkInterface merge it with the other links + $extraSchemaLinks = null; + if (is_bool($addSelfLink) === false) { + $extraSchemaLinks[LinkInterface::SELF] = $addSelfLink; + $addSelfLink = false; + } + if (is_bool($addRelatedLink) === false) { + $extraSchemaLinks[LinkInterface::RELATED] = $addRelatedLink; + $addRelatedLink = false; + } + if (empty($extraSchemaLinks) === false) { + // IDE do not understand it's defined without he line below + assert(isset($extraSchemaLinks)); + $schemaLinks = array_merge($extraSchemaLinks, $schemaLinks); + unset($extraSchemaLinks); + } + assert(is_bool($addSelfLink) === true && is_bool($addRelatedLink) === true); + + $hasLinks = $addSelfLink === true || $addRelatedLink === true || empty($schemaLinks) === false; + $links = $hasLinks === true ? + $this->parseLinks($parentSchema, $parentData, $name, $schemaLinks, $addSelfLink, $addRelatedLink) : null; + + return [$hasLinks, $links]; + } + + /** + * @param SchemaInterface $parentSchema + * @param mixed $parentData + * @param string $relationshipName + * @param iterable $schemaLinks + * @param bool $addSelfLink + * @param bool $addRelatedLink + * + * @return iterable + */ + private function parseLinks( + SchemaInterface $parentSchema, + $parentData, + string $relationshipName, + iterable $schemaLinks, + bool $addSelfLink, + bool $addRelatedLink + ): iterable { + $gotSelf = false; + $gotRelated = false; + + foreach ($schemaLinks as $name => $link) { + assert($link instanceof LinkInterface); + if ($name === LinkInterface::SELF) { + assert($gotSelf === false); + $gotSelf = true; + $addSelfLink = false; + } elseif ($name === LinkInterface::RELATED) { + assert($gotRelated === false); + $gotRelated = true; + $addRelatedLink = false; + } + + yield $name => $link; + } + + if ($addSelfLink === true) { + $link = $parentSchema->getRelationshipSelfLink($parentData, $relationshipName); + yield LinkInterface::SELF => $link; + $gotSelf = true; + } + if ($addRelatedLink === true) { + $link = $parentSchema->getRelationshipRelatedLink($parentData, $relationshipName); + yield LinkInterface::RELATED => $link; + $gotRelated = true; + } + + // spec: check links has at least one of the following: self or related + assert($gotSelf || $gotRelated); + } +} diff --git a/src/Parser/RelationshipData/RelationshipDataIsCollection.php b/src/Parser/RelationshipData/RelationshipDataIsCollection.php new file mode 100644 index 00000000..8834dc20 --- /dev/null +++ b/src/Parser/RelationshipData/RelationshipDataIsCollection.php @@ -0,0 +1,139 @@ +resources = $resources; + } + + /** + * @inheritdoc + */ + public function isCollection(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function isNull(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isResource(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isIdentifier(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function getIdentifier(): IdentifierInterface + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } + + /** + * @inheritdoc + */ + public function getIdentifiers(): iterable + { + return $this->getResources(); + } + + /** + * @inheritdoc + */ + public function getResource(): ResourceInterface + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } + + /** + * @inheritdoc + */ + public function getResources(): iterable + { + if ($this->parsedResources === null) { + foreach ($this->resources as $resource) { + $parsedResource = $this->createParsedResource($resource); + $this->parsedResources[] = $parsedResource; + + yield $parsedResource; + } + + return; + } + + yield from $this->parsedResources; + } +} diff --git a/src/Parser/RelationshipData/RelationshipDataIsIdentifier.php b/src/Parser/RelationshipData/RelationshipDataIsIdentifier.php new file mode 100644 index 00000000..ae8d6081 --- /dev/null +++ b/src/Parser/RelationshipData/RelationshipDataIsIdentifier.php @@ -0,0 +1,133 @@ +identifier = $identifier; + } + + /** + * @inheritdoc + */ + public function isCollection(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isNull(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isResource(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isIdentifier(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function getIdentifier(): ParserIdentifierInterface + { + if ($this->parsedIdentifier === null) { + $this->parsedIdentifier = $this->createParsedIdentifier($this->identifier); + } + + return $this->parsedIdentifier; + } + + /** + * @inheritdoc + */ + public function getIdentifiers(): iterable + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } + + /** + * @inheritdoc + */ + public function getResource(): ResourceInterface + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } + + /** + * @inheritdoc + */ + public function getResources(): iterable + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } +} diff --git a/src/Parser/RelationshipData/RelationshipDataIsNull.php b/src/Parser/RelationshipData/RelationshipDataIsNull.php new file mode 100644 index 00000000..f1dacafa --- /dev/null +++ b/src/Parser/RelationshipData/RelationshipDataIsNull.php @@ -0,0 +1,98 @@ +resource = $resource; + } + + /** + * @inheritdoc + */ + public function isCollection(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isNull(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function isResource(): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function isIdentifier(): bool + { + return false; + } + + /** + * @inheritdoc + */ + public function getIdentifier(): IdentifierInterface + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } + + /** + * @inheritdoc + */ + public function getIdentifiers(): iterable + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } + + /** + * @inheritdoc + */ + public function getResource(): ResourceInterface + { + if ($this->parsedResource === null) { + $this->parsedResource = $this->createParsedResource($this->resource); + } + + return $this->parsedResource; + } + + /** + * @inheritdoc + */ + public function getResources(): iterable + { + throw new LogicException(_(static::MSG_INVALID_OPERATION)); + } +} diff --git a/src/Representation/BaseWriter.php b/src/Representation/BaseWriter.php new file mode 100644 index 00000000..5c11856a --- /dev/null +++ b/src/Representation/BaseWriter.php @@ -0,0 +1,217 @@ +reset(); + } + + /** + * @inheritdoc + */ + public function setDataAsArray(): BaseWriterInterface + { + assert($this->isDataAnArray() === false); + assert(array_key_exists(DocumentInterface::KEYWORD_DATA, $this->data) === false); + + $this->data[DocumentInterface::KEYWORD_DATA] = []; + $this->isDataAnArray = true; + + return $this; + } + + /** + * @inheritdoc + */ + public function getDocument(): array + { + return $this->data; + } + + /** + * @inheritdoc + */ + public function setMeta($meta): BaseWriterInterface + { + assert(is_resource($meta) === false); + + $this->data[DocumentInterface::KEYWORD_META] = $meta; + + return $this; + } + + /** + * @inheritdoc + */ + public function setJsonApiVersion(string $version): BaseWriterInterface + { + $this->data[DocumentInterface::KEYWORD_JSON_API][DocumentInterface::KEYWORD_VERSION] = $version; + + return $this; + } + + /** + * @inheritdoc + */ + public function setJsonApiMeta($meta): BaseWriterInterface + { + assert(is_resource($meta) === false); + + $this->data[DocumentInterface::KEYWORD_JSON_API][DocumentInterface::KEYWORD_META] = $meta; + + return $this; + } + + /** + * @inheritdoc + */ + public function setUrlPrefix(string $prefix): BaseWriterInterface + { + $this->urlPrefix = $prefix; + + return $this; + } + + /** + * @inheritdoc + */ + public function setLinks(iterable $links): BaseWriterInterface + { + $representation = $this->getLinksRepresentation( + $this->getUrlPrefix(), + $links + ); + + if (empty($representation) === false) { + $this->data[DocumentInterface::KEYWORD_LINKS] = $representation; + } + + return $this; + } + + /** + * @inheritdoc + */ + public function setProfile(iterable $links): BaseWriterInterface + { + $representation = $this->getLinksListRepresentation( + $this->getUrlPrefix(), + $links + ); + + if (empty($representation) === false) { + $this->data[DocumentInterface::KEYWORD_LINKS][DocumentInterface::KEYWORD_PROFILE] = $representation; + } + + return $this; + } + + /** + * @return void + */ + protected function reset(): void + { + $this->data = []; + $this->urlPrefix = ''; + $this->isDataAnArray = false; + } + + /** + * @return string + */ + protected function getUrlPrefix(): string + { + return $this->urlPrefix; + } + + /** + * @param null|string $prefix + * @param iterable $links + * + * @return array + */ + protected function getLinksRepresentation(?string $prefix, iterable $links): array + { + $result = []; + + foreach ($links as $name => $link) { + assert($link instanceof LinkInterface); + $result[$name] = $link->canBeShownAsString() === true ? + $link->getStringRepresentation($prefix) : $link->getArrayRepresentation($prefix); + } + + return $result; + } + + /** + * @param null|string $prefix + * @param iterable $links + * + * @return array + */ + protected function getLinksListRepresentation(?string $prefix, iterable $links): array + { + $result = []; + + foreach ($links as $link) { + assert($link instanceof BaseLinkInterface); + $result[] = $link->canBeShownAsString() === true ? + $link->getStringRepresentation($prefix) : $link->getArrayRepresentation($prefix); + } + + return $result; + } + + /** + * @return bool + */ + protected function isDataAnArray(): bool + { + return $this->isDataAnArray; + } +} diff --git a/src/Representation/DocumentWriter.php b/src/Representation/DocumentWriter.php new file mode 100644 index 00000000..ff4be81d --- /dev/null +++ b/src/Representation/DocumentWriter.php @@ -0,0 +1,316 @@ +data[DocumentInterface::KEYWORD_DATA]) === false); + $this->data[DocumentInterface::KEYWORD_DATA] = null; + + return $this; + } + + /** + * @inheritdoc + */ + public function addIdentifierToData(IdentifierInterface $identifier): DocumentWriterInterface + { + $this->addToData($this->getIdentifierRepresentation($identifier)); + + return $this; + } + + /** + * @inheritdoc + */ + public function addResourceToData( + ResourceInterface $resource, + FieldSetFilterInterface $filter + ): DocumentWriterInterface { + $this->addToData($this->getResourceRepresentation($resource, $filter)); + + return $this; + } + + /** + * @inheritdoc + */ + public function addResourceToIncluded( + ResourceInterface $resource, + FieldSetFilterInterface $filter + ): DocumentWriterInterface { + // We track resources only in included section to avoid duplicates there. + // If those resources duplicate main it is not bad because if we remove them + // (and sometimes we would have to rollback and remove some of them if we meet it in the main resources) + // the client app will have to search them not only in included section but in the main as well. + // + // The spec seems to be OK with it. + + if ($this->hasNotBeenAdded($resource) === true) { + $this->registerResource($resource); + $this->addToIncluded($this->getResourceRepresentation($resource, $filter)); + } + + return $this; + } + + /** + * @inheritdoc + */ + protected function reset(): void + { + parent::reset(); + + $this->addedResources = []; + } + + /** + * If full resource has not been added yet either to includes section. + * + * @param ResourceInterface $resource + * + * @return bool + */ + protected function hasNotBeenAdded(ResourceInterface $resource): bool + { + return isset($this->addedResources[$resource->getId()][$resource->getType()]) === false; + } + + /** + * @param ResourceInterface $resource + * + * @return void + */ + protected function registerResource(ResourceInterface $resource): void + { + assert($this->hasNotBeenAdded($resource)); + + $this->addedResources[$resource->getId()][$resource->getType()] = true; + } + + /** + * @param IdentifierInterface $identifier + * + * @return array + */ + protected function getIdentifierRepresentation(IdentifierInterface $identifier): array + { + // it's odd not to have actual ID for identifier (which is OK for newly created resource). + assert($identifier->getId() !== null); + + return $identifier->hasIdentifierMeta() === false ? [ + DocumentInterface::KEYWORD_TYPE => $identifier->getType(), + DocumentInterface::KEYWORD_ID => $identifier->getId(), + ] : [ + DocumentInterface::KEYWORD_TYPE => $identifier->getType(), + DocumentInterface::KEYWORD_ID => $identifier->getId(), + DocumentInterface::KEYWORD_META => $identifier->getIdentifierMeta(), + ]; + } + + /** + * @param ResourceInterface $resource + * + * @return array + */ + protected function getIdentifierRepresentationFromResource(ResourceInterface $resource): array + { + return $resource->hasIdentifierMeta() === false ? [ + DocumentInterface::KEYWORD_TYPE => $resource->getType(), + DocumentInterface::KEYWORD_ID => $resource->getId(), + ] : [ + DocumentInterface::KEYWORD_TYPE => $resource->getType(), + DocumentInterface::KEYWORD_ID => $resource->getId(), + DocumentInterface::KEYWORD_META => $resource->getIdentifierMeta(), + ]; + } + + /** + * @param iterable $attributes + * + * @return array + */ + protected function getAttributesRepresentation(iterable $attributes): array + { + $representation = []; + foreach ($attributes as $name => $value) { + $representation[$name] = $value; + } + + return $representation; + } + + /** + * @param iterable $relationships + * + * @return array + */ + protected function getRelationshipsRepresentation(iterable $relationships): array + { + $representation = []; + foreach ($relationships as $name => $relationship) { + assert(is_string($name) === true && empty($name) === false); + assert($relationship instanceof RelationshipInterface); + $representation[$name] = $this->getRelationshipRepresentation($relationship); + } + + return $representation; + } + + /** + * @param RelationshipInterface $relationship + * + * @return array + */ + protected function getRelationshipRepresentation(RelationshipInterface $relationship): array + { + $representation = []; + + if ($relationship->hasLinks() === true) { + $representation[DocumentInterface::KEYWORD_LINKS] = + $this->getLinksRepresentation($this->getUrlPrefix(), $relationship->getLinks()); + } + + if ($relationship->hasData() === true) { + $representation[DocumentInterface::KEYWORD_DATA] = $this->getRelationshipDataRepresentation( + $relationship->getData() + ); + } + + if ($relationship->hasMeta() === true) { + $representation[DocumentInterface::KEYWORD_META] = $relationship->getMeta(); + } + + return $representation; + } + + /** + * @param RelationshipDataInterface $data + * + * @return array|null + */ + protected function getRelationshipDataRepresentation(RelationshipDataInterface $data): ?array + { + if ($data->isResource() === true) { + return $this->getIdentifierRepresentationFromResource($data->getResource()); + } elseif ($data->isIdentifier() === true) { + return $this->getIdentifierRepresentation($data->getIdentifier()); + } elseif ($data->isCollection() === true) { + $representation = []; + foreach ($data->getIdentifiers() as $identifier) { + assert($identifier instanceof IdentifierInterface); + $representation[] = $this->getIdentifierRepresentation($identifier); + } + + return $representation; + } + + assert($data->isNull() === true); + + return null; + } + + /** + * @param ResourceInterface $resource + * @param FieldSetFilterInterface $filter + * + * @return array + */ + protected function getResourceRepresentation(ResourceInterface $resource, FieldSetFilterInterface $filter): array + { + $representation = [ + DocumentInterface::KEYWORD_TYPE => $resource->getType(), + ]; + + if (($index = $resource->getId()) !== null) { + $representation[DocumentInterface::KEYWORD_ID] = $index; + } + + $attributes = $this->getAttributesRepresentation($filter->getAttributes($resource)); + if (empty($attributes) === false) { + $representation[DocumentInterface::KEYWORD_ATTRIBUTES] = $attributes; + } + + $relationships = $this->getRelationshipsRepresentation($filter->getRelationships($resource)); + if (empty($relationships) === false) { + $representation[DocumentInterface::KEYWORD_RELATIONSHIPS] = $relationships; + } + + if ($resource->hasLinks() === true) { + $representation[DocumentInterface::KEYWORD_LINKS] = + $this->getLinksRepresentation($this->getUrlPrefix(), $resource->getLinks()); + } + + if ($resource->hasResourceMeta() === true) { + $representation[DocumentInterface::KEYWORD_META] = $resource->getResourceMeta(); + } + + return $representation; + } + + /** + * @param array $representation + * + * @return void + */ + private function addToData(array $representation): void + { + if ($this->isDataAnArray() === true) { + $this->data[DocumentInterface::KEYWORD_DATA][] = $representation; + + return; + } + + // check data has not been added yet + assert(array_key_exists(DocumentInterface::KEYWORD_DATA, $this->data) === false); + $this->data[DocumentInterface::KEYWORD_DATA] = $representation; + } + + /** + * @param array $representation + * + * @return void + */ + private function addToIncluded(array $representation): void + { + $this->data[DocumentInterface::KEYWORD_INCLUDED][] = $representation; + } +} diff --git a/src/Representation/ErrorWriter.php b/src/Representation/ErrorWriter.php new file mode 100644 index 00000000..808d421c --- /dev/null +++ b/src/Representation/ErrorWriter.php @@ -0,0 +1,88 @@ +data[DocumentInterface::KEYWORD_ERRORS] = []; + } + + /** + * @inheritdoc + */ + public function addError(ErrorInterface $error): ErrorWriterInterface + { + $representation = array_filter([ + DocumentInterface::KEYWORD_ERRORS_ID => $error->getId(), + DocumentInterface::KEYWORD_LINKS => $this->getErrorLinksRepresentation($error), + DocumentInterface::KEYWORD_ERRORS_STATUS => $error->getStatus(), + DocumentInterface::KEYWORD_ERRORS_CODE => $error->getCode(), + DocumentInterface::KEYWORD_ERRORS_TITLE => $error->getTitle(), + DocumentInterface::KEYWORD_ERRORS_DETAIL => $error->getDetail(), + DocumentInterface::KEYWORD_ERRORS_SOURCE => $error->getSource(), + ]); + + if ($error->hasMeta() === true) { + $representation[DocumentInterface::KEYWORD_ERRORS_META] = $error->getMeta(); + } + + // There is a special case when error representation is an empty array + // Due to further json transform it must be an object otherwise it will be an empty array in json + $representation = empty($representation) === false ? $representation : (object)$representation; + + $this->data[DocumentInterface::KEYWORD_ERRORS][] = $representation; + + return $this; + } + + /** + * @param ErrorInterface $error + * + * @return array|null + */ + private function getErrorLinksRepresentation(ErrorInterface $error): ?array + { + $linksRepresentation = null; + + if (($value = $error->getLinks()) !== null) { + $linksRepresentation = $this->getLinksRepresentation($this->getUrlPrefix(), $value); + } + + if (($value = $error->getTypeLinks()) !== null) { + $linksRepresentation[DocumentInterface::KEYWORD_ERRORS_TYPE] = + $this->getLinksListRepresentation($this->getUrlPrefix(), $value); + } + + return $linksRepresentation; + } +} diff --git a/src/Representation/FieldSetFilter.php b/src/Representation/FieldSetFilter.php new file mode 100644 index 00000000..bce3d153 --- /dev/null +++ b/src/Representation/FieldSetFilter.php @@ -0,0 +1,137 @@ +fieldSets = []; + + foreach ($fieldSets as $type => $fields) { + assert(is_string($type) === true && empty($type) === false); + assert(is_iterable($fields) === true); + + $this->fieldSets[$type] = []; + + foreach ($fields as $field) { + assert(is_string($field) === true && empty($field) === false); + assert(isset($this->fieldSets[$type][$field]) === false); + + $this->fieldSets[$type][$field] = true; + } + } + } + + /** + * @inheritdoc + */ + public function getAttributes(ResourceInterface $resource): iterable + { + yield from $this->filterFields($resource->getType(), $resource->getAttributes()); + } + + /** + * @inheritdoc + */ + public function getRelationships(ResourceInterface $resource): iterable + { + yield from $this->filterFields($resource->getType(), $resource->getRelationships()); + } + + /** + * @inheritdoc + */ + public function shouldOutputRelationship(PositionInterface $position): bool + { + $parentType = $position->getParentType(); + if ($this->hasFilter($parentType) === true) { + return array_key_exists($position->getParentRelationship(), $this->getAllowedFields($parentType)); + } + + return true; + } + + /** + * @return array + */ + protected function getFieldSets(): array + { + return $this->fieldSets; + } + + /** + * @param string $type + * + * @return bool + */ + protected function hasFilter(string $type): bool + { + return array_key_exists($type, $this->fieldSets) === true; + } + + /** + * @param string $type + * + * @return array + */ + protected function getAllowedFields(string $type): array + { + assert($this->hasFilter($type) === true); + + return $this->getFieldSets()[$type]; + } + + /** + * @param string $type + * @param iterable $fields + * + * @return iterable + */ + protected function filterFields(string $type, iterable $fields): iterable + { + if ($this->hasFilter($type) === false) { + yield from $fields; + + return; + } + + $allowedFields = $this->getAllowedFields($type); + foreach ($fields as $name => $value) { + if (array_key_exists($name, $allowedFields) === true) { + yield $name => $value; + } + } + } +} diff --git a/src/Schema/BaseSchema.php b/src/Schema/BaseSchema.php index cf7f8020..1812a184 100644 --- a/src/Schema/BaseSchema.php +++ b/src/Schema/BaseSchema.php @@ -1,7 +1,9 @@ -getResourceType()) === true && empty($this->getResourceType()) === false, - 'Resource type is not set for Schema \'' . static::class . '\'.' - ); - - if ($this->selfSubUrl === null) { - $this->selfSubUrl = '/' . $this->getResourceType(); - } else { - assert( - is_string($this->selfSubUrl) === true && empty($this->selfSubUrl) === false && - $this->selfSubUrl[0] === '/' && $this->selfSubUrl[strlen($this->selfSubUrl) - 1] != '/', - '\'Self\' sub-url set incorrectly for Schema \'' . static::class . '\'.' - ); - } - - $this->factory = $factory; - } + private $subUrl = null; /** - * @inheritdoc + * @param FactoryInterface $factory */ - public function getResourceType(): string + public function __construct(FactoryInterface $factory) { - return $this->resourceType; + $this->factory = $factory; } /** * @inheritdoc */ - public function getSelfSubUrl($resource = null): string + public function getSelfLink($resource): LinkInterface { - return $resource === null ? $this->selfSubUrl : $this->selfSubUrl . '/' . $this->getId($resource); + return $this->getFactory()->createLink(true, $this->getSelfSubUrl($resource), false); } /** * @inheritdoc */ - public function getSelfSubLink($resource): LinkInterface + public function getLinks($resource): iterable { - return $this->createLink($this->getSelfSubUrl($resource)); - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function getRelationshipSelfLink( - $resource, - string $name, - $meta = null, - bool $treatAsHref = false - ): LinkInterface { - $link = $this->createLink($this->getRelationshipSelfUrl($resource, $name), $meta, $treatAsHref); + $links = [ + LinkInterface::SELF => $this->getSelfLink($resource), + ]; - return $link; + return $links; } /** * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ - public function getRelationshipRelatedLink( - $resource, - string $name, - $meta = null, - bool $treatAsHref = false - ): LinkInterface { - $link = $this->createLink($this->getRelationshipRelatedUrl($resource, $name), $meta, $treatAsHref); + public function getRelationshipSelfLink($resource, string $name): LinkInterface + { + // Feel free to override this method to change default URL or add meta - return $link; - } + $url = $this->getSelfSubUrl($resource) . '/' . DocumentInterface::KEYWORD_RELATIONSHIPS . '/' . $name; - /** - * @inheritdoc - */ - public function getPrimaryMeta($resource) - { - return null; + return $this->getFactory()->createLink(true, $url, false); } /** * @inheritdoc */ - public function getLinkageMeta($resource) + public function getRelationshipRelatedLink($resource, string $name): LinkInterface { - return null; - } + // Feel free to override this method to change default URL or add meta - /** - * @inheritdoc - */ - public function getInclusionMeta($resource) - { - return null; - } + $url = $this->getSelfSubUrl($resource) . '/' . $name; - /** - * @inheritdoc - */ - public function getRelationshipsPrimaryMeta($resource) - { - return null; + return $this->getFactory()->createLink(true, $url, false); } /** * @inheritdoc */ - public function getRelationshipsInclusionMeta($resource) + public function hasIdentifierMeta($resource): bool { - return null; + return false; } /** * @inheritdoc */ - public function isShowAttributesInIncluded(): bool - { - return $this->isShowAttributesInIncluded; - } - - /** - * Get resource links. - * - * @param object $resource - * @param bool $isPrimary - * @param array $includeRelationships A list of relationships that will be included as full resources. - * - * @return array - */ - public function getRelationships($resource, bool $isPrimary, array $includeRelationships): ?array + public function getIdentifierMeta($resource) { - assert($resource || $isPrimary || $includeRelationships || true); - - return []; - } - - /** - * @inheritdoc - */ - public function createResourceObject( - $resource, - bool $isOriginallyArrayed, - array $fieldKeysFilter = null - ): ResourceObjectInterface { - return $this->factory->createResourceObject($this, $resource, $isOriginallyArrayed, $fieldKeysFilter); + // default schema does not provide any meta + throw new LogicException(); } /** * @inheritdoc */ - public function getRelationshipObjectIterator($resource, bool $isPrimary, array $includeRelationships): iterable + public function hasResourceMeta($resource): bool { - $relationships = $this->getRelationships($resource, $isPrimary, $includeRelationships); - foreach ($relationships as $name => $desc) { - yield $this->createRelationshipObject($resource, $name, $desc); - } + return false; } /** * @inheritdoc */ - public function getIncludePaths(): array + public function getResourceMeta($resource) { - return []; + // default schema does not provide any meta + throw new LogicException(); } /** * @inheritdoc */ - public function getResourceLinks($resource): array + public function isAddSelfLinkInRelationshipByDefault(): bool { - $links = [ - LinkInterface::SELF => $this->getSelfSubLink($resource), - ]; - - return $links; + return true; } /** * @inheritdoc */ - public function getIncludedResourceLinks($resource): array + public function isAddRelatedLinkInRelationshipByDefault(): bool { - return []; + return true; } /** - * @param object $resource - * @param string $name - * - * @return string + * @return FactoryInterface */ - protected function getRelationshipSelfUrl($resource, $name) + protected function getFactory(): FactoryInterface { - $url = $this->getSelfSubUrl($resource) . '/' . DocumentInterface::KEYWORD_RELATIONSHIPS . '/' . $name; - - return $url; + return $this->factory; } /** - * @param object $resource - * @param string $name + * Get resources sub-URL. * * @return string */ - protected function getRelationshipRelatedUrl($resource, $name) - { - $url = $this->getSelfSubUrl($resource) . '/' . $name; - - return $url; - } - - /** - * @param string $subHref - * @param null|mixed $meta - * @param bool $treatAsHref - * - * @return LinkInterface - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - protected function createLink($subHref, $meta = null, $treatAsHref = false) - { - return $this->factory->createLink($subHref, $meta, $treatAsHref); - } - - /** - * @param object $resource - * @param string $relationshipName - * @param array $description - * @param bool $isShowSelf - * @param bool $isShowRelated - * - * @return array - */ - protected function readLinks($resource, $relationshipName, array $description, $isShowSelf, $isShowRelated) + protected function getResourcesSubUrl(): string { - $links = $description[self::LINKS] ?? []; - if ($isShowSelf === true && isset($links[LinkInterface::SELF]) === false) { - $links[LinkInterface::SELF] = $this->getRelationshipSelfLink($resource, $relationshipName); - } - if ($isShowRelated === true && isset($links[LinkInterface::RELATED]) === false) { - $links[LinkInterface::RELATED] = $this->getRelationshipRelatedLink($resource, $relationshipName); + if ($this->subUrl === null) { + $this->subUrl = '/' . $this->getType(); } - return $links; + return $this->subUrl; } /** - * @param object $resource - * @param string $name - * @param array $desc + * @param mixed $resource * - * @return RelationshipObjectInterface + * @return string */ - protected function createRelationshipObject($resource, $name, array $desc) + protected function getSelfSubUrl($resource): string { - assert( - array_key_exists(self::DATA, $desc) === true || - array_key_exists(self::META, $desc) === true || - array_key_exists(self::LINKS, $desc) === true, - 'A `' . $this->getResourceType() . ".$name` relationship must contain at least data, links or meta." - ); - - $data = $desc[self::DATA] ?? null; - $meta = $desc[self::META] ?? null; - $isShowSelf = (($desc[self::SHOW_SELF] ?? false) === true); - $isShowRelated = (($desc[self::SHOW_RELATED] ?? false) === true); - $isShowData = (($desc[self::SHOW_DATA] ?? array_key_exists(self::DATA, $desc)) === true); - $links = $this->readLinks($resource, $name, $desc, $isShowSelf, $isShowRelated); - - return $this->factory->createRelationshipObject($name, $data, $links, $meta, $isShowData, false); + return $this->getResourcesSubUrl() . '/' . $this->getId($resource); } } diff --git a/src/Document/Error.php b/src/Schema/Error.php similarity index 73% rename from src/Document/Error.php rename to src/Schema/Error.php index 0be61452..a0e61089 100644 --- a/src/Document/Error.php +++ b/src/Schema/Error.php @@ -1,7 +1,9 @@ - + * @var null|iterable */ private $links; + /** + * @var null|iterable + */ + private $typeLinks; + /** * @var string|null */ @@ -63,39 +70,52 @@ class Error implements ErrorInterface private $source; /** - * @var mixed|null + * @var bool + */ + private $hasMeta; + + /** + * @var mixed */ private $meta; /** * @param int|string|null $idx * @param LinkInterface|null $aboutLink + * @param iterable|null $typeLinks * @param string|null $status * @param string|null $code * @param string|null $title * @param string|null $detail * @param array|null $source - * @param mixed|null $meta + * @param bool $hasMeta + * @param mixed $meta */ public function __construct( $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, string $status = null, string $code = null, string $title = null, string $detail = null, array $source = null, + bool $hasMeta = false, $meta = null ) { $this ->setId($idx) ->setLink(DocumentInterface::KEYWORD_ERRORS_ABOUT, $aboutLink) + ->setTypeLinks($typeLinks) ->setStatus($status) ->setCode($code) ->setTitle($title) ->setDetail($detail) - ->setSource($source) - ->setMeta($meta); + ->setSource($source); + + if (($this->hasMeta = $hasMeta) === true) { + $this->setMeta($meta); + } } /** @@ -103,19 +123,19 @@ public function __construct( */ public function getId() { - return $this->idx; + return $this->index; } /** - * @param string|int|null $idx + * @param string|int|null $index * * @return self */ - public function setId($idx): self + public function setId($index): self { - assert($idx === null || is_int($idx) === true || is_string($idx) === true); + assert($index === null || is_int($index) === true || is_string($index) === true); - $this->idx = $idx; + $this->index = $index; return $this; } @@ -123,11 +143,19 @@ public function setId($idx): self /** * @inheritdoc */ - public function getLinks(): ?array + public function getLinks(): ?iterable { return $this->links; } + /** + * @inheritdoc + */ + public function getTypeLinks(): ?iterable + { + return $this->typeLinks; + } + /** * @param string $name * @param LinkInterface|null $link @@ -147,6 +175,18 @@ public function setLink(string $name, ?LinkInterface $link): self return $this; } + /** + * @param iterable|null $typeLinks + * + * @return self + */ + public function setTypeLinks(?iterable $typeLinks): self + { + $this->typeLinks = $typeLinks; + + return $this; + } + /** * @inheritdoc */ @@ -247,6 +287,14 @@ public function setSource(?array $source): self return $this; } + /** + * @inheritdoc + */ + public function hasMeta(): bool + { + return $this->hasMeta; + } + /** * @inheritdoc */ @@ -262,7 +310,8 @@ public function getMeta() */ public function setMeta($meta): self { - $this->meta = $meta; + $this->hasMeta = true; + $this->meta = $meta; return $this; } diff --git a/src/Exceptions/ErrorCollection.php b/src/Schema/ErrorCollection.php similarity index 62% rename from src/Exceptions/ErrorCollection.php rename to src/Schema/ErrorCollection.php index edb68baf..ae174822 100644 --- a/src/Exceptions/ErrorCollection.php +++ b/src/Schema/ErrorCollection.php @@ -1,7 +1,9 @@ -items; } @@ -119,7 +120,7 @@ public function getArrayCopy() */ public function add(ErrorInterface $error): self { - $this->items[] =$error; + $this->items[] = $error; return $this; } @@ -127,11 +128,13 @@ public function add(ErrorInterface $error): self /** * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -141,22 +144,37 @@ public function addDataError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToData(); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -166,22 +184,37 @@ public function addDataTypeError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToType(); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -191,22 +224,37 @@ public function addDataIdError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToId(); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -216,23 +264,38 @@ public function addAttributesError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToAttributes(); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $name * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -243,22 +306,37 @@ public function addDataAttributeError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToAttribute($name); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -268,23 +346,38 @@ public function addRelationshipsError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToRelationships(); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $name * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -295,23 +388,38 @@ public function addRelationshipError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToRelationship($name); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $name * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -322,23 +430,38 @@ public function addRelationshipTypeError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToRelationshipType($name); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $name * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -349,38 +472,55 @@ public function addRelationshipIdError( $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { $pointer = $this->getPathToRelationshipId($name); - return $this->addResourceError($title, $pointer, $detail, $status, $idx, $aboutLink, $code, $meta); + return $this->addResourceError( + $title, + $pointer, + $detail, + $status, + $idx, + $aboutLink, + $typeLinks, + $code, + $hasMeta, + $meta + ); } /** * @param string $name * @param string $title * @param string|null $detail - * @param int|string|null $status + * @param string|null $status * @param int|string|null $idx * @param LinkInterface|null $aboutLink - * @param int|string|null $code - * @param mixed|null $meta + * @param iterable|null $typeLinks + * @param string|null $code + * @param bool $hasMeta + * @param mixed $meta * * @return self */ public function addQueryParameterError( - $name, - $title, - $detail = null, - $status = null, + string $name, + string $title, + string $detail = null, + string $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, $code = null, + $hasMeta = false, $meta = null ): self { - $source = [Error::SOURCE_PARAMETER => $name]; - $error = new Error($idx, $aboutLink, $status, $code, $title, $detail, $source, $meta); + $source = [ErrorInterface::SOURCE_PARAMETER => $name]; + $error = new Error($idx, $aboutLink, $typeLinks, $status, $code, $title, $detail, $source, $hasMeta, $meta); $this->add($error); @@ -394,8 +534,10 @@ public function addQueryParameterError( * @param string|null $status * @param null $idx * @param LinkInterface|null $aboutLink + * @param iterable|null $typeLinks * @param string|null $code - * @param null $meta + * @param bool $hasMeta + * @param mixed $meta * * @return self */ @@ -406,11 +548,13 @@ protected function addResourceError( string $status = null, $idx = null, LinkInterface $aboutLink = null, + ?iterable $typeLinks = null, string $code = null, + $hasMeta = false, $meta = null ): self { - $source = [Error::SOURCE_POINTER => $pointer]; - $error = new Error($idx, $aboutLink, $status, $code, $title, $detail, $source, $meta); + $source = [ErrorInterface::SOURCE_POINTER => $pointer]; + $error = new Error($idx, $aboutLink, $typeLinks, $status, $code, $title, $detail, $source, $hasMeta, $meta); $this->add($error); diff --git a/src/Schema/IdentitySchema.php b/src/Schema/IdentitySchema.php deleted file mode 100644 index b514f22f..00000000 --- a/src/Schema/IdentitySchema.php +++ /dev/null @@ -1,83 +0,0 @@ -getSchemaByType($classType); - $this->resourceType = $schemaForRealType->getResourceType(); - $this->selfSubUrl = $schemaForRealType->getSelfSubUrl(); - - parent::__construct($factory); - - $this->identityClosure = $identityClosure; - } - - /** - * @inheritdoc - */ - public function getId($resource): ?string - { - $closure = $this->identityClosure; - $identity = $closure($resource); - - return $identity; - } - - /** - * @inheritdoc - */ - public function getAttributes($resource, array $fieldKeysFilter = null): ?array - { - // this method should not be called - throw new LogicException(); - } - - /** - * @inheritdoc - */ - public function getRelationships($resource, bool $isPrimary, array $includeRelationships): ?array - { - // this method should not be called - throw new LogicException(); - } -} diff --git a/src/Schema/Link.php b/src/Schema/Link.php new file mode 100644 index 00000000..845b1c2a --- /dev/null +++ b/src/Schema/Link.php @@ -0,0 +1,145 @@ +isSubUrl = $isSubUrl; + $this->value = $value; + $this->hasMeta = $hasMeta; + $this->meta = $meta; + } + + /** + * @inheritdoc + */ + public function canBeShownAsString(): bool + { + return $this->hasMeta() === false; + } + + /** + * @inheritdoc + */ + public function getStringRepresentation(string $prefix): string + { + assert($this->canBeShownAsString() === true); + + return $this->buildUrl($prefix); + } + + /** + * @inheritdoc + */ + public function getArrayRepresentation(string $prefix): array + { + assert($this->canBeShownAsString() === false); + + return [ + DocumentInterface::KEYWORD_HREF => $this->buildUrl($prefix), + DocumentInterface::KEYWORD_META => $this->getMeta(), + ]; + } + + /** + * If link contains sub-URL value and URL prefix should be added. + * + * @return bool + */ + private function isSubUrl(): bool + { + return $this->isSubUrl; + } + + /** + * Get link’s URL value (full URL or sub-URL). + * + * @return string + */ + private function getValue(): string + { + return $this->value; + } + + /** + * If link has meta information. + * + * @return bool + */ + private function hasMeta(): bool + { + return $this->hasMeta; + } + + /** + * Get meta information. + * + * @return mixed + */ + private function getMeta() + { + assert($this->hasMeta()); + + return $this->meta; + } + + /** + * @param string $prefix + * + * @return string + */ + protected function buildUrl(string $prefix): string + { + return $this->isSubUrl() ? $prefix . $this->getValue() : $this->getValue(); + } +} diff --git a/src/Schema/LinkWithAliases.php b/src/Schema/LinkWithAliases.php new file mode 100644 index 00000000..0e92d894 --- /dev/null +++ b/src/Schema/LinkWithAliases.php @@ -0,0 +1,102 @@ + $alias) { + assert(is_string($name) === true && empty($name) === false); + assert(is_string($alias) === true && empty($alias) === false); + $aliasesArray[$name] = $alias; + } + + $this->aliases = $aliasesArray; + $this->hasAliases = !empty($aliasesArray); + + parent::__construct($isSubUrl, $value, $hasMeta, $meta); + } + + /** + * @inheritdoc + */ + public function canBeShownAsString(): bool + { + return parent::canBeShownAsString() && $this->hasAliases() === false; + } + + /** + * @inheritdoc + */ + public function getArrayRepresentation(string $prefix): array + { + $linkRepresentation = parent::canBeShownAsString() === true ? [ + DocumentInterface::KEYWORD_HREF => $this->buildUrl($prefix), + ] : parent::getArrayRepresentation($prefix); + + if ($this->hasAliases() === true) { + $linkRepresentation[DocumentInterface::KEYWORD_ALIASES] = $this->getAliases(); + } + + return $linkRepresentation; + } + + /** + * @return bool + */ + private function hasAliases(): bool + { + return $this->hasAliases; + } + + /** + * Get aliases. + * + * @return array + */ + private function getAliases(): array + { + return $this->aliases; + } +} diff --git a/src/Schema/RelationshipObject.php b/src/Schema/RelationshipObject.php deleted file mode 100644 index 586516ed..00000000 --- a/src/Schema/RelationshipObject.php +++ /dev/null @@ -1,212 +0,0 @@ - - */ - private $links; - - /** - * @var object|array|null|Closure - */ - private $meta; - - /** - * @var bool - */ - private $isShowData; - - /** - * @var bool - */ - private $isRoot; - - /** - * @var bool - */ - private $isMetaEvaluated = false; - - /** - * @var bool - */ - private $isDataEvaluated = false; - - /** - * @param string|null $name - * @param object|array|null|Closure $data - * @param array $links - * @param object|array|null|Closure $meta - * @param bool $isShowData - * @param bool $isRoot - * - * @SuppressWarnings(PHPMD.StaticAccess) - */ - public function __construct( - ?string $name, - $data, - array $links, - $meta, - bool $isShowData, - bool $isRoot - ) { - $isOk = (($isRoot === false && $name !== null) || ($isRoot === true && $name === null)); - $isOk ?: Exceptions::throwInvalidArgument('name', $name); - - $this->setName($name)->setData($data)->setLinks($links)->setMeta($meta); - $this->isShowData = $isShowData; - $this->isRoot = $isRoot; - } - - /** - * @inheritdoc - */ - public function getName(): ?string - { - return $this->name; - } - - /** - * @param null|string $name - * - * @return self - */ - public function setName(?string $name): self - { - $this->name = $name; - - return $this; - } - - /** - * @inheritdoc - */ - public function getData() - { - if ($this->isDataEvaluated === false) { - $this->isDataEvaluated = true; - - if ($this->data instanceof Closure) { - /** @var Closure $data */ - $data = $this->data; - $this->setData($data()); - } - } - - assert(is_array($this->data) === true || is_object($this->data) === true || $this->data === null); - - return $this->data; - } - - /** - * @param object|array|null|Closure $data - * - * @return RelationshipObject - */ - public function setData($data): self - { - assert(is_array($data) === true || $data instanceof Closure || is_object($data) === true || $data === null); - - $this->data = $data; - $this->isDataEvaluated = false; - - return $this; - } - - /** - * @inheritdoc - */ - public function getLinks(): array - { - return $this->links; - } - - /** - * @param array $links - * - * @return self - */ - public function setLinks(array $links): self - { - $this->links = $links; - - return $this; - } - - /** - * @inheritdoc - */ - public function getMeta() - { - if ($this->isMetaEvaluated === false && $this->meta instanceof Closure) { - $meta = $this->meta; - $this->meta = $meta(); - } - - $this->isMetaEvaluated = true; - - return $this->meta; - } - - /** - * @param mixed $meta - * - * @return self - */ - public function setMeta($meta): self - { - $this->meta = $meta; - $this->isMetaEvaluated = false; - - return $this; - } - - /** - * @inheritdoc - */ - public function isShowData(): bool - { - return $this->isShowData; - } - - /** - * @inheritdoc - */ - public function isRoot(): bool - { - return $this->isRoot; - } -} diff --git a/src/Schema/ResourceIdentifierContainerAdapter.php b/src/Schema/ResourceIdentifierContainerAdapter.php deleted file mode 100644 index 843a2a65..00000000 --- a/src/Schema/ResourceIdentifierContainerAdapter.php +++ /dev/null @@ -1,91 +0,0 @@ -container = $container; - $this->factory = $factory; - } - - /** - * @inheritdoc - */ - public function getSchema($resourceObject): SchemaInterface - { - return $this->getSchemaAdapter($this->container->getSchema($resourceObject)); - } - - /** - * @inheritdoc - */ - public function hasSchema($resourceObject): bool - { - return $this->container->hasSchema($resourceObject); - } - - /** - * @inheritdoc - */ - public function getSchemaByType(string $type): SchemaInterface - { - return $this->getSchemaAdapter($this->container->getSchemaByType($type)); - } - - /** - * @inheritdoc - */ - public function getSchemaByResourceType(string $resourceType): SchemaInterface - { - return $this->getSchemaAdapter($this->container->getSchemaByResourceType($resourceType)); - } - - /** - * @param SchemaInterface $schema - * - * @return SchemaInterface - */ - protected function getSchemaAdapter(SchemaInterface $schema): SchemaInterface - { - return $this->factory->createResourceIdentifierSchemaAdapter($schema); - } -} diff --git a/src/Schema/ResourceIdentifierSchemaAdapter.php b/src/Schema/ResourceIdentifierSchemaAdapter.php deleted file mode 100644 index ea85f7d4..00000000 --- a/src/Schema/ResourceIdentifierSchemaAdapter.php +++ /dev/null @@ -1,222 +0,0 @@ -schema = $schema; - $this->factory = $factory; - } - - /** - * @inheritdoc - */ - public function getResourceType(): string - { - return $this->schema->getResourceType(); - } - - /** - * @inheritdoc - */ - public function getSelfSubUrl($resource = null): string - { - return $this->schema->getSelfSubUrl($resource); - } - - /** - * @inheritdoc - */ - public function getId($resource): ?string - { - return $this->schema->getId($resource); - } - - /** - * @inheritdoc - */ - public function getSelfSubLink($resource): LinkInterface - { - return $this->schema->getSelfSubLink($resource); - } - - /** - * @inheritdoc - */ - public function getAttributes($resource, array $fieldKeysFilter = null): ?array - { - return []; - } - - /** - * @inheritdoc - */ - public function getRelationships( - $resource, - bool $isPrimary, - array $includeRelationships, - array $fieldKeysFilter = null - ): ?array { - return []; - } - - /** - * @inheritdoc - */ - public function createResourceObject( - $resource, - bool $isOriginallyArrayed, - array $fieldKeysFilter = null - ): ResourceObjectInterface { - $fieldKeysFilter = []; - - return $this->factory->createResourceObject($this, $resource, $isOriginallyArrayed, $fieldKeysFilter); - } - - /** - * @inheritdoc - */ - public function getRelationshipObjectIterator($resource, bool $isPrimary, array $includeRelationships): iterable - { - return new EmptyIterator(); - } - - /** - * @inheritdoc - */ - public function getResourceLinks($resource): array - { - return []; - } - - /** - * @inheritdoc - */ - public function getIncludedResourceLinks($resource): array - { - return []; - } - - /** - * @inheritdoc - */ - public function isShowAttributesInIncluded(): bool - { - return false; - } - - /** - * @inheritdoc - */ - public function getIncludePaths(): array - { - return []; - } - - /** - * @inheritdoc - */ - public function getPrimaryMeta($resource) - { - return $this->schema->getPrimaryMeta($resource); - } - - /** - * @inheritdoc - */ - public function getInclusionMeta($resource) - { - return null; - } - - /** - * @inheritdoc - */ - public function getRelationshipsPrimaryMeta($resource) - { - return null; - } - - /** - * @inheritdoc - */ - public function getRelationshipsInclusionMeta($resource) - { - return null; - } - - /** - * @inheritdoc - */ - public function getLinkageMeta($resource) - { - return $this->schema->getLinkageMeta($resource); - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function getRelationshipSelfLink( - $resource, - string $name, - $meta = null, - bool $treatAsHref = false - ): LinkInterface { - return $this->schema->getRelationshipSelfLink($resource, $name, $meta, $treatAsHref); - } - - /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - */ - public function getRelationshipRelatedLink( - $resource, - string $name, - $meta = null, - bool $treatAsHref = false - ): LinkInterface { - return $this->schema->getRelationshipRelatedLink($resource, $name, $meta, $treatAsHref); - } -} diff --git a/src/Schema/ResourceObject.php b/src/Schema/ResourceObject.php deleted file mode 100644 index 876fa750..00000000 --- a/src/Schema/ResourceObject.php +++ /dev/null @@ -1,289 +0,0 @@ -|null - */ - protected $fieldKeysFilter; - - /** - * @var bool - */ - private $isSelfSubLinkSet = false; - - /** - * @var bool - */ - private $isRelationshipMetaSet = false; - - /** - * @var mixed - */ - private $relationshipMeta; - - /** - * @var bool - */ - private $isInclusionMetaSet = false; - - /** - * @var mixed - */ - private $inclusionMeta; - - /** - * @var bool - */ - private $isRelPrimaryMetaSet = false; - - /** - * @var mixed - */ - private $relPrimaryMeta; - - /** - * @var bool - */ - private $isRelIncMetaSet = false; - - /** - * @var mixed - */ - private $relInclusionMeta; - - /** - * @param SchemaInterface $schema - * @param object $resource - * @param bool $isInArray - * @param array|null $attributeKeysFilter - * - * @SuppressWarnings(PHPMD.StaticAccess) - */ - public function __construct( - SchemaInterface $schema, - $resource, - bool $isInArray, - array $fieldKeysFilter = null - ) { - is_object($resource) === true ?: Exceptions::throwInvalidArgument('resource', $resource); - - $this->schema = $schema; - $this->resource = $resource; - $this->isInArray = $isInArray; - $this->fieldKeysFilter = $fieldKeysFilter; - } - - /** - * @inheritdoc - */ - public function getType(): string - { - return $this->schema->getResourceType(); - } - - /** - * @inheritdoc - */ - public function getId(): ?string - { - if ($this->idx === false) { - $index = $this->schema->getId($this->resource); - $this->idx = $index === null ? $index : (string)$index; - } - - return $this->idx; - } - - /** - * @inheritdoc - */ - public function getAttributes(): ?array - { - if ($this->attributes === null) { - $attributes = $this->schema->getAttributes($this->resource, $this->fieldKeysFilter); - if ($this->fieldKeysFilter !== null) { - $attributes = array_intersect_key($attributes, $this->fieldKeysFilter); - } - $this->attributes = $attributes; - } - - return $this->attributes; - } - - /** - * @inheritdoc - */ - public function getSelfSubLink(): LinkInterface - { - if ($this->isSelfSubLinkSet === false) { - $this->selfSubLink = $this->schema->getSelfSubLink($this->resource); - $this->isSelfSubLinkSet = true; - } - - return $this->selfSubLink; - } - - /** - * @inheritdoc - */ - public function getResourceLinks(): array - { - return $this->schema->getResourceLinks($this->resource); - } - - /** - * @inheritdoc - */ - public function getIncludedResourceLinks(): array - { - return $this->schema->getIncludedResourceLinks($this->resource); - } - - /** - * @inheritdoc - */ - public function isShowAttributesInIncluded(): bool - { - return $this->schema->isShowAttributesInIncluded(); - } - - /** - * @inheritdoc - */ - public function isInArray(): bool - { - return $this->isInArray; - } - - /** - * @inheritdoc - */ - public function getPrimaryMeta() - { - if ($this->isPrimaryMetaSet === false) { - $this->primaryMeta = $this->schema->getPrimaryMeta($this->resource); - $this->isPrimaryMetaSet = true; - } - - return $this->primaryMeta; - } - - /** - * @inheritdoc - */ - public function getInclusionMeta() - { - if ($this->isInclusionMetaSet === false) { - $this->inclusionMeta = $this->schema->getInclusionMeta($this->resource); - $this->isInclusionMetaSet = true; - } - - return $this->inclusionMeta; - } - - /** - * @inheritdoc - */ - public function getRelationshipsPrimaryMeta() - { - if ($this->isRelPrimaryMetaSet === false) { - $this->relPrimaryMeta = $this->schema->getRelationshipsPrimaryMeta($this->resource); - $this->isRelPrimaryMetaSet = true; - } - - return $this->relPrimaryMeta; - } - - /** - * @inheritdoc - */ - public function getRelationshipsInclusionMeta() - { - if ($this->isRelIncMetaSet === false) { - $this->relInclusionMeta = $this->schema->getRelationshipsInclusionMeta($this->resource); - $this->isRelIncMetaSet = true; - } - - return $this->relInclusionMeta; - } - - /** - * @inheritdoc - */ - public function getLinkageMeta() - { - if ($this->isRelationshipMetaSet === false) { - $this->relationshipMeta = $this->schema->getLinkageMeta($this->resource); - $this->isRelationshipMetaSet = true; - } - - return $this->relationshipMeta; - } -} diff --git a/src/Schema/Container.php b/src/Schema/SchemaContainer.php similarity index 54% rename from src/Schema/Container.php rename to src/Schema/SchemaContainer.php index 29773088..ad3f26f3 100644 --- a/src/Schema/Container.php +++ b/src/Schema/SchemaContainer.php @@ -1,7 +1,9 @@ - 'Type must be non-empty string.', - self::MSG_INVALID_SCHEME => - 'Schema for type \'%s\' must be non-empty string, callable or SchemaInterface instance.', - self::MSG_TYPE_REUSE_FORBIDDEN => - 'Type should not be used more than once to register a schema (\'%s\').', - self::MSG_UNREGISTERED_SCHEME_FOR_TYPE => 'Schema is not registered for type \'%s\'.', - self::MSG_UNREGISTERED_SCHEME_FOR_RESOURCE_TYPE => 'Schema is not registered for resource type \'%s\'.', - ]; + const MSG_TYPE_REUSE_FORBIDDEN = 'Type should not be used more than once to register a schema (`%s`).'; /** * @var array @@ -86,25 +61,18 @@ class Container implements ContainerInterface, LoggerAwareInterface private $resType2JsonType = []; /** - * @var SchemaFactoryInterface + * @var FactoryInterface */ private $factory; /** - * @var array - */ - private $messages; - - /** - * @param SchemaFactoryInterface $factory - * @param iterable $schemas - * @param array $messages + * @param FactoryInterface $factory + * @param iterable $schemas */ - public function __construct(SchemaFactoryInterface $factory, iterable $schemas = [], $messages = self::MESSAGES) + public function __construct(FactoryInterface $factory, iterable $schemas) { - $this->factory = $factory; - $this->messages = $messages; - $this->registerArray($schemas); + $this->factory = $factory; + $this->registerCollection($schemas); } /** @@ -121,25 +89,30 @@ public function __construct(SchemaFactoryInterface $factory, iterable $schemas = public function register(string $type, $schema): void { if (empty($type) === true || class_exists($type) === false) { - throw new InvalidArgumentException(_($this->messages[self::MSG_INVALID_TYPE])); + throw new InvalidArgumentException(_(static::MSG_INVALID_MODEL_TYPE)); } $isOk = ( - (is_string($schema) === true && empty($schema) === false) || + ( + is_string($schema) === true && + empty($schema) === false && + class_exists($schema) === true && + in_array(SchemaInterface::class, class_implements($schema)) === true + ) || is_callable($schema) || $schema instanceof SchemaInterface ); if ($isOk === false) { - throw new InvalidArgumentException(_($this->messages[self::MSG_INVALID_SCHEME], $type)); + throw new InvalidArgumentException(_(static::MSG_INVALID_SCHEME, $type)); } if ($this->hasProviderMapping($type) === true) { - throw new InvalidArgumentException(_($this->messages[self::MSG_TYPE_REUSE_FORBIDDEN], $type)); + throw new InvalidArgumentException(_(static::MSG_TYPE_REUSE_FORBIDDEN, $type)); } if ($schema instanceof SchemaInterface) { $this->setProviderMapping($type, get_class($schema)); - $this->setResourceToJsonTypeMapping($schema->getResourceType(), $type); + $this->setResourceToJsonTypeMapping($schema->getType(), $type); $this->setCreatedProvider($type, $schema); } else { $this->setProviderMapping($type, $schema); @@ -153,7 +126,7 @@ public function register(string $type, $schema): void * * @return void */ - public function registerArray(iterable $schemas): void + public function registerCollection(iterable $schemas): void { foreach ($schemas as $type => $schema) { $this->register($type, $schema); @@ -163,11 +136,9 @@ public function registerArray(iterable $schemas): void /** * @inheritdoc */ - public function getSchema($resource): ?SchemaInterface + public function getSchema($resource): SchemaInterface { - if ($resource === null) { - return null; - } + assert($this->hasSchema($resource)); $resourceType = $this->getResourceType($resource); @@ -189,16 +160,12 @@ public function hasSchema($resourceObject): bool * @SuppressWarnings(PHPMD.StaticAccess) * @SuppressWarnings(PHPMD.ElseExpression) */ - public function getSchemaByType(string $type): SchemaInterface + private function getSchemaByType(string $type): SchemaInterface { if ($this->hasCreatedProvider($type) === true) { return $this->getCreatedProvider($type); } - if ($this->hasProviderMapping($type) === false) { - throw new InvalidArgumentException(_($this->messages[self::MSG_UNREGISTERED_SCHEME_FOR_TYPE], $type)); - } - $classNameOrCallable = $this->getProviderMapping($type); if (is_string($classNameOrCallable) === true) { $schema = $this->createSchemaFromClassName($classNameOrCallable); @@ -210,50 +177,15 @@ public function getSchemaByType(string $type): SchemaInterface /** @var SchemaInterface $schema */ - $this->setResourceToJsonTypeMapping($schema->getResourceType(), $type); + $this->setResourceToJsonTypeMapping($schema->getType(), $type); return $schema; } /** - * @inheritdoc - * - * @SuppressWarnings(PHPMD.StaticAccess) - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - public function getSchemaByResourceType(string $resourceType): SchemaInterface - { - // Schema is not found among instantiated schemas for resource type $resourceType - $isOk = (is_string($resourceType) === true && $this->hasResourceToJsonTypeMapping($resourceType) === true); - - // Schema might not be found if it hasn't been searched by type (not resource type) before. - // We instantiate all schemas and then find one. - if ($isOk === false) { - foreach ($this->getProviderMappings() as $type => $schema) { - if ($this->hasCreatedProvider($type) === false) { - // it will instantiate the schema - $this->getSchemaByType($type); - } - } - } - - // search one more time - $isOk = (is_string($resourceType) === true && $this->hasResourceToJsonTypeMapping($resourceType) === true); - - if ($isOk === false) { - throw new InvalidArgumentException(_( - $this->messages[self::MSG_UNREGISTERED_SCHEME_FOR_RESOURCE_TYPE], - $resourceType - )); - } - - return $this->getSchemaByType($this->getJsonType($resourceType)); - } - - /** - * @return SchemaFactoryInterface + * @return FactoryInterface */ - protected function getFactory(): SchemaFactoryInterface + protected function getFactory(): FactoryInterface { return $this->factory; } @@ -273,7 +205,7 @@ protected function getProviderMappings(): array */ protected function hasProviderMapping(string $type): bool { - return array_key_exists($type, $this->providerMapping); + return array_key_exists($type, $this->getProviderMappings()); } /** @@ -283,7 +215,7 @@ protected function hasProviderMapping(string $type): bool */ protected function getProviderMapping(string $type) { - return $this->providerMapping[$type]; + return $this->getProviderMappings()[$type]; } /** @@ -328,26 +260,6 @@ protected function setCreatedProvider(string $type, SchemaInterface $provider): $this->createdProviders[$type] = $provider; } - /** - * @param string $resourceType - * - * @return bool - */ - protected function hasResourceToJsonTypeMapping(string $resourceType): bool - { - return array_key_exists($resourceType, $this->resType2JsonType); - } - - /** - * @param string $resourceType - * - * @return string - */ - protected function getJsonType(string $resourceType): string - { - return $this->resType2JsonType[$resourceType]; - } - /** * @param string $resourceType * @param string $jsonType diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 850e9562..a012c866 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -1,7 +1,9 @@ -pushHandler(new StreamHandler($path)); - $factory->setLogger($log); - - $container = $factory->createContainer($schemas); - $encoder = $factory->createEncoder($container, $encodeOptions); - - return $encoder; + return new Factory(); } } diff --git a/tests/Data/AuthorCModelSchema.php b/tests/Data/AuthorCModelSchema.php deleted file mode 100644 index d83d7b92..00000000 --- a/tests/Data/AuthorCModelSchema.php +++ /dev/null @@ -1,82 +0,0 @@ - $author[AuthorCModel::ATTRIBUTE_FIRST_NAME], - AuthorCModel::ATTRIBUTE_LAST_NAME => $author[AuthorCModel::ATTRIBUTE_LAST_NAME], - ]; - } - - /** - * @inheritdoc - */ - public function getRelationships($author, bool $isPrimary, array $includeRelationships): ?array - { - assert($author instanceof AuthorCModel); - - if (($isPrimary && $this->isIsLinksInPrimary()) || (!$isPrimary && $this->isIsLinksInIncluded())) { - $selfLink = $this->getRelationshipSelfLink($author, AuthorCModel::LINK_COMMENTS); - $links = [ - AuthorCModel::LINK_COMMENTS => [ - self::LINKS => [LinkInterface::SELF => $selfLink], - self::SHOW_DATA => false, - ], - ]; - } else { - $links = [ - AuthorCModel::LINK_COMMENTS => [ - // closures for data are supported as well - self::DATA => function () use ($author) { - return isset($author[AuthorCModel::LINK_COMMENTS]) ? - $author[AuthorCModel::LINK_COMMENTS] : null; - }, - ], - ]; - } - - // NOTE: The line(s) below for testing purposes only. Not for production. - $this->fixLinks($author, $links); - - return $links; - } -} diff --git a/tests/Data/AuthorSchema.php b/tests/Data/AuthorSchema.php deleted file mode 100644 index 01265ec5..00000000 --- a/tests/Data/AuthorSchema.php +++ /dev/null @@ -1,78 +0,0 @@ -{Author::ATTRIBUTE_ID}; - } - - /** - * @inheritdoc - */ - public function getAttributes($author, array $fieldKeysFilter = null): ?array - { - return [ - Author::ATTRIBUTE_FIRST_NAME => $author->{Author::ATTRIBUTE_FIRST_NAME}, - Author::ATTRIBUTE_LAST_NAME => $author->{Author::ATTRIBUTE_LAST_NAME}, - ]; - } - - /** - * @inheritdoc - */ - public function getRelationships($author, bool $isPrimary, array $includeRelationships): ?array - { - assert($author instanceof Author); - - if (($isPrimary && $this->isIsLinksInPrimary()) || (!$isPrimary && $this->isIsLinksInIncluded())) { - $selfLink = $this->getRelationshipSelfLink($author, Author::LINK_COMMENTS); - $links = [ - Author::LINK_COMMENTS => [self::LINKS => [LinkInterface::SELF => $selfLink], self::SHOW_DATA => false], - ]; - } else { - $links = [ - Author::LINK_COMMENTS => [ - // closures for data are supported as well - self::DATA => function () use ($author) { - return isset($author->{Author::LINK_COMMENTS}) ? $author->{Author::LINK_COMMENTS} : null; - }, - ], - ]; - } - - // NOTE: The line(s) below for testing purposes only. Not for production. - $this->fixLinks($author, $links); - - return $links; - } -} diff --git a/tests/Data/Collection.php b/tests/Data/Collection.php index 10980ab5..614396a8 100644 --- a/tests/Data/Collection.php +++ b/tests/Data/Collection.php @@ -1,7 +1,9 @@ -{Comment::ATTRIBUTE_ID}; - } - - /** - * @inheritdoc - */ - public function getAttributes($comment, array $fieldKeysFilter = null): ?array - { - return [ - Comment::ATTRIBUTE_BODY => $comment->{Comment::ATTRIBUTE_BODY}, - ]; - } - - /** - * @inheritdoc - */ - public function getRelationships($comment, bool $isPrimary, array $includeRelationships): ?array - { - assert($comment instanceof Comment); - - if (isset($includeRelationships[Comment::LINK_AUTHOR]) === true) { - $data = $comment->{Comment::LINK_AUTHOR}; - } else { - // issue #75 https://github.com/neomerx/json-api/issues/75 - // as author will not be included as full resource let's replace it with just identity (type + id) - /** @var Author $author */ - $author = $comment->{Comment::LINK_AUTHOR}; - - if ($author !== null) { - $authorId = $author->{Author::ATTRIBUTE_ID}; - $authorIdentity = Author::instance($authorId, null, null); - } else { - $authorIdentity = null; - } - - $data = $authorIdentity; - } - - if (($isPrimary && $this->isIsLinksInPrimary()) || (!$isPrimary && $this->isIsLinksInIncluded())) { - $selfLink = $this->getRelationshipSelfLink($comment, Comment::LINK_AUTHOR); - $links = [ - Comment::LINK_AUTHOR => [self::LINKS => [LinkInterface::SELF => $selfLink], self::SHOW_DATA => false], - ]; - } else { - $links = [ - Comment::LINK_AUTHOR => [self::DATA => $data], - ]; - } - - // NOTE: The line(s) below for testing purposes only. Not for production. - $this->fixLinks($comment, $links); - - return $links; - } - - /** - * @inheritdoc - */ - public function getIncludedResourceLinks($resource): array - { - $links = [ - LinkInterface::SELF => $this->getSelfSubLink($resource), - ]; - - return $links; - } -} diff --git a/tests/Data/DevSchema.php b/tests/Data/DevSchema.php deleted file mode 100644 index f49c39ce..00000000 --- a/tests/Data/DevSchema.php +++ /dev/null @@ -1,239 +0,0 @@ -relationshipsMeta ?: parent::getRelationshipsPrimaryMeta($resource); - } - - /** - * @inheritdoc - */ - public function getRelationshipsInclusionMeta($resource) - { - return $this->relationshipsMeta ?: parent::getRelationshipsInclusionMeta($resource); - } - - /** - * @inheritdoc - */ - public function getResourceLinks($resource): array - { - if (($linksClosure = $this->resourceLinksClosure) === null) { - return parent::getResourceLinks($resource); - } else { - return $linksClosure($resource); - } - } - - /** - * @param array $relationshipMeta - */ - public function setRelationshipsMeta($relationshipMeta) - { - $this->relationshipsMeta = $relationshipMeta; - } - - /** - * @param Closure $linksClosure - */ - public function setResourceLinksClosure(Closure $linksClosure) - { - $this->resourceLinksClosure = $linksClosure; - } - - /** - * Add to 'add to link' list. - * - * @param string $name - * @param string $key - * @param mixed $value - * - * @return void - */ - public function linkAddTo($name, $key, $value) - { - assert(is_string($name) && is_string($key)); - $this->linkAddTo[] = [$name, $key, $value]; - } - - /** - * Add to 'remove from link' list. - * - * @param string $name - * @param string $key - * - * @return void - */ - public function linkRemoveFrom($name, $key) - { - assert(is_string($name) && is_string($key)); - $this->linkRemoveFrom[] = [$name, $key]; - } - - /** - * Add to 'remove link' list. - * - * @param string $name - * - * @return void - */ - public function linkRemove($name) - { - assert(is_string($name)); - $this->linkRemove[] = $name; - } - - /** - * Get include paths. - * - * @return string[] - */ - public function getIncludePaths(): array - { - return empty($this->includePaths) === false ? $this->includePaths : parent::getIncludePaths(); - } - - /** - * Set include paths. - * - * @param string[] $includePaths - */ - public function setIncludePaths($includePaths) - { - $this->includePaths = $includePaths; - } - - /** - * @return boolean - */ - public function isIsLinksInPrimary() - { - return $this->isLinksInPrimary; - } - - /** - * @param boolean $isLinksInPrimary - */ - public function setIsLinksInPrimary($isLinksInPrimary) - { - $this->isLinksInPrimary = $isLinksInPrimary; - } - - /** - * @return boolean - */ - public function isIsLinksInIncluded() - { - return $this->isLinksInIncluded; - } - - /** - * @param boolean $isLinksInIncluded - */ - public function setIsLinksInIncluded($isLinksInIncluded) - { - $this->isLinksInIncluded = $isLinksInIncluded; - } - - /** - * Add/remove values in input array. - * - * @param object $resource - * - * @param array $links - */ - protected function fixLinks($resource, array &$links) - { - foreach ($this->linkAddTo as list($name, $key, $value)) { - if ($key === self::LINKS) { - foreach ($value as $linkKey => $linkOrClosure) { - $link = $linkOrClosure instanceof Closure ? $linkOrClosure($this, $resource) : $linkOrClosure; - $links[$name][$key][$linkKey] = $link; - } - } else { - $links[$name][$key] = $value; - } - } - - foreach ($this->linkRemoveFrom as list($name, $key)) { - unset($links[$name][$key]); - } - - foreach ($this->linkRemove as $key) { - unset($links[$key]); - } - } -} diff --git a/tests/Data/DummySchema.php b/tests/Data/DummySchema.php deleted file mode 100644 index 15ce02dc..00000000 --- a/tests/Data/DummySchema.php +++ /dev/null @@ -1,49 +0,0 @@ -{static::IDENTIFIER_META} = $meta; + + return $this; + } + + /** + * @param mixed $meta + * + * @return self + */ + public function setResourceMeta($meta): self + { + $this->{static::RESOURCE_META} = $meta; + + return $this; + } } diff --git a/tests/Data/AuthorCModel.php b/tests/Data/Models/AuthorCModel.php similarity index 77% rename from tests/Data/AuthorCModel.php rename to tests/Data/Models/AuthorCModel.php index 07c4bf13..3cbb1149 100644 --- a/tests/Data/AuthorCModel.php +++ b/tests/Data/Models/AuthorCModel.php @@ -1,7 +1,9 @@ -index = $index; + } + + /** + * Get identifier's type. + * + * @return string + */ + public function getType(): string + { + return 'people'; + } + + /** + * @inheritdoc + */ + public function getId(): string + { + return $this->index; + } + + /** + * @inheritdoc + */ + public function hasIdentifierMeta(): bool + { + return $this->hasMeta; + } + + /** + * @inheritdoc + */ + public function getIdentifierMeta() + { + return $this->meta; + } + + /** + * @param mixed $meta + * + * @return AuthorIdentity + */ + public function setMeta($meta) + { + $this->meta = $meta; + $this->hasMeta = true; + + return $this; + } +} diff --git a/tests/Data/Comment.php b/tests/Data/Models/Comment.php similarity index 76% rename from tests/Data/Comment.php rename to tests/Data/Models/Comment.php index 4f74b421..d3d68b14 100644 --- a/tests/Data/Comment.php +++ b/tests/Data/Models/Comment.php @@ -1,7 +1,9 @@ -{self::ATTRIBUTE_ID} = $identity; diff --git a/tests/Data/Site.php b/tests/Data/Models/Site.php similarity index 76% rename from tests/Data/Site.php rename to tests/Data/Models/Site.php index 81e1b1f9..0714565d 100644 --- a/tests/Data/Site.php +++ b/tests/Data/Models/Site.php @@ -1,7 +1,9 @@ -{Post::ATTRIBUTE_ID}; - } - - /** - * @inheritdoc - */ - public function getAttributes($post, array $fieldKeysFilter = null): ?array - { - assert($post instanceof Post); - - return [ - Post::ATTRIBUTE_TITLE => $post->{Post::ATTRIBUTE_TITLE}, - Post::ATTRIBUTE_BODY => $post->{Post::ATTRIBUTE_BODY}, - ]; - } - - /** - * @inheritdoc - */ - public function getRelationships($post, bool $isPrimary, array $includeRelationships): ?array - { - assert($post instanceof Post); - - if (($isPrimary && $this->isIsLinksInPrimary()) || (!$isPrimary && $this->isIsLinksInIncluded())) { - $authorSelfLink = $this->getRelationshipSelfLink($post, Post::LINK_AUTHOR); - $commentsSelfLink = $this->getRelationshipSelfLink($post, Post::LINK_COMMENTS); - $links = [ - Post::LINK_AUTHOR => - [self::LINKS => [LinkInterface::SELF => $authorSelfLink], self::SHOW_DATA => false], - Post::LINK_COMMENTS => - [self::LINKS => [LinkInterface::SELF => $commentsSelfLink], self::SHOW_DATA => false], - ]; - } else { - $links = [ - Post::LINK_AUTHOR => [self::DATA => $post->{Post::LINK_AUTHOR}], - Post::LINK_COMMENTS => [self::DATA => $post->{Post::LINK_COMMENTS}], - ]; - } - - // NOTE: The line(s) below for testing purposes only. Not for production. - $this->fixLinks($post, $links); - - return $links; - } -} diff --git a/tests/Data/Schemas/AuthorCModelSchema.php b/tests/Data/Schemas/AuthorCModelSchema.php new file mode 100644 index 00000000..f5ced688 --- /dev/null +++ b/tests/Data/Schemas/AuthorCModelSchema.php @@ -0,0 +1,84 @@ + $resource[AuthorCModel::ATTRIBUTE_FIRST_NAME], + AuthorCModel::ATTRIBUTE_LAST_NAME => $resource[AuthorCModel::ATTRIBUTE_LAST_NAME], + ]; + } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + assert($resource instanceof AuthorCModel); + + if (array_key_exists(AuthorCModel::LINK_COMMENTS, (array)$resource) === true) { + $description = [self::RELATIONSHIP_DATA => $resource[AuthorCModel::LINK_COMMENTS]]; + } else { + $selfLink = $this->getRelationshipSelfLink($resource, AuthorCModel::LINK_COMMENTS); + $description = [self::RELATIONSHIP_LINKS => [LinkInterface::SELF => $selfLink]]; + } + + // NOTE: The `fixing` thing is for testing purposes only. Not for production. + return $this->fixDescriptions( + $resource, + [ + AuthorCModel::LINK_COMMENTS => $description, + ] + ); + } +} diff --git a/tests/Data/Schemas/AuthorSchema.php b/tests/Data/Schemas/AuthorSchema.php new file mode 100644 index 00000000..f9ca28e2 --- /dev/null +++ b/tests/Data/Schemas/AuthorSchema.php @@ -0,0 +1,124 @@ +{Author::ATTRIBUTE_ID}; + + return $index === null ? $index : (string)$index; + } + + /** + * @inheritdoc + */ + public function getAttributes($resource): iterable + { + assert($resource instanceof Author); + + return [ + Author::ATTRIBUTE_FIRST_NAME => $resource->{Author::ATTRIBUTE_FIRST_NAME}, + Author::ATTRIBUTE_LAST_NAME => $resource->{Author::ATTRIBUTE_LAST_NAME}, + ]; + } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + assert($resource instanceof Author); + + if (property_exists($resource, Author::LINK_COMMENTS) === true) { + $description = [self::RELATIONSHIP_DATA => $resource->{Author::LINK_COMMENTS}]; + } else { + $selfLink = $this->getRelationshipSelfLink($resource, Author::LINK_COMMENTS); + $description = [self::RELATIONSHIP_LINKS => [LinkInterface::SELF => $selfLink]]; + } + + // NOTE: The `fixing` thing is for testing purposes only. Not for production. + return $this->fixDescriptions( + $resource, + [ + Author::LINK_COMMENTS => $description, + ] + ); + } + + /** + * @inheritdoc + */ + public function hasIdentifierMeta($resource): bool + { + assert($resource instanceof Author); + + return parent::hasIdentifierMeta($resource) || property_exists($resource, Author::IDENTIFIER_META); + } + + /** + * @inheritdoc + */ + public function getIdentifierMeta($resource) + { + assert($resource instanceof Author); + + return $resource->{Author::IDENTIFIER_META}; + } + + /** + * @inheritdoc + */ + public function hasResourceMeta($resource): bool + { + assert($resource instanceof Author); + + return parent::hasResourceMeta($resource) || property_exists($resource, Author::RESOURCE_META); + } + + /** + * @inheritdoc + */ + public function getResourceMeta($resource) + { + assert($resource instanceof Author); + + return $resource->{Author::RESOURCE_META}; + } +} diff --git a/tests/Data/Schemas/CommentSchema.php b/tests/Data/Schemas/CommentSchema.php new file mode 100644 index 00000000..232bc136 --- /dev/null +++ b/tests/Data/Schemas/CommentSchema.php @@ -0,0 +1,75 @@ +{Comment::ATTRIBUTE_ID}; + + return $index === null ? $index : (string)$index; + } + + /** + * @inheritdoc + */ + public function getAttributes($resource): iterable + { + assert($resource instanceof Comment); + + return [ + Comment::ATTRIBUTE_BODY => $resource->{Comment::ATTRIBUTE_BODY}, + ]; + } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + assert($resource instanceof Comment); + + // NOTE: The `fixing` thing is for testing purposes only. Not for production. + return $this->fixDescriptions( + $resource, + [ + Comment::LINK_AUTHOR => [self::RELATIONSHIP_DATA => $resource->{Comment::LINK_AUTHOR}], + ] + ); + } +} diff --git a/tests/Data/Schemas/DevSchema.php b/tests/Data/Schemas/DevSchema.php new file mode 100644 index 00000000..7db24b29 --- /dev/null +++ b/tests/Data/Schemas/DevSchema.php @@ -0,0 +1,225 @@ +resourceLinksClosure) === null) { + return parent::getLinks($resource); + } else { + return $linksClosure($resource); + } + } + + /** + * @param Closure $linksClosure + * + * @return void + */ + public function setResourceLinksClosure(Closure $linksClosure): void + { + $this->resourceLinksClosure = $linksClosure; + } + + /** + * Add value to relationship description. + * + * @param string $name Relationship name. + * @param int $key Description key. + * @param mixed $value Value to add (might be array of links). + * + * @return void + */ + public function addToRelationship(string $name, int $key, $value): void + { + $this->addToRelationship[] = [$name, $key, $value]; + } + + /** + * Remove from relationship description. + * + * @param string $name Relationship name. + * @param int $key Description key. + * + * @return void + */ + public function removeFromRelationship(string $name, int $key): void + { + $this->removeFromRelationship[] = [$name, $key]; + } + + /** + * Remove entire relationship from description. + * + * @param string $name Relationship name. + * + * @return void + */ + public function removeRelationship(string $name): void + { + assert(is_string($name)); + $this->relationshipToRemove[] = $name; + } + + /** + * @param mixed $resource + * + * @return string + */ + public function getSelfSubUrl($resource): string + { + return parent::getSelfSubUrl($resource); + } + + /** + * Hide `self` link in relationship. + * + * @param string $name + */ + public function hideSelfLinkInRelationship(string $name): void + { + $this->addToRelationship($name, AuthorSchema::RELATIONSHIP_LINKS_SELF, false); + } + + /** + * Set custom `self` link in relationship. + * + * @param string $name + * @param LinkInterface $link + * + * @return void + */ + public function setSelfLinkInRelationship(string $name, LinkInterface $link): void + { + $this->addToRelationship($name, AuthorSchema::RELATIONSHIP_LINKS_SELF, $link); + } + + /** + * Hide `related` link in relationship. + * + * @param string $name + */ + public function hideRelatedLinkInRelationship(string $name): void + { + $this->addToRelationship($name, AuthorSchema::RELATIONSHIP_LINKS_RELATED, false); + } + + /** + * Set custom `related` link in relationship. + * + * @param string $name + * @param LinkInterface $link + * + * @return void + */ + public function setRelatedLinkInRelationship(string $name, LinkInterface $link): void + { + $this->addToRelationship($name, AuthorSchema::RELATIONSHIP_LINKS_RELATED, $link); + } + + /** + * @param string $name + */ + public function hideDefaultLinksInRelationship(string $name): void + { + $this->hideSelfLinkInRelationship($name); + $this->hideRelatedLinkInRelationship($name); + } + + /** + * Hide `links` section for resource. + */ + public function hideResourceLinks(): void + { + $this->setResourceLinksClosure( + function (): array { + return []; + } + ); + } + + /** + * Add/remove values in input array. + * + * @param object $resource + * @param array $descriptions + * + * @return array + */ + protected function fixDescriptions($resource, array $descriptions): array + { + foreach ($this->addToRelationship as list($name, $key, $value)) { + if ($key === self::RELATIONSHIP_LINKS) { + foreach ($value as $linkKey => $linkOrClosure) { + $link = $linkOrClosure instanceof Closure ? $linkOrClosure( + $this, + $resource + ) : $linkOrClosure; + $descriptions[$name][$key][$linkKey] = $link; + } + } else { + $descriptions[$name][$key] = $value; + } + } + + foreach ($this->removeFromRelationship as list($name, $key)) { + unset($descriptions[$name][$key]); + } + + foreach ($this->relationshipToRemove as $key) { + unset($descriptions[$key]); + } + + return $descriptions; + } +} diff --git a/tests/Data/Schemas/PostSchema.php b/tests/Data/Schemas/PostSchema.php new file mode 100644 index 00000000..cd6be17a --- /dev/null +++ b/tests/Data/Schemas/PostSchema.php @@ -0,0 +1,92 @@ +{Post::ATTRIBUTE_ID}; + + return $index === null ? $index : (string)$index; + } + + /** + * @inheritdoc + */ + public function getAttributes($resource): iterable + { + assert($resource instanceof Post); + + return [ + Post::ATTRIBUTE_TITLE => $resource->{Post::ATTRIBUTE_TITLE}, + Post::ATTRIBUTE_BODY => $resource->{Post::ATTRIBUTE_BODY}, + ]; + } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + assert($resource instanceof Post); + + if (property_exists($resource, Post::LINK_AUTHOR) === true) { + $authorDescription = [self::RELATIONSHIP_DATA => $resource->{Post::LINK_AUTHOR}]; + } else { + $selfLink = $this->getRelationshipSelfLink($resource, Post::LINK_AUTHOR); + $authorDescription = [self::RELATIONSHIP_LINKS => [LinkInterface::SELF => $selfLink]]; + } + + if (property_exists($resource, Post::LINK_COMMENTS) === true) { + $commentsDescription = [self::RELATIONSHIP_DATA => $resource->{Post::LINK_COMMENTS}]; + } else { + $selfLink = $this->getRelationshipSelfLink($resource, Post::LINK_COMMENTS); + $commentsDescription = [self::RELATIONSHIP_LINKS => [LinkInterface::SELF => $selfLink]]; + } + + // NOTE: The `fixing` thing is for testing purposes only. Not for production. + return $this->fixDescriptions( + $resource, + [ + Post::LINK_AUTHOR => $authorDescription, + Post::LINK_COMMENTS => $commentsDescription, + ] + ); + } +} diff --git a/tests/Data/Schemas/SiteSchema.php b/tests/Data/Schemas/SiteSchema.php new file mode 100644 index 00000000..6897ba1c --- /dev/null +++ b/tests/Data/Schemas/SiteSchema.php @@ -0,0 +1,83 @@ +{Site::ATTRIBUTE_ID}; + + return $index === null ? $index : (string)$index; + } + + /** + * @inheritdoc + */ + public function getAttributes($resource): iterable + { + assert($resource instanceof Site); + + return [ + Site::ATTRIBUTE_NAME => $resource->{Site::ATTRIBUTE_NAME}, + ]; + } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + assert($resource instanceof Site); + + if (property_exists($resource, Site::LINK_POSTS) === true) { + $description = [self::RELATIONSHIP_DATA => $resource->{Site::LINK_POSTS}]; + } else { + $selfLink = $this->getRelationshipSelfLink($resource, Site::LINK_POSTS); + $description = [self::RELATIONSHIP_LINKS => [LinkInterface::SELF => $selfLink]]; + } + + // NOTE: The `fixing` thing is for testing purposes only. Not for production. + return $this->fixDescriptions( + $resource, + [ + Site::LINK_POSTS => $description, + ] + ); + } +} diff --git a/tests/Data/SiteSchema.php b/tests/Data/SiteSchema.php deleted file mode 100644 index 7e9be9a7..00000000 --- a/tests/Data/SiteSchema.php +++ /dev/null @@ -1,89 +0,0 @@ -setIncludePaths([ - Site::LINK_POSTS, - Site::LINK_POSTS . '.' . Post::LINK_AUTHOR, - Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, - ]); - } - - /** - * @inheritdoc - */ - public function getId($site): ?string - { - return $site->{Site::ATTRIBUTE_ID}; - } - - /** - * @inheritdoc - */ - public function getAttributes($site, array $fieldKeysFilter = null): array - { - assert($site instanceof Site); - - return [ - Site::ATTRIBUTE_NAME => $site->{Site::ATTRIBUTE_NAME}, - ]; - } - - /** - * @inheritdoc - */ - public function getRelationships($site, bool $isPrimary, array $includeRelationships): ?array - { - assert($site instanceof Site); - - if (($isPrimary && $this->isIsLinksInPrimary()) || (!$isPrimary && $this->isIsLinksInIncluded())) { - $selfLink = $this->getRelationshipSelfLink($site, Site::LINK_POSTS); - $links = [ - Site::LINK_POSTS => [self::LINKS => [LinkInterface::SELF => $selfLink], self::SHOW_DATA => false], - ]; - } else { - $links = [ - Site::LINK_POSTS => [self::DATA => $site->{Site::LINK_POSTS}], - ]; - } - - // NOTE: The line(s) below for testing purposes only. Not for production. - $this->fixLinks($site, $links); - - return $links; - } -} diff --git a/tests/Document/DocumentTest.php b/tests/Document/DocumentTest.php deleted file mode 100644 index 0c6beb69..00000000 --- a/tests/Document/DocumentTest.php +++ /dev/null @@ -1,1289 +0,0 @@ -documentFactory = $this->schemaFactory = new Factory(); - - $this->document = $this->documentFactory->createDocument(); - } - - /** - * Test set document links. - */ - public function testSetDocumentLinks() - { - $this->document->setDocumentLinks([ - Link::SELF => new Link($selfUrl = 'selfUrl'), - Link::FIRST => new Link($firstUrl = 'firstUrl'), - Link::LAST => new Link($lastUrl = 'lastUrl'), - Link::PREV => new Link($prevUrl = 'prevUrl'), - Link::NEXT => new Link($nextUrl = 'nextUrl'), - ]); - - $expected = <<check($expected); - } - - /** - * Test set document meta. - */ - public function testSetMetaToDocument() - { - $this->document->setMetaToDocument([ - "copyright" => "Copyright 2015 Example Corp.", - "authors" => [ - "Yehuda Katz", - "Steve Klabnik", - "Dan Gebhardt" - ] - ]); - - $expected = <<check($expected); - } - - /** - * Test add to 'data' section. Resource in array. Members are shown. - */ - public function testAddToDataArrayedShowMembers() - { - $this->document->addToData($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('selfUrl'), - [LinkInterface::SELF => new Link('selfUrl')], // links for resource - ['some' => 'meta'] - ), new stdClass(), true)); - $this->document->setResourceCompleted($resource); - - $expected = <<check($expected); - } - - /** - * Test add to 'data' section. Resource not in array. Members are hidden. - */ - public function testAddToDataNotArrayedHiddenMembers() - { - $this->document->addToData($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - null, // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $this->document->setResourceCompleted($resource); - - $expected = <<check($expected); - } - - /** - * Test set [] to 'data' section. - */ - public function testSetEmptyData() - { - $this->document->setEmptyData(); - - $expected = <<check($expected); - } - - /** - * Test set null to 'data' section. - */ - public function testSetNullData() - { - $this->document->setNullData(); - - $expected = <<check($expected); - } - - /** - * Test add link to 'data' section. Show link members. - */ - public function testAddLinkToDataShowLinkMembers() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'comments', - '321', - null, // attributes - new Link('commentsSelfUrl/'), - [LinkInterface::SELF => new Link('commentsSelfUrl/')], // links for resource - ['this meta' => 'wont be included'], - [], // links for included resource - false, - null, - ['some' => 'comment meta'] - ), new stdClass(), false); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [ - LinkInterface::SELF => $this->createLink('selfSubUrl'), - LinkInterface::RELATED => $this->createLink('relatedSubUrl'), - LinkInterface::FIRST => new Link('/first', null, true), - ], - ['some' => 'relationship meta'], - true, - false // is root - ); - $this->document->addRelationshipToData($parent, $link, $resource); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add link to 'data' section. Hide link members except relationships. - */ - public function testAddLinkToDataHideLinkMembersExceptRelationships() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('selfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'comments', - '321', - null, // attributes - new Link('commentsSelfUrl/'), - [LinkInterface::SELF => new Link('commentsSelfUrl/')], // links for resource - ['this meta' => 'wont be shown'], // meta when resource is primary - [], // links for included resource - false, - ['this meta' => 'wont be shown'], // meta when resource within 'included' - ['some' => 'comment meta'] // meta when resource is in relationship - ), new stdClass(), false); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - $this->document->addRelationshipToData($parent, $link, $resource); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add multiple items to relationship's 'data' section. Hide link members except linkage. - */ - public function testAddMultipleRelationshipItemsToData() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('selfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'comments', - '321', - null, // attributes - new Link('selfUrlWillBeHidden/'), - [LinkInterface::SELF => new Link('selfUrlWillBeHidden/')], // links for resource - ['this meta' => 'wont be shown'], // meta when resource is primary - [], // links for included resource - false, // show relationships in 'included' - ['this meta' => 'wont be shown'], // meta when resource within 'included' - ['some' => 'comment meta'] // meta when resource is in relationship - ), new stdClass(), true); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - $this->document->addRelationshipToData($parent, $link, $resource); - $this->document->addRelationshipToData($parent, $link, $resource); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add link to 'data' section. Hide link members except meta. - */ - public function testAddLinkToDataHideLinkMembersExceptMeta() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('selfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'comments', - '321', - null, // attributes - new Link('selfUrlWillBeHidden/'), - [LinkInterface::SELF => new Link('selfUrlWillBeHidden/')], // links for resource - ['this meta' => 'wont be shown'], // meta when resource is primary - [], // links for included resource - false, // show relationships in 'included' - ['this meta' => 'wont be shown'], // meta when resource within 'included' - ['some' => 'comment meta'] // meta when resource is in relationship - ), new stdClass(), false); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - ['some' => 'relationship meta'], // relationship meta - false, // show data - false // is root - ); - - $this->document->addRelationshipToData($parent, $link, $resource); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add empty (empty array) link to 'data' section. - */ - public function testAddEmptyLinkToData() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'relationship-name', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - ['some' => 'relationship meta'], // relationship meta - true, // show data - false // is root - ); - - $this->document->addEmptyRelationshipToData($parent, $link); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add null link to 'data' section. - */ - public function testAddNullLinkToData() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'relationship-name', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - - $this->document->addNullRelationshipToData($parent, $link); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add to 'included' section. Members are shown. - */ - public function testAddToIncludedShowMembers() - { - $this->document->addToIncluded($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null, // meta - [LinkInterface::SELF => new Link('peopleSelfUrl/')], // links for included resource - false, // show 'relationships' in 'included' - ['some' => 'meta'] // meta when resource within 'included' - ), new stdClass(), false)); - - $this->document->setResourceCompleted($resource); - - $expected = <<check($expected); - } - - /** - * Test add to 'included' section. Members are hidden. - */ - public function testAddToIncludedHideMembers() - { - $this->document->addToIncluded($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $this->document->setResourceCompleted($resource); - - $expected = <<check($expected); - } - - /** - * Test add link to 'included' section. Show link members. - */ - public function testAddLinkToIncludedShowLinkMembers() - { - $this->document->addToIncluded($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl'), // self url - [], // links for resource - ['this meta' => 'wont be shown'], // meta when primary resource - [LinkInterface::SELF => new Link('peopleSelfUrl')], // links for included resource - false, // show 'relationships' in 'included' - ['some' => 'author meta'] // meta when resource within 'included' - ), new stdClass(), false)); - - $resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'comments', - '321', - null, // attributes - new Link('selfUrlWillBeHidden/'), - [LinkInterface::SELF => new Link('selfUrlWillBeHidden/')], // links for resource - ['this meta' => 'wont be shown'], // meta when resource is primary - [], // links for included resource - false, // show relationships in 'included' - ['this meta' => 'wont be shown'], // meta when resource within 'included' - ['some' => 'comment meta'] // meta when resource is in relationship - ), new stdClass(), false); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [ - LinkInterface::SELF => new Link('selfSubUrl'), - LinkInterface::RELATED => new Link('relatedSubUrl'), - ], - ['some' => 'relationship meta'], // relationship meta - true, // show data - false // is root - ); - - $this->document->addRelationshipToIncluded($parent, $link, $resource); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add link to 'included' section. Hide members for linked resource. - */ - public function testAddLinkToIncludedHideMembersForLinkedResource() - { - $this->document->addToIncluded($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - ['this meta' => 'wont be shown'], // meta when primary resource - [LinkInterface::SELF => new Link('peopleSelfUrl/')], // links for included resource - false, // show 'relationships' in 'included' - ['some' => 'author meta'] // meta when resource within 'included' - ), new stdClass(), false)); - - $resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'comments', - '321', - null, // attributes - new Link('selfUrlWillBeHidden/'), - [LinkInterface::SELF => new Link('selfUrlWillBeHidden/')], // links for resource - ['this meta' => 'wont be shown'], // meta when resource is primary - [], // links for included resource - false, // show relationships in 'included' - ['this meta' => 'wont be shown'], // meta when resource within 'included' - ['some' => 'comment meta'] // meta when resource is in relationship - ), new stdClass(), false); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - - $this->document->addRelationshipToIncluded($parent, $link, $resource); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add empty (empty array) link to 'included' section. - */ - public function testAddEmptyLinkToIncluded() - { - $this->document->addToIncluded($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - ['this meta' => 'wont be shown'], // meta when primary resource - [LinkInterface::SELF => new Link('peopleSelfUrl/')], // links for included resource - false, // show 'relationships' in 'included' - ['some' => 'author meta'] // meta when resource within 'included' - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - - $this->document->addEmptyRelationshipToIncluded($parent, $link); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add null link to 'included' section. - */ - public function testAddNullLinkToIncluded() - { - $this->document->addToIncluded($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - ['this meta' => 'wont be shown'], // meta when primary resource - [LinkInterface::SELF => new Link('peopleSelfUrl/')], // links for included resource - false, // show 'relationships' in 'included' - ['some' => 'author meta'] // meta when resource within 'included' - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'comments-relationship', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - - $this->document->addNullRelationshipToIncluded($parent, $link); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add type and id only. This functionality is required for replies on 'request links'. - */ - public function testAddTypeAndIdOnly() - { - $this->document->addToData($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - null, // attributes - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null // meta when primary resource - ), new stdClass(), false)); - $this->document->setResourceCompleted($resource); - - $expected = <<check($expected); - } - - /** - * Test add error. - */ - public function testAddError() - { - // First add something to document. When error is added nothing except errors must be in result - $this->document->setDocumentLinks([Link::SELF => new Link('selfUrl')]); - - $this->document->addError($this->documentFactory->createError( - 'some-id', - new Link('about-link'), - 'some-status', - 'some-code', - 'some-title', - 'some-detail', - ['source' => 'data'], - ['meta' => 'data'] - )); - - $expected = <<check($expected); - } - - /** - * Test add error. - */ - public function testAddErrorWithIntegerStatusAndCode() - { - // First add something to document. When error is added nothing except errors must be in result - $this->document->setDocumentLinks([Link::SELF => new Link('selfUrl')]); - - $this->document->addError($this->documentFactory->createError( - 'some-id', - new Link('about-link'), - 500, - 1337, - 'some-title', - 'some-detail', - ['source' => 'data'], - ['meta' => 'data'] - )); - - $expected = <<check($expected); - } - - /** - * Test add JSON API version info. - */ - public function testAddVersion() - { - $this->document->addJsonApiVersion('1.0', ['some' => 'meta']); - $this->document->unsetData(); - - $expected = <<check($expected); - } - - /** - * Test unset data. - */ - public function testUnsetData() - { - $this->document->setMetaToDocument([ - "some" => "values", - ]); - - $this->document->addToData($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - null, // attributes - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null // meta when primary resource - ), new stdClass(), false)); - $this->document->setResourceCompleted($resource); - - $this->document->unsetData(); - - $expected = <<check($expected); - } - - /** - * Test add meta information to relationships. - */ - public function testRelationshipsPrimaryMeta() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null, // meta - [], // links for included resource - false, // show relationships in included - null, // inclusion meta - null, // relationship meta - [], // include paths - true, // show attributes in included - ['some' => 'relationships meta'] // relationships primary meta - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'relationship-name', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - - $this->document->addNullRelationshipToData($parent, $link); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test add meta information to relationships. - */ - public function testRelationshipsInclusionMeta() - { - $this->document->addToIncluded($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null, // meta - [], // links for included resource - false, // show relationships in included - null, // inclusion meta - null, // relationship meta - [], // include paths - true, // show attributes in included - null, // relationships primary meta - ['some' => 'relationships meta'] // relationships inclusion meta - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'relationship-name', - new stdClass(), // in reality it will be a Comment class instance where $resource properties were taken from - [], // links - null, // relationship meta - true, // show data - false // is root - ); - - $this->document->addNullRelationshipToIncluded($parent, $link); - $this->document->setResourceCompleted($parent); - - $expected = <<check($expected); - } - - /** - * Test relationship with invalid name. - * - * @expectedException \InvalidArgumentException - */ - public function testAddRelationshipWithInvalidName() - { - $this->document->addToData($parent = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'lastName' => 'Dow'], - new Link('peopleSelfUrl/'), // self url - [], // links for resource - null // meta - ), new stdClass(), false)); - - $link = $this->schemaFactory->createRelationshipObject( - 'self', // <-- 'self' is a reserved word and cannot be used as a name - new stdClass(), - [], // links - ['some' => 'relationship meta'], // relationship meta - true, // show data - false // is root - ); - - $this->document->addEmptyRelationshipToData($parent, $link); - } - - /** - * Test invalid name for resource attributes. - * - * @expectedException \InvalidArgumentException - */ - public function testInvalidNamesForResourceAttributesId() - { - $this->document->addToData($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'id' => 'whatever'], // <-- 'id' is a reserved word and cannot be used - new Link('selfUrl'), - [LinkInterface::SELF => new Link('selfUrl')], // links for resource - ['some' => 'meta'] - ), new stdClass(), true)); - } - - /** - * Test invalid name for resource attributes. - * - * @expectedException \InvalidArgumentException - */ - public function testInvalidNamesForResourceAttributesType() - { - $this->document->addToData($resource = $this->schemaFactory->createResourceObject($this->getSchema( - 'people', - '123', - ['firstName' => 'John', 'type' => 'whatever'], // <-- 'type' is a reserved word and cannot be used - new Link('selfUrl'), - [LinkInterface::SELF => new Link('selfUrl')], // links for resource - ['some' => 'meta'] - ), new stdClass(), true)); - } - - /** - * @param string $subHref - * - * @return LinkInterface - */ - private function createLink($subHref) - { - return $this->schemaFactory->createLink($subHref); - } - - /** - * @param string $expected - */ - private function check($expected) - { - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - $actual = json_encode($this->document->getDocument()); - $this->assertEquals($expected, $actual); - } - - /** - * @param string $type - * @param string $idx - * @param array|null $attributes - * @param LinkInterface|null $selfLink - * @param array $resourceLinks - * @param mixed $primaryMeta - * @param array $includedResLinks - * @param bool $relShipsInIncluded - * @param mixed $inclusionMeta - * @param mixed $relationshipMeta - * @param array $includePaths - * @param bool $showAttributesInIncluded - * @param mixed $relPrimaryMeta - * @param mixed $relIncMeta - * - * @return SchemaInterface - */ - private function getSchema( - $type, - $idx, - $attributes, - $selfLink, - array $resourceLinks, - $primaryMeta, - array $includedResLinks = [], - $relShipsInIncluded = false, - $inclusionMeta = null, - $relationshipMeta = null, - $includePaths = [], - $showAttributesInIncluded = true, - $relPrimaryMeta = null, - $relIncMeta = null - ) { - /** @var Mockery\Mock $schema */ - $schema = Mockery::mock(SchemaInterface::class); - - $schema->shouldReceive('getResourceType')->zeroOrMoreTimes()->andReturn($type); - $schema->shouldReceive('getId')->zeroOrMoreTimes()->andReturn($idx); - $schema->shouldReceive('getSelfSubLink')->zeroOrMoreTimes()->andReturn($selfLink); - $schema->shouldReceive('getAttributes')->zeroOrMoreTimes()->andReturn($attributes); - $schema->shouldReceive('getResourceLinks')->zeroOrMoreTimes()->andReturn($resourceLinks); - $schema->shouldReceive('getIncludedResourceLinks')->zeroOrMoreTimes()->andReturn($includedResLinks); - $schema->shouldReceive('isShowAttributesInIncluded')->zeroOrMoreTimes()->andReturn($showAttributesInIncluded); - $schema->shouldReceive('isShowRelationshipsInIncluded')->zeroOrMoreTimes()->andReturn($relShipsInIncluded); - $schema->shouldReceive('getIncludePaths')->zeroOrMoreTimes()->andReturn($includePaths); - $schema->shouldReceive('getPrimaryMeta')->zeroOrMoreTimes()->andReturn($primaryMeta); - $schema->shouldReceive('getLinkageMeta')->zeroOrMoreTimes()->andReturn($relationshipMeta); - $schema->shouldReceive('getInclusionMeta')->zeroOrMoreTimes()->andReturn($inclusionMeta); - $schema->shouldReceive('getRelationshipsPrimaryMeta')->zeroOrMoreTimes()->andReturn($relPrimaryMeta); - $schema->shouldReceive('getRelationshipsInclusionMeta')->zeroOrMoreTimes()->andReturn($relIncMeta); - - /** @var SchemaInterface $schema */ - - return $schema; - } -} diff --git a/tests/Document/FactoryTest.php b/tests/Document/FactoryTest.php deleted file mode 100644 index 8c79588d..00000000 --- a/tests/Document/FactoryTest.php +++ /dev/null @@ -1,78 +0,0 @@ -factory = new Factory(); - } - - /** - * Test create document. - */ - public function testCreateDocument() - { - $this->assertNotNull($this->factory->createDocument()); - } - - /** - * Test create error. - */ - public function testCreateError() - { - $this->assertNotNull($error = $this->factory->createError( - $idx = 'some-id', - $link = new Link('about-link'), - $status = 'some-status', - $code = 'some-code', - $title = 'some-title', - $detail = 'some-detail', - $source = ['source' => 'info'], - $meta = ['meta' => 'info'] - )); - - $this->assertEquals($idx, $error->getId()); - $this->assertEquals([DocumentInterface::KEYWORD_ERRORS_ABOUT => $link], $error->getLinks()); - $this->assertEquals($status, $error->getStatus()); - $this->assertEquals($code, $error->getCode()); - $this->assertEquals($title, $error->getTitle()); - $this->assertEquals($detail, $error->getDetail()); - $this->assertEquals($source, $error->getSource()); - $this->assertEquals($meta, $error->getMeta()); - } -} diff --git a/tests/Encoder/EncodeErrorsTest.php b/tests/Encoder/EncodeErrorsTest.php index be470ccd..806d5739 100644 --- a/tests/Encoder/EncodeErrorsTest.php +++ b/tests/Encoder/EncodeErrorsTest.php @@ -1,7 +1,9 @@ -getError(); $encoder = Encoder::instance(); - $actual = $encoder->encodeError($error); - + $actual = $encoder->encodeError($error); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** * Test encode error array. */ - public function testEncodeErrorsArray() + public function testEncodeErrorsArray(): void { $error = $this->getError(); $encoder = Encoder::instance(); @@ -71,7 +74,10 @@ public function testEncodeErrorsArray() { "errors":[{ "id" : "some-id", - "links" : {"about" : "about-link"}, + "links" : { + "about" : "about-link", + "type" : [{ "href": "http://example.com/errors/123", "aliases": {"v": "version"} }] + }, "status" : "some-status", "code" : "some-code", "title" : "some-title", @@ -81,18 +87,15 @@ public function testEncodeErrorsArray() }] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** * Test encode error array. */ - public function testEncodeErrorsCollection() + public function testEncodeErrorsCollection(): void { - $errors = new ErrorCollection(); + $errors = new ErrorCollection(); $errors->add($this->getError()); $encoder = Encoder::instance(); @@ -103,7 +106,10 @@ public function testEncodeErrorsCollection() { "errors":[{ "id" : "some-id", - "links" : {"about" : "about-link"}, + "links" : { + "about" : "about-link", + "type" : [{ "href": "http://example.com/errors/123", "aliases": {"v": "version"} }] + }, "status" : "some-status", "code" : "some-code", "title" : "some-title", @@ -113,10 +119,7 @@ public function testEncodeErrorsCollection() }] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -124,7 +127,7 @@ public function testEncodeErrorsCollection() * * @see https://github.com/neomerx/json-api/issues/62 */ - public function testEncodeEmptyError() + public function testEncodeEmptyError(): void { $error = new Error(); $encoder = Encoder::instance(); @@ -137,10 +140,7 @@ public function testEncodeEmptyError() ] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -148,19 +148,16 @@ public function testEncodeEmptyError() * * @see https://github.com/neomerx/json-api/issues/151 */ - public function testEncodeEmptyErrorArray() + public function testEncodeEmptyErrorArray(): void { - $actual = Encoder::instance()->encodeErrors([]); + $actual = Encoder::instance()->encodeErrors([]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -168,20 +165,21 @@ public function testEncodeEmptyErrorArray() * * @see https://github.com/neomerx/json-api/issues/171 */ - public function testEncodeErrorWithMetaAndJsonApi() + public function testEncodeErrorWithMetaAndJsonApi(): void { $error = $this->getError(); $encoder = Encoder::instance(); $actual = $encoder - ->withJsonApiVersion(['some' => 'meta']) + ->withJsonApiVersion(Encoder::JSON_API_VERSION) + ->withJsonApiMeta(['some' => 'meta']) ->withMeta(["copyright" => "Copyright 2015 Example Corp."]) ->encodeError($error); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** - * @return Error + * @return ErrorInterface */ - private function getError() + private function getError(): ErrorInterface { return new Error( 'some-id', - new Link('about-link'), + new Link(false, 'about-link', false), + [new LinkWithAliases(false, 'http://example.com/errors/123', ['v' => 'version'], false)], 'some-status', 'some-code', 'some-title', 'some-detail', ['source' => 'data'], + true, ['some' => 'meta'] ); } diff --git a/tests/Encoder/EncodeIncludedObjectsTest.php b/tests/Encoder/EncodeIncludedObjectsTest.php index 41f9194c..20f71d38 100644 --- a/tests/Encoder/EncodeIncludedObjectsTest.php +++ b/tests/Encoder/EncodeIncludedObjectsTest.php @@ -1,7 +1,9 @@ -author), Comment::instance(12, 'I like XML better', $this->author), ]; - $this->post = Post::instance( + $this->post = Post::instance( 1, 'JSON API paints my bikeshed!', 'Outside every fat man there was an even fatter man trying to close in', $this->author, $this->comments ); - $this->site = Site::instance(2, 'site name', [$this->post]); - $this->encoderOptions = new EncoderOptions(0, 'http://example.com'); + $this->site = Site::instance(2, 'site name', [$this->post]); } /** @@ -88,19 +82,26 @@ protected function setUp() */ public function testEncodeWithIncludedObjects() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => function ($factory) { - $schema = new CommentSchema($factory); - $schema->linkRemove(Comment::LINK_AUTHOR); - return $schema; - }, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->setIncludePaths([Post::LINK_COMMENTS]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->post); + $this->author->setIdentifierMeta('id meta'); + + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => function ($factory) { + $schema = new CommentSchema($factory); + $schema->removeRelationship(Comment::LINK_AUTHOR); + return $schema; + }, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->withIncludedPaths( + [Post::LINK_COMMENTS] + )->encodeData($this->post); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -160,21 +158,41 @@ public function testEncodeWithRecursiveIncludedObjects() { $this->author->{Author::LINK_COMMENTS} = $this->comments; - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site, new EncodingParameters( - // include only this relation (according to the spec intermediate will be included as well) - [Site::LINK_POSTS . '.' . Post::LINK_COMMENTS], - // include only these attributes and links + $actual = Encoder::instance( [ - 'comments' => [Comment::ATTRIBUTE_BODY, Comment::LINK_AUTHOR], - 'posts' => [Post::LINK_COMMENTS], - 'sites' => [Site::LINK_POSTS], + Author::class => AuthorSchema::class, + Comment::class => function ($factory) { + $schema = new CommentSchema($factory); + $schema->hideDefaultLinksInRelationship(Comment::LINK_AUTHOR); + return $schema; + }, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, ] - )); + ) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths( + [ + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, + ] + )->withFieldSets( + [ + // include only these attributes and links (note we specify relationships for linkage, + // otherwise those intermediate resources will not be included in the output) + 'comments' => [Comment::ATTRIBUTE_BODY, Comment::LINK_AUTHOR], + 'posts' => [Post::LINK_COMMENTS], + 'sites' => [Site::LINK_POSTS], + ] + )->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -249,12 +267,22 @@ public function testEncodeWithNullAndEmptyLinks() $this->post->{Post::LINK_AUTHOR} = null; $this->post->{Post::LINK_COMMENTS} = []; - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site); + $actual = Encoder::instance( + [ + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->withIncludedPaths([Site::LINK_POSTS])->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -306,12 +331,22 @@ public function testEncodeDuplicatesWithCyclicDeps() // Note: Will use existing link (in schema) but set to 'wrong' type and let's see if it can handle it correctly. $this->post->{Post::LINK_AUTHOR} = $this->post; - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site); + $actual = Encoder::instance( + [ + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->withIncludedPaths([Site::LINK_POSTS])->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -361,16 +393,27 @@ public function testEncodeDuplicatesWithCyclicDeps() */ public function testEncodeLinkNonIncludableWithIncludableLinks() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => function ($factory) { - $schema = new SiteSchema($factory); - $schema->setIncludePaths([Site::LINK_POSTS]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->site); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, + ] + ) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths([Site::LINK_POSTS]) + ->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -424,23 +464,27 @@ public function testEncodeLinkNonIncludableWithIncludableLinks() */ public function testEncodeWithLinkWithPagination() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkAddTo( - Post::LINK_COMMENTS, - PostSchema::LINKS, - [ - Link::FIRST => function (PostSchema $schema, Post $post) { - return new Link($schema->getSelfSubUrl($post) . '/comments/first'); - } - ] - ); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->post); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideRelatedLinkInRelationship(Post::LINK_COMMENTS); + $schema->addToRelationship( + Post::LINK_COMMENTS, + PostSchema::RELATIONSHIP_LINKS, + [ + Link::FIRST => function (PostSchema $schema, Post $post) { + return new Link(true, $schema->getSelfSubUrl($post) . '/comments/first', false); + }, + ] + ); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->encodeData($this->post); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -484,12 +526,41 @@ public function testEncodeWithLinkWithPagination() */ public function testEncodeDeepDuplicateHierarchies() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData([$this->site, $this->site]); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + return $schema; + }, + Comment::class => function ($factory) { + $schema = new CommentSchema($factory); + $schema->hideDefaultLinksInRelationship(Comment::LINK_AUTHOR); + return $schema; + }, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, + ] + ) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths( + [ + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS . '.' . + Comment::LINK_AUTHOR . '.' . Author::LINK_COMMENTS, + ] + ) + ->encodeData([$this->site, $this->site]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -596,36 +668,45 @@ public function testEncodeWithIncludedForPolymorphicArrays() { $this->author->{Author::LINK_COMMENTS} = $this->comments; - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData([$this->site, $this->author], new EncodingParameters([ - Site::LINK_POSTS . '.' . Post::LINK_AUTHOR, - Author::LINK_COMMENTS, - ])); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + return $schema; + }, + Comment::class => function ($factory) { + $schema = new CommentSchema($factory); + $schema->hideDefaultLinksInRelationship(Comment::LINK_AUTHOR); + return $schema; + }, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, + ] + ) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths( + [ + Site::LINK_POSTS . '.' . Post::LINK_AUTHOR, + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, + Author::LINK_COMMENTS, + ] + ) + ->encodeData([$this->author, $this->site]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -716,14 +808,40 @@ public function testEncodePolymorphicRelationship() // let's hack a little bit and place additional resource(s) into relationship $this->author->{Author::LINK_COMMENTS} = array_merge([$this->site], $this->comments); - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->author, new EncodingParameters([ - Author::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR, - ])); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + return $schema; + }, + Comment::class => function ($factory) { + $schema = new CommentSchema($factory); + $schema->hideDefaultLinksInRelationship(Comment::LINK_AUTHOR); + return $schema; + }, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + Site::class => function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + return $schema; + }, + ] + ) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths( + [ + Author::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR, + ] + ) + ->encodeData($this->author); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -808,16 +923,21 @@ public function testEncodePolymorphicRelationship() */ public function testEncodeRelationshipsAsLinksDoNotFollowLinksWhenIncludePathSet() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->setIsLinksInPrimary(true); - $schema->setIncludePaths([Post::LINK_AUTHOR, Post::LINK_COMMENTS]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->post); + unset($this->post->{Post::LINK_AUTHOR}); + unset($this->post->{Post::LINK_COMMENTS}); + + $actual = Encoder::instance( + [ + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + ] + ) + ->withUrlPrefix('http://example.com') + ->encodeData($this->post); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -859,25 +976,29 @@ public function testEncodeRelationshipsAsLinksDoNotFollowLinksWhenIncludePathSet */ public function testEncodeRelationshipsAsLinks() { - $this->author->{Author::LINK_COMMENTS} = $this->comments; + unset($this->author->{Author::LINK_COMMENTS}); - $actual = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->setIsLinksInIncluded(true); - return $schema; - }, - Comment::class => function ($factory) { - $schema = new CommentSchema($factory); - $schema->linkRemove(Comment::LINK_AUTHOR); - return $schema; - }, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->setIncludePaths([Post::LINK_AUTHOR]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->post); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + return $schema; + }, + Comment::class => function ($factory) { + $schema = new CommentSchema($factory); + $schema->removeRelationship(Comment::LINK_AUTHOR); + return $schema; + }, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->withIncludedPaths([Post::LINK_AUTHOR])->encodeData($this->post); $expected = <<assertEquals($expected, $actual); - } - - /** - * Test override default includes with empty list from encoding parameters. - * - * @see https://github.com/neomerx/json-api/issues/203 - */ - public function testOverrideDefaultIncludesFromEncodingParams() - { - $this->author->{Author::LINK_COMMENTS} = $this->comments; - - // note we set an empty list as include paths - $encodingParams = new EncodingParameters([]); - - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->setIncludePaths([Post::LINK_AUTHOR]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->post, $encodingParams); - - $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } } diff --git a/tests/Encoder/EncodeSimpleObjectsTest.php b/tests/Encoder/EncodeSimpleObjectsTest.php index 4be91f42..4b9acfb3 100644 --- a/tests/Encoder/EncodeSimpleObjectsTest.php +++ b/tests/Encoder/EncodeSimpleObjectsTest.php @@ -1,7 +1,9 @@ -encoderOptions = new EncoderOptions(0, 'http://example.com'); - } - /** * Test encode null. */ public function testEncodeNull(): void { - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - ]); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + ); $actual = $encoder->encodeData(null); @@ -65,10 +58,8 @@ public function testEncodeNull(): void "data" : null } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -76,9 +67,11 @@ public function testEncodeNull(): void */ public function testEncodeEmpty(): void { - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - ]); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + ); $actual = $encoder->encodeData([]); @@ -87,10 +80,7 @@ public function testEncodeEmpty(): void "data" : [] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -98,21 +88,20 @@ public function testEncodeEmpty(): void */ public function testEncodeEmptyIterator(): void { - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - ]); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + ); - $actual = $encoder->encodeData(new \ArrayIterator([])); + $actual = $encoder->encodeData(new ArrayIterator([])); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -122,24 +111,25 @@ public function testEncodeEmptyIterator(): void */ public function testEncodeEmptyWithParameters(): void { - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - ]); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + )->withFieldSets( + [ + // include only these attributes and links + 'authors' => [Author::ATTRIBUTE_FIRST_NAME, Author::LINK_COMMENTS], + ] + ); - $actual = $encoder->encodeData([], new EncodingParameters(null, [ - // include only these attributes and links - 'authors' => [Author::ATTRIBUTE_FIRST_NAME, Author::LINK_COMMENTS], - ])); + $actual = $encoder->encodeData([]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -148,14 +138,16 @@ public function testEncodeEmptyWithParameters(): void public function testEncodeObjectWithAttributesOnly(): void { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], $this->encoderOptions); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData($author); @@ -174,10 +166,43 @@ public function testEncodeObjectWithAttributesOnly(): void } } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); + self::assertJsonStringEqualsJsonString($expected, $actual); + } + + /** + * Test encode identifier. + */ + public function testEncodeIdentifier(): void + { + $encoder = Encoder::instance([]); + + $identity = (new AuthorIdentity('123'))->setMeta('id meta'); + + $actual = $encoder->encodeData($identity); + $expected = <<assertEquals($expected, $actual); + $actual = $encoder->encodeData([$identity]); + $expected = <<{Author::ATTRIBUTE_ID} = null; - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); - $schema->setResourceLinksClosure(function () { - return []; // no `self` link and others - }); - - return $schema; - }, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); + $schema->setResourceLinksClosure( + function () { + return []; // no `self` link and others + } + ); + + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData($author); @@ -212,10 +241,7 @@ public function testEncodeObjectWithAttributesOnlyAndNoId(): void } } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -225,22 +251,26 @@ public function testEncodeObjectWithAttributesOnlyAndNoId(): void */ public function testEncodeObjectWithAttributesAndCustomLinks(): void { - $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); - $schema->setResourceLinksClosure(function ($resource) { - $this->assertNotNull($resource); - - return [ - 'custom' => new Link('http://custom-link.com/', null, true), - ]; - }); - - return $schema; - }, - ], $this->encoderOptions); + $author = Author::instance(9, 'Dan', 'Gebhardt')->setResourceMeta('resource meta'); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); + $schema->setResourceLinksClosure( + function ($resource) { + self::assertNotNull($resource); + + return [ + 'custom' => new Link(false, 'http://custom-link.com/', false), + ]; + } + ); + + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData($author); @@ -255,14 +285,12 @@ public function testEncodeObjectWithAttributesAndCustomLinks(): void }, "links" : { "custom" : "http://custom-link.com/" - } + }, + "meta": "resource meta" } } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -270,10 +298,16 @@ public function testEncodeObjectWithAttributesAndCustomLinks(): void */ public function testEncodeObjectAsResourceIdentity(): void { - $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - ], $this->encoderOptions); + $author = Author::instance(9, 'Dan', 'Gebhardt')->setIdentifierMeta('id meta'); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeIdentifiers($author); @@ -281,14 +315,12 @@ public function testEncodeObjectAsResourceIdentity(): void { "data" : { "type" : "people", - "id" : "9" + "id" : "9", + "meta": "id meta" } } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -297,9 +329,11 @@ public function testEncodeObjectAsResourceIdentity(): void public function testEncodeArrayAsResourceIdentity(): void { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeIdentifiers([$author]); @@ -311,10 +345,132 @@ public function testEncodeArrayAsResourceIdentity(): void }] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); + self::assertJsonStringEqualsJsonString($expected, $actual); + } + + /** + * Test encode simple object as resource identity with included resources. + */ + public function testEncodeObjectAsResourceIdentityWithIncludes(): void + { + $comment = Comment::instance(1, 'One!'); + $author = Author::instance(9, 'Dan', 'Gebhardt', [$comment]); + $comment->{Comment::LINK_AUTHOR} = $author; + + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + ] + ) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths([Comment::LINK_AUTHOR]) + ->encodeIdentifiers($comment); + + $expected = <<encodeIdentifiers($author); + $expected = <<encodeIdentifiers([$author]); + $expected = <<assertEquals($expected, $actual); + $actual = $encoder->encodeIdentifiers(null); + $expected = << function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], $this->encoderOptions); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData([$author]); @@ -349,10 +507,7 @@ public function testEncodeObjectWithAttributesOnlyInArray(): void }] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -361,16 +516,18 @@ public function testEncodeObjectWithAttributesOnlyInArray(): void public function testEncodeObjectWithAttributesOnlyInAssocArray(): void { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], $this->encoderOptions); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); - $actual = $encoder->encodeData(['key_doesnt_matter' => $author]); + $actual = $encoder->encodeData(['key_does_not_matter' => $author]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -399,14 +553,16 @@ public function testEncodeObjectWithAttributesOnlyInAssocArray(): void public function testEncodeObjectWithAttributesOnlyPrettyPrinted(): void { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], new EncoderOptions(JSON_PRETTY_PRINT, 'http://example.com')); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->withEncodeOptions(JSON_PRETTY_PRINT); $actual = $encoder->encodeData($author); @@ -426,7 +582,7 @@ public function testEncodeObjectWithAttributesOnlyPrettyPrinted(): void } EOL; - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -436,14 +592,16 @@ public function testEncodeArrayOfObjectsWithAttributesOnly(): void { $author1 = Author::instance(7, 'First', 'Last'); $author2 = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], $this->encoderOptions); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData([$author1, $author2]); @@ -475,10 +633,7 @@ public function testEncodeArrayOfObjectsWithAttributesOnly(): void ] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -486,9 +641,13 @@ public function testEncodeArrayOfObjectsWithAttributesOnly(): void */ public function testEncodeMetaAndtopLinksForSimpleObject(): void { - $author = Author::instance(9, 'Dan', 'Gebhardt'); - $links = [Link::SELF => new Link('/people/9')]; - $meta = [ + $author = Author::instance(9, 'Dan', 'Gebhardt'); + $links = [Link::SELF => new Link(true, '/people/9', false)]; + $profile = [ + new LinkWithAliases(false, 'http://example.com/profiles/flexible-pagination', [], false), + new LinkWithAliases(false, 'http://example.com/profiles/resource-versioning', ['version' => 'v'], false), + ]; + $meta = [ "copyright" => "Copyright 2015 Example Corp.", "authors" => [ "Yehuda Katz", @@ -497,14 +656,22 @@ public function testEncodeMetaAndtopLinksForSimpleObject(): void ], ]; - $actual = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], $this->encoderOptions)->withLinks($links)->withMeta($meta)->encodeData($author); + return $schema; + }, + ] + ) + ->withUrlPrefix('http://example.com') + ->withLinks([]) + ->withLinks($links) + ->withProfile($profile) + ->withMeta($meta) + ->encodeData($author); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -552,14 +725,16 @@ public function testEncodeMeta(): void ], ]; - $actual = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ])->encodeMeta($meta); + return $schema; + }, + ] + )->encodeMeta($meta); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -584,21 +756,20 @@ public function testEncodeMeta(): void */ public function testEncodeJsonApiVersion(): void { - $actual = Encoder::instance()->withJsonApiVersion(['some' => 'meta'])->encodeData(null); + $actual = Encoder::instance() + ->withJsonApiVersion(Encoder::JSON_API_VERSION)->withJsonApiMeta(['some' => 'meta']) + ->encodeData(null); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -608,10 +779,12 @@ public function testEncodePolymorphicArray(): void { $author = Author::instance(7, 'First', 'Last', []); $site = Site::instance(9, 'Main Site', []); - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Site::class => SiteSchema::class, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData([$author, $site]); @@ -627,6 +800,10 @@ public function testEncodePolymorphicArray(): void }, "relationships" : { "comments" : { + "links": { + "self" : "http://example.com/people/7/relationships/comments", + "related" : "http://example.com/people/7/comments" + }, "data" : [] } }, @@ -641,6 +818,10 @@ public function testEncodePolymorphicArray(): void }, "relationships" : { "posts" : { + "links": { + "self" : "http://example.com/sites/9/relationships/posts", + "related" : "http://example.com/sites/9/posts" + }, "data" : [] } }, @@ -651,10 +832,7 @@ public function testEncodePolymorphicArray(): void ] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -663,14 +841,16 @@ public function testEncodePolymorphicArray(): void public function testEncodeObjectWithAttributesOnlyInArrayAccessCollection(): void { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); - return $schema; - }, - ], $this->encoderOptions); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $collection = new Collection(); $collection[] = $author; @@ -692,10 +872,7 @@ public function testEncodeObjectWithAttributesOnlyInArrayAccessCollection(): voi }] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -706,12 +883,14 @@ public function testEncodeObjectWithAttributesOnlyInArrayAccessCollection(): voi public function testEncodeWithSchmaInstance(): void { $authorSchema = new AuthorSchema(new Factory()); - $authorSchema->linkRemove(Author::LINK_COMMENTS); + $authorSchema->removeRelationship(Author::LINK_COMMENTS); $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => $authorSchema, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => $authorSchema, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData($author); @@ -730,10 +909,7 @@ public function testEncodeWithSchmaInstance(): void } } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -747,31 +923,42 @@ public function testEncodingSimilarRelationships(): void { /** * It's odd to have a second comments relationship and the naming is also weird but... - * we need a second relationship that which naming start identical to the first one. + * let's check a relationship that which naming start identical to the first one. */ $secondRelName = Author::LINK_COMMENTS . '-second-name'; $comment1 = Comment::instance(1, 'One!'); $comment5 = Comment::instance(5, 'Five!'); - $author = Author::instance(9, 'Dan', 'Gebhardt', [$comment1]); - - $actual = Encoder::instance([ - Author::class => function ($factory) use ($secondRelName, $comment5) { - $schema = new AuthorSchema($factory); + $author = Author::instance(9, 'Dan', 'Gebhardt', [$comment1]); + + $actual = Encoder::instance( + [ + Author::class => function ($factory) use ($secondRelName, $comment5) { + $schema = new AuthorSchema($factory); + + // make the author have the comment only in that odd relationship + // we will emulate the new relationship with that comment + $schema->addToRelationship( + $secondRelName, + AuthorSchema::RELATIONSHIP_DATA, + function () use ($comment5 + ) { + return [$comment5]; + } + ); - // make the author have the comment only in that odd relationship - // we will emulate the new relationship with that comment - $schema->linkAddTo($secondRelName, AuthorSchema::DATA, function () use ($comment5) { - return [$comment5]; - }); + // hide links + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + $schema->hideDefaultLinksInRelationship($secondRelName); - return $schema; - }, - Comment::class => CommentSchema::class, - ], $this->encoderOptions)->encodeData($author, new EncodingParameters( + return $schema; + }, + Comment::class => CommentSchema::class, + ] + )->withUrlPrefix('http://example.com')->withIncludedPaths( // include only the new odd relationship and omit the original `comments` relationship [$secondRelName] - )); + )->encodeData($author); // The issue was that comment with id 1 was also added in `included` section $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -836,11 +1026,13 @@ public function testEncodingSimilarRelationships(): void public function testEncodeArrayBasedObject(): void { $author = new AuthorCModel(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - AuthorCModel::class => AuthorCModelSchema::class, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + AuthorCModel::class => AuthorCModelSchema::class, + ] + ); - $actual = $encoder->encodeData($author); + $actual = $encoder->withUrlPrefix('http://example.com')->encodeData($author); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); // same but as array - $actual = $encoder->encodeData([$author]); + $actual = $encoder->withUrlPrefix('http://example.com')->encodeData([$author]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } } diff --git a/tests/Encoder/EncodeSparseAndFieldSetsTest.php b/tests/Encoder/EncodeSparseAndFieldSetsTest.php index 963e5641..f62fbd98 100644 --- a/tests/Encoder/EncodeSparseAndFieldSetsTest.php +++ b/tests/Encoder/EncodeSparseAndFieldSetsTest.php @@ -1,7 +1,9 @@ -author), Comment::instance(12, 'I like XML better', $this->author), ]; - $this->post = Post::instance( + $this->post = Post::instance( 1, 'JSON API paints my bikeshed!', 'Outside every fat man there was an even fatter man trying to close in', $this->author, $this->comments ); - $this->site = Site::instance(2, 'site name', [$this->post]); - $this->encoderOptions = new EncoderOptions(0, 'http://example.com'); + $this->site = Site::instance(2, 'site name', [$this->post]); } /** @@ -89,19 +84,15 @@ public function testEncodeWithRecursiveIncludedObjects() { $this->author->{Author::LINK_COMMENTS} = $this->comments; - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site, new EncodingParameters( - // include only this relations - [ - Site::LINK_POSTS, - Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, - ], - null // no filter for attributes - )); + $actual = Encoder::instance($this->getSchemasWithoutDefaultLinksInRelationships()) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths( + [ + // include only this relations + Site::LINK_POSTS, + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, + ] + )->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -185,21 +173,21 @@ public function testEncodeOnlyFieldSets() { $this->author->{Author::LINK_COMMENTS} = $this->comments; - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site, new EncodingParameters( - null, - // include only these attributes and links - [ - // note: no filter for 'comments' - 'people' => [Author::ATTRIBUTE_LAST_NAME, Author::ATTRIBUTE_FIRST_NAME], - 'posts' => [Post::LINK_COMMENTS, Post::LINK_AUTHOR], - 'sites' => [Site::LINK_POSTS], - ] - )); + $actual = Encoder::instance($this->getSchemasWithoutDefaultLinksInRelationships()) + ->withUrlPrefix('http://example.com')->withFieldSets( + [ + // note: no filter for 'comments' + 'people' => [Author::ATTRIBUTE_LAST_NAME, Author::ATTRIBUTE_FIRST_NAME], + 'posts' => [Post::LINK_COMMENTS, Post::LINK_AUTHOR], + 'sites' => [Site::LINK_POSTS], + ] + )->withIncludedPaths( + [ + Site::LINK_POSTS, + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR, + ] + )->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -288,20 +273,17 @@ public function testEncodeOnlyFieldSets() */ public function testIncludeAndSparseFieldSets() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site, new EncodingParameters( - [ - Site::LINK_POSTS - ], - // include only these attributes and links - [ - 'posts' => [Post::ATTRIBUTE_TITLE, Post::ATTRIBUTE_BODY], - ] - )); + $actual = Encoder::instance($this->getSchemasWithoutDefaultLinksInRelationships()) + ->withUrlPrefix('http://example.com') + ->withIncludedPaths( + [ + Site::LINK_POSTS, + ] + )->withFieldSets( + [ + 'posts' => [Post::ATTRIBUTE_TITLE, Post::ATTRIBUTE_BODY], + ] + )->encodeData($this->site); $expected = <<assertEquals($expected, $actual); - } - - /** - * Test closures are not executed in lazy relationships. - */ - public function testDataNotLoadedInLazyRelationships() - { - $throwExClosure = function () { - throw new \Exception(); - }; - - $actual = Encoder::instance([ - Author::class => function ($factory) use ($throwExClosure) { - $schema = new AuthorSchema($factory); - $schema->linkAddTo(Author::LINK_COMMENTS, AuthorSchema::DATA, $throwExClosure); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->author, new EncodingParameters( - // do not include any relationships - [], - // include only these attributes (thus relationship that throws exception should not be invoked) - [ - 'people' => [Author::ATTRIBUTE_LAST_NAME, Author::ATTRIBUTE_FIRST_NAME], - ] - )); - - $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -390,23 +324,23 @@ public function testDataNotLoadedInLazyRelationships() public function testMetaNotLoadedInLazyRelationships() { $throwExClosure = function () { - throw new \Exception(); + throw new Exception(); }; - $actual = Encoder::instance([ - Author::class => function ($factory) use ($throwExClosure) { - $schema = new AuthorSchema($factory); - $schema->linkAddTo(Author::LINK_COMMENTS, AuthorSchema::META, $throwExClosure); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->author, new EncodingParameters( - // do not include any relationships - [], - // include only these attributes (thus relationship that throws exception should not be invoked) + $actual = Encoder::instance( [ + Author::class => function ($factory) use ($throwExClosure) { + $schema = new AuthorSchema($factory); + $schema->addToRelationship(Author::LINK_COMMENTS, AuthorSchema::RELATIONSHIP_META, $throwExClosure); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->withFieldSets( + [ + // include only these attributes (thus relationship that throws exception should not be invoked) 'people' => [Author::ATTRIBUTE_LAST_NAME, Author::ATTRIBUTE_FIRST_NAME], ] - )); + )->encodeData($this->author); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -439,27 +370,22 @@ public function testIncludeAndSparseFieldSetsInGreedyMode() { $this->author->{Author::LINK_COMMENTS} = $this->comments; - $actual = $this->createLoggedEncoder([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - Site::class => SiteSchema::class, - ], $this->encoderOptions)->encodeData($this->site, new EncodingParameters( - [ - Site::LINK_POSTS, - Site::LINK_POSTS . '.' . Post::LINK_AUTHOR, - Site::LINK_POSTS . '.' . Post::LINK_COMMENTS, - Site::LINK_POSTS . '.' . Post::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR, - Site::LINK_POSTS . '.' . Post::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR . '.' . Author::LINK_COMMENTS, - ], - // include only these attributes and links - [ - 'sites' => [], - 'posts' => [], - 'comments' => [], - 'people' => [Author::ATTRIBUTE_LAST_NAME, Author::LINK_COMMENTS], - ] - )); + $actual = Encoder::instance($this->getSchemasWithoutDefaultLinksInRelationships()) + ->withUrlPrefix('http://example.com')->withIncludedPaths( + [ + Site::LINK_POSTS . '.' . Post::LINK_AUTHOR, + Site::LINK_POSTS . '.' . Post::LINK_COMMENTS . '.' . + Comment::LINK_AUTHOR . '.' . Author::LINK_COMMENTS, + ] + )->withFieldSets( + [ + // include only these attributes and links + 'sites' => [], // note relationship resources will NOT be in included + 'posts' => [Post::LINK_AUTHOR], // note relationship resources will be in included + 'comments' => [], // note relationship resources will NOT be in included + 'people' => [Author::ATTRIBUTE_LAST_NAME, Author::LINK_COMMENTS], + ] + )->encodeData($this->site); $expected = <<assertEquals($expected, $actual); + /** + * @return array + */ + private function getSchemasWithoutDefaultLinksInRelationships(): array + { + $authorSchema = function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + + return $schema; + }; + $commentSchema = function ($factory) { + $schema = new CommentSchema($factory); + $schema->hideDefaultLinksInRelationship(Comment::LINK_AUTHOR); + + return $schema; + }; + $postSchema = function ($factory) { + $schema = new PostSchema($factory); + $schema->hideResourceLinks(); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + + return $schema; + }; + $siteSchema = function ($factory) { + $schema = new SiteSchema($factory); + $schema->hideDefaultLinksInRelationship(Site::LINK_POSTS); + + return $schema; + }; + + return [ + Author::class => $authorSchema, + Comment::class => $commentSchema, + Post::class => $postSchema, + Site::class => $siteSchema, + ]; } } diff --git a/tests/Encoder/EncoderTest.php b/tests/Encoder/EncoderTest.php index 3a5f3d41..6c92a139 100644 --- a/tests/Encoder/EncoderTest.php +++ b/tests/Encoder/EncoderTest.php @@ -1,7 +1,9 @@ -encoderOptions = new EncoderOptions(0, 'http://example.com'); - } - - /** - * @expectedException \InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ public function testEncodeInvalidData() { - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + )->withUrlPrefix('http://example.com'); /** @noinspection PhpParamsInspection */ $encoder->encodeData('input must be an object or array of objects or iterator over objects'); @@ -70,13 +57,15 @@ public function testEncodeInvalidData() public function testEncodeArrayOfDuplicateObjectsWithAttributesOnly() { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); - return $schema; - } - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData([$author, $author]); @@ -108,10 +97,7 @@ public function testEncodeArrayOfDuplicateObjectsWithAttributesOnly() ] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -125,13 +111,11 @@ public function testEncodeDuplicatesWithCircularReferencesInData() // Don't be confused by the link name. It does not matter and I just don't want to create a new schema. $author->{Author::LINK_COMMENTS} = $author; - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->setIncludePaths([]); - return $schema; - }, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + ] + )->withUrlPrefix('http://example.com'); $actual = $encoder->encodeData([$author, $author]); @@ -147,6 +131,10 @@ public function testEncodeDuplicatesWithCircularReferencesInData() }, "relationships" : { "comments" : { + "links": { + "self" : "http://example.com/people/9/relationships/comments", + "related" : "http://example.com/people/9/comments" + }, "data" : { "type" : "people", "id" : "9" } } }, @@ -162,6 +150,10 @@ public function testEncodeDuplicatesWithCircularReferencesInData() }, "relationships" : { "comments" : { + "links": { + "self" : "http://example.com/people/9/relationships/comments", + "related" : "http://example.com/people/9/comments" + }, "data" : { "type" : "people", "id" : "9" } } }, @@ -172,10 +164,7 @@ public function testEncodeDuplicatesWithCircularReferencesInData() ] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -188,17 +177,19 @@ public function testEncodeDuplicatesWithRelationFieldSetFilter() Comment::instance(5, 'First!', $author), Comment::instance(12, 'I like XML better', $author), ]; - $encoder = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + ] + )->withUrlPrefix('http://example.com')->withFieldSets( + // filter attributes + ['people' => [Author::ATTRIBUTE_LAST_NAME, Author::ATTRIBUTE_FIRST_NAME]] + ); $author->{Author::LINK_COMMENTS} = $comments; - $actual = $encoder->encodeData([$author, $author], new EncodingParameters( - null, - ['people' => [Author::ATTRIBUTE_LAST_NAME, Author::ATTRIBUTE_FIRST_NAME]] // filter attributes - )); + $actual = $encoder->encodeData([$author, $author]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -239,11 +227,13 @@ public function testEncodeDuplicatesWithRelationFieldSetFilter() */ public function testEncodeSimpleLinks() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => PostSchema::class, + ] + )->withUrlPrefix('http://example.com')->encodeData($this->getStandardPost()); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -282,16 +277,23 @@ public function testEncodeSimpleLinks() */ public function testEncodeEmptyLinks() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::DATA, null); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::DATA, []); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->addToRelationship(Post::LINK_AUTHOR, PostSchema::RELATIONSHIP_DATA, null); + $schema->addToRelationship(Post::LINK_COMMENTS, PostSchema::RELATIONSHIP_DATA, []); + + // hide links + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->encodeData($this->getStandardPost()); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -323,18 +322,13 @@ public function testEncodeEmptyLinks() */ public function testEncodeLinksInDocumentAndRelationships() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_SELF, true); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_RELATED, true); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::SHOW_SELF, true); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::SHOW_RELATED, true); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => PostSchema::class, + ] + )->withUrlPrefix('http://example.com')->encodeData($this->getStandardPost()); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -386,26 +377,30 @@ public function testEncodeLinkWithMeta() Comment::instance(12, 'I like XML better'), ]; $author = Author::instance(9, 'Dan', 'Gebhardt', $comments); - $actual = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkAddTo(Author::LINK_COMMENTS, AuthorSchema::SHOW_SELF, true); - $schema->linkAddTo( - Author::LINK_COMMENTS, - AuthorSchema::LINKS, - [ - LinkInterface::SELF => function (AuthorSchema $schema, Author $author) { - return new Link( - $schema->getSelfSubUrl($author) . '/relationships/comments', - ['some' => 'meta'] - ); - } - ] - ); - return $schema; - }, - Comment::class => CommentSchema::class, - ], $this->encoderOptions)->encodeData($author); + $actual = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideRelatedLinkInRelationship(Author::LINK_COMMENTS); + $schema->addToRelationship( + Author::LINK_COMMENTS, + AuthorSchema::RELATIONSHIP_LINKS, + [ + LinkInterface::SELF => function (AuthorSchema $schema, Author $author) { + return new Link( + true, + $schema->getSelfSubUrl($author) . '/relationships/comments', + true, + ['some' => 'meta'] + ); + }, + ] + ); + return $schema; + }, + Comment::class => CommentSchema::class, + ] + )->withUrlPrefix('http://example.com')->encodeData($author); $expected = <<assertEquals($expected, $actual); - } - - /** - * Test relationships meta. - */ - public function testRelationshipsMeta() - { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->setRelationshipsMeta(['some' => 'meta']); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); - - $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -497,25 +439,38 @@ public function testRelationshipsMeta() */ public function testAddLinksToEmptyRelationship() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::DATA, null); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_DATA, false); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_RELATED, true); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::LINKS, ['foo' => new Link('/your/link', null, true)]); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::DATA, []); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::SHOW_DATA, false); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::LINKS, [ - 'boo' => function (PostSchema $schema, Post $post) { - return new Link($schema->getSelfSubUrl($post) . '/another/link'); - } - ]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->removeFromRelationship(Post::LINK_AUTHOR, PostSchema::RELATIONSHIP_DATA); + $selfLink = new Link(false, 'http://foo.boo/custom-self', false); + $relatedLink = new Link(false, 'http://foo.boo/custom-related', false); + $schema->setSelfLinkInRelationship(Post::LINK_AUTHOR, $selfLink); + $schema->setRelatedLinkInRelationship(Post::LINK_AUTHOR, $relatedLink); + $schema->addToRelationship( + Post::LINK_AUTHOR, + PostSchema::RELATIONSHIP_LINKS, + ['foo' => new Link(false, '/your/link', false)] + ); + $schema->removeFromRelationship(Post::LINK_COMMENTS, PostSchema::RELATIONSHIP_DATA); + $schema->addToRelationship( + Post::LINK_COMMENTS, + PostSchema::RELATIONSHIP_LINKS, + [ + 'boo' => function (PostSchema $schema, Post $post) { + return new Link(true, $schema->getSelfSubUrl($post) . '/another/link', false); + }, + ] + ); + $schema->hideRelatedLinkInRelationship(Post::LINK_COMMENTS); + + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->encodeData($this->getStandardPost()); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -551,22 +510,28 @@ public function testAddLinksToEmptyRelationship() */ public function testAddMetaToEmptyRelationship() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::DATA, null); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_DATA, false); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::META, ['author' => 'meta']); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::DATA, []); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::SHOW_DATA, false); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::META, function () { - return ['comments' => 'meta']; - }); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->removeFromRelationship(Post::LINK_AUTHOR, PostSchema::RELATIONSHIP_DATA); + $schema->hideDefaultLinksInRelationship(Post::LINK_AUTHOR); + $schema->addToRelationship(Post::LINK_AUTHOR, PostSchema::RELATIONSHIP_META, ['author' => 'meta']); + $schema->removeFromRelationship(Post::LINK_COMMENTS, PostSchema::RELATIONSHIP_DATA); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + $schema->addToRelationship( + Post::LINK_COMMENTS, + PostSchema::RELATIONSHIP_META, + function () { + return ['comments' => 'meta']; + } + ); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->encodeData($this->getStandardPost()); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -602,81 +564,35 @@ public function testAddMetaToEmptyRelationship() */ public function testHideDataSectionIfOmittedInSchema() { - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkRemoveFrom(Post::LINK_AUTHOR, PostSchema::DATA); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_RELATED, true); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::LINKS, ['foo' => new Link('/your/link', null, true)]); - $schema->linkRemoveFrom(Post::LINK_COMMENTS, PostSchema::DATA); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::LINKS, [ - 'boo' => function (PostSchema $schema, Post $post) { - return new Link($schema->getSelfSubUrl($post) . '/another/link'); - } - ]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->removeFromRelationship(Post::LINK_AUTHOR, PostSchema::RELATIONSHIP_DATA); + $schema->hideSelfLinkInRelationship(Post::LINK_AUTHOR); + $schema->addToRelationship( + Post::LINK_AUTHOR, + PostSchema::RELATIONSHIP_LINKS, + ['foo' => new Link(false, '/your/link', false)] + ); + $schema->removeFromRelationship(Post::LINK_COMMENTS, PostSchema::RELATIONSHIP_DATA); + $schema->addToRelationship( + Post::LINK_COMMENTS, + PostSchema::RELATIONSHIP_LINKS, + [ + 'boo' => function (PostSchema $schema, Post $post) { + return new Link(true, $schema->getSelfSubUrl($post) . '/another/link', false); + }, + ] + ); + $schema->hideRelatedLinkInRelationship(Post::LINK_COMMENTS); - $expected = <<assertEquals($expected, $actual); - } - - /** - * Test closures are not executed in hidden relationships. - */ - public function testDataNotLoadedInHiddenRelationships() - { - $throwExClosure = function () { - throw new \Exception(); - }; - - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => function ($factory) use ($throwExClosure) { - $schema = new PostSchema($factory); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::DATA, $throwExClosure); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_DATA, false); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::SHOW_RELATED, true); - $schema->linkAddTo(Post::LINK_AUTHOR, PostSchema::LINKS, ['foo' => new Link('/your/link', null, true)]); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::DATA, $throwExClosure); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::SHOW_DATA, false); - $schema->linkAddTo(Post::LINK_COMMENTS, PostSchema::LINKS, [ - 'boo' => function (PostSchema $schema, Post $post) { - return new Link($schema->getSelfSubUrl($post) . '/another/link'); - } - ]); - return $schema; - }, - ], $this->encoderOptions)->encodeData($this->getStandardPost()); + ] + )->withUrlPrefix('http://example.com')->encodeData($this->getStandardPost()); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -713,20 +629,24 @@ public function testDataNotLoadedInHiddenRelationships() public function testEncodeTraversableObjectsWithAttributesOnly() { $author = Author::instance(9, 'Dan', 'Gebhardt'); - $encoder = Encoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - //$schema->linkRemove(Author::LINK_COMMENTS); - return $schema; - }, - Comment::class => CommentSchema::class, - ], $this->encoderOptions); + $encoder = Encoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->hideDefaultLinksInRelationship(Author::LINK_COMMENTS); + return $schema; + }, + Comment::class => CommentSchema::class, + ] + )->withUrlPrefix('http://example.com'); // iterator here - $author->{Author::LINK_COMMENTS} = new ArrayIterator([ - 'comment1' => Comment::instance(5, 'First!'), - 'comment2' => Comment::instance(12, 'I like XML better'), - ]); + $author->{Author::LINK_COMMENTS} = new ArrayIterator( + [ + 'comment1' => Comment::instance(5, 'First!'), + 'comment2' => Comment::instance(12, 'I like XML better'), + ] + ); // and iterator here $itemSet = new ArrayIterator(['what_if_its_not_zero_based_array' => $author]); @@ -757,10 +677,7 @@ public function testEncodeTraversableObjectsWithAttributesOnly() ] } EOL; - // remove formatting from 'expected' - $expected = json_encode(json_decode($expected)); - - $this->assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -770,14 +687,17 @@ public function testEncodeRelationshipWithSingleItem() { $post = Post::instance(1, 'Title', 'Body', null, [Comment::instance(5, 'First!')]); - $actual = Encoder::instance([ - Comment::class => CommentSchema::class, - Post::class => function ($factory) { - $schema = new PostSchema($factory); - $schema->linkRemove(Post::LINK_AUTHOR); - return $schema; - }, - ], $this->encoderOptions)->encodeData($post); + $actual = Encoder::instance( + [ + Comment::class => CommentSchema::class, + Post::class => function ($factory) { + $schema = new PostSchema($factory); + $schema->removeRelationship(Post::LINK_AUTHOR); + $schema->hideDefaultLinksInRelationship(Post::LINK_COMMENTS); + return $schema; + }, + ] + )->withUrlPrefix('http://example.com')->encodeData($post); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -813,9 +730,11 @@ public function testEncodeRelationshipWithSingleItem() public function testEncodeWithRelationshipSelfLink() { $post = $this->getStandardPost(); - $actual = Encoder::instance([ - Post::class => PostSchema::class, - ])->withRelationshipSelfLink($post, Post::LINK_AUTHOR)->encodeData([]); + $actual = Encoder::instance( + [ + Post::class => PostSchema::class, + ] + )->withRelationshipSelfLink($post, Post::LINK_AUTHOR)->encodeData([]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -837,9 +753,11 @@ public function testEncodeWithRelationshipSelfLink() public function testEncodeWithRelationshipRelatedLink() { $post = $this->getStandardPost(); - $actual = Encoder::instance([ - Post::class => PostSchema::class, - ])->withRelationshipRelatedLink($post, Post::LINK_AUTHOR)->encodeData([]); + $actual = Encoder::instance( + [ + Post::class => PostSchema::class, + ] + )->withRelationshipRelatedLink($post, Post::LINK_AUTHOR)->encodeData([]); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -865,16 +780,17 @@ public function testEncodeUnrecognizedResourceAtRoot() /** @var InvalidArgumentException $catch */ $catch = null; try { - Encoder::instance([ - Post::class => PostSchema::class, - ], $this->encoderOptions)->encodeData($author); + Encoder::instance( + [ + Post::class => PostSchema::class, + ] + )->withUrlPrefix('http://example.com')->encodeData($author); } catch (InvalidArgumentException $exception) { $catch = $exception; } - $this->assertNotNull($catch); - $this->assertContains('top-level', $catch->getMessage()); - $this->assertNotNull($catch->getPrevious()); + self::assertNotNull($catch); + self::assertContains('top-level', $catch->getMessage()); } /** @@ -888,19 +804,22 @@ public function testEncodeUnrecognizedResourceInRelationship() /** @var InvalidArgumentException $catch */ $catch = null; try { - Encoder::instance([ - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - ], $this->encoderOptions)->encodeData($post, new EncodingParameters([ - Post::LINK_COMMENTS - ])); + Encoder::instance( + [ + Comment::class => CommentSchema::class, + Post::class => PostSchema::class, + ] + )->withUrlPrefix('http://example.com')->withIncludedPaths( + [ + Post::LINK_COMMENTS, + ] + )->encodeData($post); } catch (InvalidArgumentException $exception) { $catch = $exception; } - $this->assertNotNull($catch); - $this->assertContains(Post::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR, $catch->getMessage()); - $this->assertNotNull($catch->getPrevious()); + self::assertNotNull($catch); + self::assertContains(Post::LINK_COMMENTS . '.' . Comment::LINK_AUTHOR, $catch->getMessage()); } /** diff --git a/tests/Encoder/Parameters/ParametersAnalyzerTest.php b/tests/Encoder/Parameters/ParametersAnalyzerTest.php deleted file mode 100644 index dd96a2ab..00000000 --- a/tests/Encoder/Parameters/ParametersAnalyzerTest.php +++ /dev/null @@ -1,136 +0,0 @@ -parameters = new EncodingParameters( - $paths = ['some.path'], - $fieldsets = ['type' => ['field1', 'field2']] - ); - - $this->container = Mockery::mock(ContainerInterface::class); - } - - /** - * Test isPathIncluded when include path are specified. - */ - public function testIsPathIncludedForSpecifiedIncludePaths() - { - /** @var ContainerInterface $container */ - $container = $this->container; - $analyzer = (new Factory())->createParametersAnalyzer($this->parameters, $container); - - $type = 'type'; - - $this->assertTrue($analyzer->isPathIncluded('some.path', $type)); - $this->assertTrue($analyzer->isPathIncluded('some', $type)); - $this->assertFalse($analyzer->isPathIncluded('some.path.plus', $type)); - $this->assertFalse($analyzer->isPathIncluded('completely.different', $type)); - } - - /** - * Test isPathIncluded when include path are not specified. - */ - public function testIsPathIncludedForNotSpecifiedIncludePaths() - { - /** @var ContainerInterface $container */ - $container = $this->container; - $analyzer = (new Factory())->createParametersAnalyzer(new EncodingParameters(), $container); - - $type = 'type'; - - $schema1 = Mockery::mock(SchemaInterface::class); - $schema2 = Mockery::mock(SchemaInterface::class); - - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->container->shouldReceive('getSchemaByResourceType')->zeroOrMoreTimes()->with($type)->andReturn($schema1); - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $schema1->shouldReceive('getIncludePaths')->zeroOrMoreTimes()->withNoArgs()->andReturn(['some.path']); - - $this->assertTrue($analyzer->isPathIncluded('some.path', $type)); - $this->assertTrue($analyzer->isPathIncluded('some', $type)); - $this->assertFalse($analyzer->isPathIncluded('some.path.plus', $type)); - $this->assertFalse($analyzer->isPathIncluded('completely.different', $type)); - - $type = 'some-other-type'; - - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->container->shouldReceive('getSchemaByResourceType')->zeroOrMoreTimes()->with($type)->andReturn($schema2); - - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $schema2->shouldReceive('getIncludePaths')->zeroOrMoreTimes()->withNoArgs()->andReturn(['other.path']); - - $this->assertFalse($analyzer->isPathIncluded('some.path', $type)); - } - - /** - * Test get relationships to be included for input path. - */ - public function testGetIncludeRelationships() - { - $type = 'some-type'; - - /** @var ContainerInterface $container */ - $container = $this->container; - $analyzer = (new Factory())->createParametersAnalyzer(new EncodingParameters([ - 'some', - 'some.path', - 'some.path.deeper', - 'some.another.path', - 'some.another.even.deeper', - ]), $container); - - $getRels = function ($path) use ($analyzer, $type) { - return $analyzer->getIncludeRelationships($path, $type); - }; - - $this->assertEquals(['some' => 'some'], $getRels(null)); - $this->assertEquals(['some' => 'some'], $getRels('')); - $this->assertEquals(['path' => 'path', 'another' => 'another'], $getRels('some')); - $this->assertEquals(['deeper' => 'deeper'], $getRels('some.path')); - $this->assertEquals(['path' => 'path', 'even' => 'even'], $getRels('some.another')); - } -} diff --git a/tests/Encoder/Parser/ParserTest.php b/tests/Encoder/Parser/ParserTest.php deleted file mode 100644 index 846a1b29..00000000 --- a/tests/Encoder/Parser/ParserTest.php +++ /dev/null @@ -1,256 +0,0 @@ -parserManager = Mockery::mock(ParserManagerInterface::class); - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->parserManager->shouldReceive('getFieldSet')->zeroOrMoreTimes()->withAnyArgs()->andReturnNull(); - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->parserManager->shouldReceive('isShouldParseRelationships') - ->zeroOrMoreTimes()->withAnyArgs()->andReturn(true); - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->parserManager->shouldReceive('isRelationshipInFieldSet') - ->zeroOrMoreTimes()->withAnyArgs()->andReturn(true); - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->parserManager->shouldReceive('getIncludeRelationships') - ->zeroOrMoreTimes()->withAnyArgs()->andReturn([]); - - /** @var ParserManagerInterface $parserManager */ - $parserManager = $this->parserManager; - - $schemas = [ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - Post::class => PostSchema::class, - ]; - $factory = new Factory(); - $container = $factory->createContainer($schemas); - $this->parser = $factory->createParser($container, $parserManager); - - $this->author = Author::instance(9, 'Dan', 'Gebhardt'); - $this->comments = [ - Comment::instance(5, 'First!', $this->author), - Comment::instance(12, 'I like XML better', $this->author), - ]; - $this->author->{Author::LINK_COMMENTS} = $this->comments; - - $this->post = Post::instance( - 1, - 'JSON API paints my bikeshed!', - 'Outside every fat man there was an even fatter man trying to close in', - $this->author, - $this->comments - ); - - $this->authorAttr = [ - Author::ATTRIBUTE_FIRST_NAME => $this->author->{Author::ATTRIBUTE_FIRST_NAME}, - Author::ATTRIBUTE_LAST_NAME => $this->author->{Author::ATTRIBUTE_LAST_NAME}, - ]; - - $this->authorAttrNull = [ - Author::ATTRIBUTE_FIRST_NAME => null, - Author::ATTRIBUTE_LAST_NAME => null, - ]; - - $this->commAttr5 = [ - Comment::ATTRIBUTE_BODY => $this->comments[0]->{Comment::ATTRIBUTE_BODY}, - ]; - - $this->commAttr12 = [ - Comment::ATTRIBUTE_BODY => $this->comments[1]->{Comment::ATTRIBUTE_BODY}, - ]; - - $this->postAttr = [ - Post::ATTRIBUTE_TITLE => $this->post->{Post::ATTRIBUTE_TITLE}, - Post::ATTRIBUTE_BODY => $this->post->{Post::ATTRIBUTE_BODY}, - ]; - } - - /** - * Test parse simple object with a reference to null. - */ - public function testParseSimpleObjectWithNullLink() - { - $this->author->{Author::LINK_COMMENTS} = null; - - $start = ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED; - $startNull = ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED; - $complete = ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED; - $expected = [ - // level link name type id attributes meta - [$start, 1, '', 'people', 9, $this->authorAttr, null], - [$startNull, 2, 'comments', null, null, null, null], - [$complete, 1, '', 'people', 9, $this->authorAttr, null], - ]; - - $allReplies = []; - foreach ($this->parser->parse($this->author) as $reply) { - /** @var ParserReplyInterface $reply */ - $allReplies[] = $this->replyToArray($reply); - } - $this->assertEquals($expected, $allReplies); - } - - /** - * Test parse simple object with a reference to empty array. - */ - public function testParseSimpleObjectWithEmptyArrayLink() - { - $this->author->{Author::LINK_COMMENTS} = []; - - $start = ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED; - $startEmpty = ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED; - $complete = ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED; - $expected = [ - // level link name type id attributes meta - [$start, 1, '', 'people', 9, $this->authorAttr, null], - [$startEmpty, 2, 'comments', null, null, null, null], - [$complete, 1, '', 'people', 9, $this->authorAttr, null], - ]; - - $allReplies = []; - foreach ($this->parser->parse($this->author) as $reply) { - /** @var ParserReplyInterface $reply */ - $allReplies[] = $this->replyToArray($reply); - } - $this->assertEquals($expected, $allReplies); - } - - /** - * Test parse object with circular references. - */ - public function testParseObjectWithCircularReferences() - { - $start = ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED; - $complete = ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED; - $expected = [ - // level link name type id attributes meta - [$start, 1, '', 'people', 9, $this->authorAttr, null], - [$start, 2, 'comments', 'comments', 5, $this->commAttr5, null], - [$start, 3, 'author', 'people', 9, $this->authorAttrNull, null], - [$complete, 2, 'comments', 'comments', 5, $this->commAttr5, null], - [$start, 2, 'comments', 'comments', 12, $this->commAttr12, null], - [$start, 3, 'author', 'people', 9, $this->authorAttrNull, null], - [$complete, 2, 'comments', 'comments', 12, $this->commAttr12, null], - [$complete, 1, '', 'people', 9, $this->authorAttr, null], - ]; - - $allReplies = []; - foreach ($this->parser->parse($this->author) as $reply) { - /** @var ParserReplyInterface $reply */ - $allReplies[] = $this->replyToArray($reply); - } - $this->assertEquals($expected, $allReplies); - } - - /** - * @param ParserReplyInterface $reply - * - * @return array - */ - private function replyToArray(ParserReplyInterface $reply) - { - $current = $reply->getStack()->end(); - $link = $current->getRelationship(); - $resource = $current->getResource(); - return [ - $reply->getReplyType(), - $current->getLevel(), - $link === null ? null : $link->getName(), - $resource === null ? null : $resource->getType(), - $resource === null ? null : $resource->getId(), - $resource === null ? null : $resource->getAttributes(), - $resource === null ? null : $resource->getPrimaryMeta(), - ]; - } -} diff --git a/tests/Encoder/SchemaTest.php b/tests/Encoder/SchemaTest.php deleted file mode 100644 index 1232ca3b..00000000 --- a/tests/Encoder/SchemaTest.php +++ /dev/null @@ -1,48 +0,0 @@ -schema = new DummySchema($schemaFactory); - } - - public function testGetLinks() - { - $this->assertEmpty($this->schema->getRelationships(null, true, [])); - } -} diff --git a/tests/Encoder/Stack/StackTest.php b/tests/Encoder/Stack/StackTest.php deleted file mode 100644 index d86cf7bc..00000000 --- a/tests/Encoder/Stack/StackTest.php +++ /dev/null @@ -1,133 +0,0 @@ -stack = (new Factory())->createStack(); - $this->mockLinkObject = Mockery::mock(RelationshipObjectInterface::class); - $this->mockResourceObject = Mockery::mock(ResourceObjectInterface::class); - - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->mockLinkObject->shouldReceive('getName')->zeroOrMoreTimes()->withAnyArgs()->andReturn('someName'); - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->mockLinkObject->shouldReceive('isShouldBeIncluded')->zeroOrMoreTimes()->withAnyArgs()->andReturn(true); - } - - /** - * Test empty stack. - */ - public function testEmptyStack() - { - $this->assertEquals(0, $this->stack->count()); - $this->assertNull($this->stack->end()); - $this->stack->pop(); - $this->assertNull($this->stack->end()); - $this->assertNull($this->stack->penult()); - - $checkEmpty = true; - foreach ($this->stack as $frame) { - $frame ?: null; - $checkEmpty = false; - } - $this->assertTrue($checkEmpty); - } - - /** - * Test push. - */ - public function testPush() - { - $this->stack->push(); - $this->assertEquals(1, $this->stack->count()); - $this->assertEquals(1, $this->stack->end()->getLevel()); - $this->assertNull($this->stack->end()->getResource()); - $this->assertNull($this->stack->end()->getRelationship()); - - $this->stack->setCurrentResource($this->mockResourceObject); - $this->assertSame($this->mockResourceObject, $this->stack->end()->getResource()); - $this->assertNull($this->stack->penult()); - - $frame2 = $this->stack->push(); - $this->stack->setCurrentRelationship($this->mockLinkObject); - $this->assertSame($this->mockLinkObject, $this->stack->end()->getRelationship()); - $this->assertEquals(2, $this->stack->count()); - $this->assertEquals(2, $this->stack->end()->getLevel()); - $this->assertSame($frame2, $this->stack->end()); - $this->assertNull($this->stack->end()->getResource()); - $this->assertSame($this->mockLinkObject, $this->stack->end()->getRelationship()); - $this->stack->pop(); - $this->assertNull($this->stack->penult()); - } - - /** - * Test pop. - */ - public function testPop() - { - $this->assertNotNull($frame1 = $this->stack->push()); - $this->assertSame($frame1, $this->stack->end()); - $this->stack->pop(); - $this->assertNull($this->stack->end()); - $this->assertEquals(0, $this->stack->count()); - - $this->assertNotNull($frame1 = $this->stack->push()); - $this->assertNotNull($frame2 = $this->stack->push()); - $this->assertSame($frame2, $this->stack->end()); - $this->assertSame($frame1, $this->stack->penult()); - - $this->stack->pop(); - $this->assertSame($frame1, $this->stack->end()); - $this->assertNull($this->stack->penult()); - - $this->stack->pop(); - $this->assertEquals(0, $this->stack->count()); - $this->assertNull($this->stack->end()); - } -} diff --git a/tests/Exceptions/JsonApiExceptionTest.php b/tests/Exceptions/JsonApiExceptionTest.php index 919d2c66..cf922493 100644 --- a/tests/Exceptions/JsonApiExceptionTest.php +++ b/tests/Exceptions/JsonApiExceptionTest.php @@ -1,7 +1,9 @@ -collection = new ErrorCollection(); - $this->error = new Error('some-id', null, 404, 'some-code', 'some title', 'some details'); + $this->error = new Error('some-id', null, null, '404', 'some-code', 'some title', 'some details'); } /** diff --git a/tests/Extensions/Issue154/CustomContainerInterface.php b/tests/Extensions/Issue154/CustomContainerInterface.php index 24be46b8..a3a472b3 100644 --- a/tests/Extensions/Issue154/CustomContainerInterface.php +++ b/tests/Extensions/Issue154/CustomContainerInterface.php @@ -1,7 +1,9 @@ -getContainer(); + $container = $this->getSchemaContainer(); $container->register($type, $schema); return $this; diff --git a/tests/Extensions/Issue154/CustomEncoderInterface.php b/tests/Extensions/Issue154/CustomEncoderInterface.php index 75155359..274f8dce 100644 --- a/tests/Extensions/Issue154/CustomEncoderInterface.php +++ b/tests/Extensions/Issue154/CustomEncoderInterface.php @@ -1,7 +1,9 @@ -assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } } diff --git a/tests/Extensions/Issue169/CustomEncoder.php b/tests/Extensions/Issue169/CustomEncoder.php index 7636db05..752eeb3a 100644 --- a/tests/Extensions/Issue169/CustomEncoder.php +++ b/tests/Extensions/Issue169/CustomEncoder.php @@ -1,11 +1,9 @@ -encodeDataToArray($data); + } + + /** + * @param object|array|Iterator|null $data + * + * @return array + */ + public function serializeIdentifiers($data): array + { + return $this->encodeIdentifiersToArray($data); + } + + /** + * @param ErrorInterface $error + * + * @return array + */ + public function serializeError(ErrorInterface $error): array + { + return $this->encodeErrorToArray($error); + } + + /** + * @param ErrorInterface[]|ErrorCollection $errors + * + * @return array + */ + public function serializeErrors($errors): array + { + return $this->encodeErrorsToArray($errors); + } + + /** + * @param array|object $meta + * + * @return array + */ + public function serializeMeta($meta): array + { + return $this->encodeMetaToArray($meta); + } } diff --git a/tests/Extensions/Issue169/CustomFactory.php b/tests/Extensions/Issue169/CustomFactory.php index 5b642c5e..e9db7a7d 100644 --- a/tests/Extensions/Issue169/CustomFactory.php +++ b/tests/Extensions/Issue169/CustomFactory.php @@ -1,7 +1,9 @@ - function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkRemove(Author::LINK_COMMENTS); - return $schema; - } - ]); + $encoder = CustomEncoder::instance( + [ + Author::class => function ($factory) { + $schema = new AuthorSchema($factory); + $schema->removeRelationship(Author::LINK_COMMENTS); + return $schema; + }, + ] + ); $actual = $encoder->serializeData($author); @@ -51,12 +55,12 @@ public function testDataSerialization() 'id' => '9', 'attributes' => [ 'first_name' => 'Dan', - 'last_name' => 'Gebhardt' + 'last_name' => 'Gebhardt', ], 'links' => [ - 'self' => '/people/9' - ] - ] + 'self' => '/people/9', + ], + ], ]; $this->assertEquals($expected, $actual); @@ -69,11 +73,13 @@ public function testDataSerialization() */ public function testIdentifiersSerialization() { - $author = Author::instance(9, 'Dan', 'Gebhardt'); + $author = Author::instance(9, 'Dan', 'Gebhardt'); /** @var CustomEncoder $encoder */ - $encoder = CustomEncoder::instance([ - Author::class => AuthorSchema::class - ]); + $encoder = CustomEncoder::instance( + [ + Author::class => AuthorSchema::class, + ] + ); $actual = $encoder->serializeIdentifiers($author); @@ -81,7 +87,7 @@ public function testIdentifiersSerialization() 'data' => [ 'type' => 'people', 'id' => '9', - ] + ], ]; $this->assertEquals($expected, $actual); @@ -94,7 +100,7 @@ public function testIdentifiersSerialization() */ public function testErrorSerialization() { - $error = new Error('some-id'); + $error = new Error('some-id'); /** @var CustomEncoder $encoder */ $encoder = CustomEncoder::instance(); @@ -109,7 +115,7 @@ public function testErrorSerialization() */ public function testMetaSerialization() { - $meta = ['some meta']; + $meta = ['some meta']; /** @var CustomEncoder $encoder */ $encoder = CustomEncoder::instance(); diff --git a/tests/Extensions/Issue47/CustomEncoder.php b/tests/Extensions/Issue47/CustomEncoder.php index cd0267fe..aacdce6b 100644 --- a/tests/Extensions/Issue47/CustomEncoder.php +++ b/tests/Extensions/Issue47/CustomEncoder.php @@ -1,7 +1,9 @@ -hasFilter($type) === true) { + $allowedFields = $this->getAllowedFields($type); + $fields = $this->iterableToArray($fields); + + $this->deepArrayFilter($fields, $allowedFields, ''); + } + + yield from $fields; + } + + /** + * @param iterable $iterable + * + * @return array + */ + private function iterableToArray(iterable $iterable): array + { + if (is_array($iterable) === true) { + return $iterable; + } else { + assert($iterable instanceof Traversable); + return iterator_to_array($iterable); + } + } + + /** + * @param array $array + * @param array $filters + * @param string $parentPath + * + * @return void + */ + private function deepArrayFilter(array &$array, array $filters, string $parentPath): void + { + foreach ($array as $key => &$value) { + $filterKey = empty($parentPath) === true ? $key : $parentPath . '.' . $key; + if (is_array($value) === false) { + if (array_key_exists($filterKey, $filters) === false) { + unset($array[$key]); + } + } else { + $this->deepArrayFilter($value, $filters, $key); + } + } + } +} diff --git a/tests/Extensions/Issue47/CustomResourceObject.php b/tests/Extensions/Issue47/CustomResourceObject.php deleted file mode 100644 index 65040429..00000000 --- a/tests/Extensions/Issue47/CustomResourceObject.php +++ /dev/null @@ -1,60 +0,0 @@ -fieldKeysFilter; - $attributes = $this->schema->getAttributes($this->resource); - - // in real app here should come filtering however for testing it's ok to make sure we have - // - attributes - // - filter params - // - if we return correct result it will be delivered to to invoking test - - // check we have received attribute filter - $filterEquals = ($filter === ['private.email' => 0]); - if ($filterEquals === false) { - throw new InvalidArgumentException(); - } - - // check we have received attributes - $attributesEqual = ($attributes === [ - 'username' => 'vivalacrowe', - 'private' => [ - 'email' => 'hello@vivalacrowe.com', - 'name' => 'Rob', - ], - ]); - if ($attributesEqual === false) { - throw new InvalidArgumentException(); - } - - return ['private' => ['email' => 'hello@vivalacrowe.com']]; - } -} diff --git a/tests/Extensions/Issue47/IssueTest.php b/tests/Extensions/Issue47/IssueTest.php index fcbf8bde..1cecc9c8 100644 --- a/tests/Extensions/Issue47/IssueTest.php +++ b/tests/Extensions/Issue47/IssueTest.php @@ -1,7 +1,9 @@ - 'hello@vivalacrowe.com', 'name' => 'Rob']); - $actual = CustomEncoder::instance([ - User::class => UserBaseSchema::class, - ])->encodeData($user, new EncodingParameters( - null, + $actual = CustomEncoder::instance( [ - 'users' => ['private.email'], + User::class => UserBaseSchema::class, ] - )); + ) + ->withFieldSets( + [ + 'users' => ['private.email'], + ] + ) + ->encodeData($user); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } } diff --git a/tests/Extensions/Issue47/User.php b/tests/Extensions/Issue47/User.php index 72cd2128..8067d3a1 100644 --- a/tests/Extensions/Issue47/User.php +++ b/tests/Extensions/Issue47/User.php @@ -1,7 +1,9 @@ -identity = $identity; $this->name = $name; diff --git a/tests/Extensions/Issue47/UserBaseSchema.php b/tests/Extensions/Issue47/UserBaseSchema.php index 97cd2c43..e0ccd61d 100644 --- a/tests/Extensions/Issue47/UserBaseSchema.php +++ b/tests/Extensions/Issue47/UserBaseSchema.php @@ -1,7 +1,9 @@ -identity; } /** * @inheritdoc */ - public function getAttributes($user, array $fieldKeysFilter = null): ?array + public function getAttributes($user): iterable { - /** @var User $user */ + assert($user instanceof User); + return [ 'username' => $user->name, 'private' => $user->contactDetails, ]; } + + /** + * @inheritdoc + */ + public function getRelationships($resource): iterable + { + return []; + } } diff --git a/tests/Extensions/Issue49/CustomFactory.php b/tests/Extensions/Issue49/CustomFactory.php deleted file mode 100644 index 419b5429..00000000 --- a/tests/Extensions/Issue49/CustomFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -stack->end(); - $relationship = $curFrame->getRelationship(); - $updatedRelationship = $this->schemaFactory->createRelationshipObject( - $relationship->getName(), - $relationship->getData(), - $relationship->getLinks(), - $relationship->getMeta(), - true, - $relationship->isRoot() - ); - $this->stack->setCurrentRelationship($updatedRelationship); - - return parent::analyzeCurrentData(); - } -} diff --git a/tests/Extensions/Issue49/IssueTest.php b/tests/Extensions/Issue49/IssueTest.php deleted file mode 100644 index f166461c..00000000 --- a/tests/Extensions/Issue49/IssueTest.php +++ /dev/null @@ -1,158 +0,0 @@ -author = Author::instance(9, 'Dan', 'Gebhardt'); - $this->comments = [ - Comment::instance(5, 'First!', $this->author), - Comment::instance(12, 'I like XML better', $this->author), - ]; - $this->post = Post::instance( - 1, - 'JSON API paints my bikeshed!', - 'Outside every fat man there was an even fatter man trying to close in', - $this->author, - $this->comments - ); - $this->site = Site::instance(2, 'site name', [$this->post]); - $this->encoderOptions = new EncoderOptions(0, 'http://example.com'); - } - - /** - * Test override SHOW_DATA=false with included path parameter. - */ - public function testOverrideShowDataWithIncluded() - { - $this->author->{Author::LINK_COMMENTS} = $this->comments; - - $actual = CustomEncoder::instance([ - Author::class => function ($factory) { - $schema = new AuthorSchema($factory); - $schema->linkAddTo(Author::LINK_COMMENTS, AuthorSchema::META, ['tip' => 'could be included']); - $schema->linkAddTo(Author::LINK_COMMENTS, AuthorSchema::SHOW_DATA, false); - return $schema; - }, - Comment::class => CommentSchema::class, - ], $this->encoderOptions)->encodeData($this->author, new EncodingParameters( - // include only these relationships - [Author::LINK_COMMENTS] - )); - - $expected = <<assertEquals($expected, $actual); - } -} diff --git a/tests/Extensions/Issue67/CustomEncoder.php b/tests/Extensions/Issue67/CustomEncoder.php index ce84b95f..3df8f926 100644 --- a/tests/Extensions/Issue67/CustomEncoder.php +++ b/tests/Extensions/Issue67/CustomEncoder.php @@ -1,10 +1,9 @@ -{Comment::LINK_AUTHOR}; - } else { - /** @var Author $author */ - $author = $comment->{Comment::LINK_AUTHOR}; - $authorId = $author->{Author::ATTRIBUTE_ID}; - - $data = $authorId === null ? null : new AuthorIdentity($authorId); - } + // emulate situation when we have only ID in relationship (e.g. user ID) and know type. + $author = $comment->{Comment::LINK_AUTHOR}; + $authorId = (string)$author->{Author::ATTRIBUTE_ID}; - $links = [ - Comment::LINK_AUTHOR => [self::DATA => $data], - ]; + $authorIdentity = new AuthorIdentity($authorId); - // NOTE: The line(s) below for testing purposes only. Not for production. - $this->fixLinks($comment, $links); - - return $links; - } + $hasMeta = property_exists($author, Author::IDENTIFIER_META); + if ($hasMeta === true) { + $authorIdentity->setMeta($author->{Author::IDENTIFIER_META}); + } - /** - * @inheritdoc - */ - public function getIncludedResourceLinks($resource): array - { - return []; + return $this->fixDescriptions( + $comment, + [ + Comment::LINK_AUTHOR => [self::RELATIONSHIP_DATA => $authorIdentity], + ] + ); } } diff --git a/tests/Extensions/Issue81/IssueTest.php b/tests/Extensions/Issue81/IssueTest.php index 8762f3d5..c8823512 100644 --- a/tests/Extensions/Issue81/IssueTest.php +++ b/tests/Extensions/Issue81/IssueTest.php @@ -1,7 +1,9 @@ -createIdentitySchema($container, $classType, $identityClosure); - - return $schema; - }; - } - /** * Test encoder will encode identities. * @@ -59,23 +35,15 @@ private function createIdentitySchema($classType, Closure $identityClosure) */ public function testEnheritedEncoder() { - $author = Author::instance('321', 'John', 'Dow'); - $comment = Comment::instance('123', 'Comment body', $author); + $author = Author::instance(321, 'John', 'Dow')->setIdentifierMeta('id meta'); + $comment = Comment::instance(123, 'Comment body', $author); $author->{Author::LINK_COMMENTS} = [$comment]; - $factory = new Factory(); - - // AuthorSchema is will provide JSON-API type however anything else from it won't be used - $container = new SchemaContainer($factory, [ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - AuthorIdentity::class => $this->createIdentitySchema(Author::class, function (AuthorIdentity $identity) { - return $identity->idx; - }), - ]); - $encoder = $factory->createEncoder($container); - - $actual = $encoder->encodeData($comment); + $actual = Encoder::instance( + [ + Comment::class => CommentSchema::class, + ] + )->encodeData($comment); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } } diff --git a/tests/Extensions/Issue81/SchemaContainer.php b/tests/Extensions/Issue81/SchemaContainer.php deleted file mode 100644 index 4589d909..00000000 --- a/tests/Extensions/Issue81/SchemaContainer.php +++ /dev/null @@ -1,46 +0,0 @@ -getFactory(), $this); - - return $schema; - } - - /** - * @inheritdoc - */ - protected function createSchemaFromClassName(string $className): SchemaInterface - { - $schema = new $className($this->getFactory(), $this); - - return $schema; - } -} diff --git a/tests/Extensions/Issue82/AuthorSchema.php b/tests/Extensions/Issue82/AuthorSchema.php new file mode 100644 index 00000000..f2abf91d --- /dev/null +++ b/tests/Extensions/Issue82/AuthorSchema.php @@ -0,0 +1,70 @@ +author_id; + } + + /** + * @inheritdoc + */ + public function getAttributes($author): iterable + { + return [ + 'first-name' => $author->first_name, + 'last-name' => $author->last_name, + ]; + } + + /** + * @inheritdoc + */ + public function getRelationships($author): iterable + { + return [ + 'comments' => [ + self::RELATIONSHIP_LINKS_SELF => false, + self::RELATIONSHIP_LINKS_RELATED => true, + + // Data could be included as well + // self::RELATIONSHIP_DATA => $author->comments, + ], + ]; + } +} diff --git a/tests/Extensions/Issue82/IssueTest.php b/tests/Extensions/Issue82/IssueTest.php new file mode 100644 index 00000000..194ee38b --- /dev/null +++ b/tests/Extensions/Issue82/IssueTest.php @@ -0,0 +1,69 @@ + AuthorSchema::class, + ]) + ->withUrlPrefix('http://example.com/api/v1') + ->withEncodeOptions(JSON_PRETTY_PRINT); + + $actual = $encoder->encodeData($author); + + $expected = <<index = $index; $this->description = $description; diff --git a/tests/Extensions/Issue91/CategorySchema.php b/tests/Extensions/Issue91/CategorySchema.php index cf23257f..78343763 100644 --- a/tests/Extensions/Issue91/CategorySchema.php +++ b/tests/Extensions/Issue91/CategorySchema.php @@ -1,7 +1,9 @@ -index; + assert($resource instanceof Category); + + return (string)$resource->index; } /** * @inheritdoc */ - public function getAttributes($resource, array $fieldKeysFilter = null): ?array + public function getAttributes($resource, array $fieldKeysFilter = null): iterable { /** @var Category $resource */ return [ @@ -51,21 +57,27 @@ public function getAttributes($resource, array $fieldKeysFilter = null): ?array /** * @inheritdoc */ - public function getRelationships($resource, bool $isPrimary, array $includeRelationships): ?array + public function getRelationships($resource): iterable { /** @var Category $resource */ return [ - 'parent' => [self::DATA => $resource->parent], + 'parent' => [self::RELATIONSHIP_DATA => $resource->parent], ]; } /** * @inheritdoc */ - public function getIncludePaths(): array + public function isAddSelfLinkInRelationshipByDefault(): bool { - return [ - 'parent', - ]; + return false; + } + + /** + * @inheritdoc + */ + public function isAddRelatedLinkInRelationshipByDefault(): bool + { + return false; } } diff --git a/tests/Extensions/Issue91/IssueTest.php b/tests/Extensions/Issue91/IssueTest.php index 53e1abd6..1e73c470 100644 --- a/tests/Extensions/Issue91/IssueTest.php +++ b/tests/Extensions/Issue91/IssueTest.php @@ -1,7 +1,9 @@ -createHierarchy(); - $actual = Encoder::instance([ - Category::class => CategorySchema::class, - ])->encodeData($hierarchy); + $actual = Encoder::instance( + [ + Category::class => CategorySchema::class, + ] + )->withIncludedPaths(['parent'])->encodeData($hierarchy); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** diff --git a/tests/Factories/ExceptionsTest.php b/tests/Factories/ExceptionsTest.php deleted file mode 100644 index 58a481b5..00000000 --- a/tests/Factories/ExceptionsTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertNotNull($logMock = Mockery::mock(LoggerInterface::class)); - - /** @var LoggerInterface $logMock */ - - $factory->setLogger($logMock); - } -} diff --git a/tests/Factories/ProxyLoggerTest.php b/tests/Factories/ProxyLoggerTest.php deleted file mode 100644 index 4e1f4b1f..00000000 --- a/tests/Factories/ProxyLoggerTest.php +++ /dev/null @@ -1,50 +0,0 @@ -assertNotNull($logMock = Mockery::mock(LoggerInterface::class)); - - $logger->debug('Nothing hapens. Should not fail.'); - - $logMock->shouldReceive('log')->once()->withAnyArgs()->andReturnUndefined(); - - /** @var LoggerInterface $logMock */ - - $logger->setLogger($logMock); - - $logger->debug('This one should trigger mock to log.'); - } -} diff --git a/tests/Http/Headers/AcceptHeaderTest.php b/tests/Http/Headers/AcceptHeaderTest.php index 0427584a..01239f25 100644 --- a/tests/Http/Headers/AcceptHeaderTest.php +++ b/tests/Http/Headers/AcceptHeaderTest.php @@ -1,7 +1,9 @@ -checkSorting([ - 'type/*', - '*/*', - 'foo/bar.baz', - ], $types); - - $this->assertEquals('type', $types[0]->getType()); - $this->assertEquals('*', $types[0]->getSubType()); - $this->assertEquals('type/*', $types[0]->getMediaType()); - $this->assertEquals(1, $types[0]->getQuality()); - $this->assertEquals(null, $types[0]->getParameters()); - - $this->assertEquals('*', $types[1]->getType()); - $this->assertEquals('*', $types[1]->getSubType()); - $this->assertEquals('*/*', $types[1]->getMediaType()); - $this->assertEquals(1, $types[1]->getQuality()); - $this->assertEquals(null, $types[1]->getParameters()); - - $this->assertEquals('foo', $types[2]->getType()); - $this->assertEquals('bar.baz', $types[2]->getSubType()); - $this->assertEquals('foo/bar.baz', $types[2]->getMediaType()); - $this->assertEquals(0.5, $types[2]->getQuality()); - $this->assertEquals(['media' => 'param'], $types[2]->getParameters()); + $this->checkSorting( + [ + 'type/*', + '*/*', + 'foo/bar.baz', + ], + $types + ); + + self::assertEquals('type', $types[0]->getType()); + self::assertEquals('*', $types[0]->getSubType()); + self::assertEquals('type/*', $types[0]->getMediaType()); + self::assertEquals(1, $types[0]->getQuality()); + self::assertEquals(null, $types[0]->getParameters()); + + self::assertEquals('*', $types[1]->getType()); + self::assertEquals('*', $types[1]->getSubType()); + self::assertEquals('*/*', $types[1]->getMediaType()); + self::assertEquals(1, $types[1]->getQuality()); + self::assertEquals(null, $types[1]->getParameters()); + + self::assertEquals('foo', $types[2]->getType()); + self::assertEquals('bar.baz', $types[2]->getSubType()); + self::assertEquals('foo/bar.baz', $types[2]->getMediaType()); + self::assertEquals(0.5, $types[2]->getQuality()); + self::assertEquals(['media' => 'param'], $types[2]->getParameters()); } /** @@ -80,22 +85,25 @@ public function testCompareByQuality1(): void usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'boo/baz', - 'foo/bar', - ], $types); - - $this->assertEquals('boo', $types[0]->getType()); - $this->assertEquals('baz', $types[0]->getSubType()); - $this->assertEquals('boo/baz', $types[0]->getMediaType()); - $this->assertEquals(0.6, $types[0]->getQuality()); - $this->assertEquals([], $types[0]->getParameters()); - - $this->assertEquals('foo', $types[1]->getType()); - $this->assertEquals('bar', $types[1]->getSubType()); - $this->assertEquals('foo/bar', $types[1]->getMediaType()); - $this->assertEquals(0.5, $types[1]->getQuality()); - $this->assertEquals([], $types[1]->getParameters()); + $this->checkSorting( + [ + 'boo/baz', + 'foo/bar', + ], + $types + ); + + self::assertEquals('boo', $types[0]->getType()); + self::assertEquals('baz', $types[0]->getSubType()); + self::assertEquals('boo/baz', $types[0]->getMediaType()); + self::assertEquals(0.6, $types[0]->getQuality()); + self::assertEquals([], $types[0]->getParameters()); + + self::assertEquals('foo', $types[1]->getType()); + self::assertEquals('bar', $types[1]->getSubType()); + self::assertEquals('foo/bar', $types[1]->getMediaType()); + self::assertEquals(0.5, $types[1]->getQuality()); + self::assertEquals([], $types[1]->getParameters()); } /** @@ -112,10 +120,13 @@ public function testCompareByQuality2(): void usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'foo/bar', - 'boo/baz', - ], $types); + $this->checkSorting( + [ + 'foo/bar', + 'boo/baz', + ], + $types + ); } /** @@ -133,22 +144,25 @@ public function testCompareBySubType(): void /** @var MediaTypeInterface[] $types */ usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'boo/baz', - 'foo/*', - ], $types); - - $this->assertEquals('boo', $types[0]->getType()); - $this->assertEquals('baz', $types[0]->getSubType()); - $this->assertEquals('boo/baz', $types[0]->getMediaType()); - $this->assertEquals(1.0, $types[0]->getQuality()); - $this->assertNull($types[0]->getParameters()); - - $this->assertEquals('foo', $types[1]->getType()); - $this->assertEquals('*', $types[1]->getSubType()); - $this->assertEquals('foo/*', $types[1]->getMediaType()); - $this->assertEquals(1.0, $types[1]->getQuality()); - $this->assertNull($types[1]->getParameters()); + $this->checkSorting( + [ + 'boo/baz', + 'foo/*', + ], + $types + ); + + self::assertEquals('boo', $types[0]->getType()); + self::assertEquals('baz', $types[0]->getSubType()); + self::assertEquals('boo/baz', $types[0]->getMediaType()); + self::assertEquals(1.0, $types[0]->getQuality()); + self::assertNull($types[0]->getParameters()); + + self::assertEquals('foo', $types[1]->getType()); + self::assertEquals('*', $types[1]->getSubType()); + self::assertEquals('foo/*', $types[1]->getMediaType()); + self::assertEquals(1.0, $types[1]->getQuality()); + self::assertNull($types[1]->getParameters()); } /** @@ -166,22 +180,25 @@ public function testCompareByParams(): void /** @var MediaTypeInterface[] $types */ usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'boo/baz', - 'foo/bar', - ], $types); - - $this->assertEquals('boo', $types[0]->getType()); - $this->assertEquals('baz', $types[0]->getSubType()); - $this->assertEquals('boo/baz', $types[0]->getMediaType()); - $this->assertEquals(1.0, $types[0]->getQuality()); - $this->assertEquals(['param' => 'value'], $types[0]->getParameters()); - - $this->assertEquals('foo', $types[1]->getType()); - $this->assertEquals('bar', $types[1]->getSubType()); - $this->assertEquals('foo/bar', $types[1]->getMediaType()); - $this->assertEquals(1.0, $types[1]->getQuality()); - $this->assertNull($types[1]->getParameters()); + $this->checkSorting( + [ + 'boo/baz', + 'foo/bar', + ], + $types + ); + + self::assertEquals('boo', $types[0]->getType()); + self::assertEquals('baz', $types[0]->getSubType()); + self::assertEquals('boo/baz', $types[0]->getMediaType()); + self::assertEquals(1.0, $types[0]->getQuality()); + self::assertEquals(['param' => 'value'], $types[0]->getParameters()); + + self::assertEquals('foo', $types[1]->getType()); + self::assertEquals('bar', $types[1]->getSubType()); + self::assertEquals('foo/bar', $types[1]->getMediaType()); + self::assertEquals(1.0, $types[1]->getQuality()); + self::assertNull($types[1]->getParameters()); } /** @@ -198,22 +215,25 @@ public function testCompareByPosition(): void usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'foo/bar', - 'boo/baz', - ], $types); - - $this->assertEquals('foo', $types[0]->getType()); - $this->assertEquals('bar', $types[0]->getSubType()); - $this->assertEquals('foo/bar', $types[0]->getMediaType()); - $this->assertEquals(0.5, $types[0]->getQuality()); - $this->assertEquals([], $types[0]->getParameters()); - - $this->assertEquals('boo', $types[1]->getType()); - $this->assertEquals('baz', $types[1]->getSubType()); - $this->assertEquals('boo/baz', $types[1]->getMediaType()); - $this->assertEquals(0.5, $types[1]->getQuality()); - $this->assertEquals([], $types[1]->getParameters()); + $this->checkSorting( + [ + 'foo/bar', + 'boo/baz', + ], + $types + ); + + self::assertEquals('foo', $types[0]->getType()); + self::assertEquals('bar', $types[0]->getSubType()); + self::assertEquals('foo/bar', $types[0]->getMediaType()); + self::assertEquals(0.5, $types[0]->getQuality()); + self::assertEquals([], $types[0]->getParameters()); + + self::assertEquals('boo', $types[1]->getType()); + self::assertEquals('baz', $types[1]->getSubType()); + self::assertEquals('boo/baz', $types[1]->getMediaType()); + self::assertEquals(0.5, $types[1]->getQuality()); + self::assertEquals([], $types[1]->getParameters()); } /** @@ -221,7 +241,7 @@ public function testCompareByPosition(): void * * @return void * - * @expectedException \InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ public function testInvalidParameters1(): void { @@ -233,7 +253,7 @@ public function testInvalidParameters1(): void * * @return void * - * @expectedException \InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ public function testInvalidParameters2(): void { @@ -243,7 +263,7 @@ public function testInvalidParameters2(): void /** * Test sample from RFC. */ - public function testParseHeaderRfcSample1() + public function testParseHeaderRfcSample1(): void { $types = [ new AcceptMediaType(0, 'audio', '*', null, 0.2), @@ -253,16 +273,19 @@ public function testParseHeaderRfcSample1() /** @var AcceptMediaTypeInterface[] $types */ usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'audio/basic', - 'audio/*', - ], $types); + $this->checkSorting( + [ + 'audio/basic', + 'audio/*', + ], + $types + ); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample2() + public function testParseHeaderRfcSample2(): void { $types = [ new AcceptMediaType(0, 'text', 'plain', null, 0.5), @@ -274,18 +297,21 @@ public function testParseHeaderRfcSample2() /** @var AcceptMediaTypeInterface[] $types */ usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'text/html', - 'text/x-c', - 'text/x-dvi', - 'text/plain', - ], $types); + $this->checkSorting( + [ + 'text/html', + 'text/x-c', + 'text/x-dvi', + 'text/plain', + ], + $types + ); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample3() + public function testParseHeaderRfcSample3(): void { $types = [ new AcceptMediaType(0, 'text', '*'), @@ -297,20 +323,23 @@ public function testParseHeaderRfcSample3() /** @var AcceptMediaTypeInterface[] $types */ usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'text/html', - 'text/html', - 'text/*', - '*/*', - ], $types); - - $this->assertEquals(['level' => '1'], $types[0]->getParameters()); + $this->checkSorting( + [ + 'text/html', + 'text/html', + 'text/*', + '*/*', + ], + $types + ); + + self::assertEquals(['level' => '1'], $types[0]->getParameters()); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample4() + public function testParseHeaderRfcSample4(): void { $types = [ new AcceptMediaType(0, 'text', '*', null, 0.3), @@ -323,17 +352,20 @@ public function testParseHeaderRfcSample4() /** @var AcceptMediaTypeInterface[] $types */ usort($types, AcceptMediaType::getCompare()); - $this->checkSorting([ - 'text/html', - 'text/html', - '*/*', - 'text/html', - 'text/*', - ], $types); - - $this->assertEquals(1.0, $types[0]->getQuality()); - $this->assertEquals(0.7, $types[1]->getQuality()); - $this->assertEquals(0.4, $types[3]->getQuality()); + $this->checkSorting( + [ + 'text/html', + 'text/html', + '*/*', + 'text/html', + 'text/*', + ], + $types + ); + + self::assertEquals(1.0, $types[0]->getQuality()); + self::assertEquals(0.7, $types[1]->getQuality()); + self::assertEquals(0.4, $types[3]->getQuality()); } /** @@ -344,10 +376,10 @@ public function testParseHeaderRfcSample4() */ private function checkSorting(array $sorted, array $mediaTypes): void { - $this->assertEquals($count = count($mediaTypes), count($sorted)); + self::assertEquals($count = count($mediaTypes), count($sorted)); for ($idx = 0; $idx < $count; ++$idx) { - $this->assertEquals($mediaTypes[$idx]->getMediaType(), $sorted[$idx]); + self::assertEquals($mediaTypes[$idx]->getMediaType(), $sorted[$idx]); } } } diff --git a/tests/Http/Headers/HeaderParametersParserTest.php b/tests/Http/Headers/HeaderParametersParserTest.php index 0d2a0e61..699219ad 100644 --- a/tests/Http/Headers/HeaderParametersParserTest.php +++ b/tests/Http/Headers/HeaderParametersParserTest.php @@ -1,7 +1,9 @@ -parser = (new Factory())->createHeaderParametersParser(); + $this->parser = new HeaderParametersParser(new Factory()); } /** @@ -58,14 +61,14 @@ protected function setUp() public function testParseHeadersNoParams1(): void { /** @var MediaTypeInterface $contentType */ - $contentType = $this->parser->parseContentTypeHeader(self::TYPE); - $this->assertEquals(self::TYPE, $contentType->getMediaType()); - $this->assertNull($contentType->getParameters()); + $contentType = $this->parser->parseContentTypeHeader(self::MEDIA_TYPE); + self::assertEquals(self::MEDIA_TYPE, $contentType->getMediaType()); + self::assertNull($contentType->getParameters()); /** @var AcceptMediaTypeInterface $accept */ - $accept = $this->first($this->parser->parseAcceptHeader(self::TYPE)); - $this->assertEquals(self::TYPE, $accept->getMediaType()); - $this->assertNull($accept->getParameters()); + $accept = $this->first($this->parser->parseAcceptHeader(self::MEDIA_TYPE)); + self::assertEquals(self::MEDIA_TYPE, $accept->getMediaType()); + self::assertNull($accept->getParameters()); } /** @@ -74,14 +77,14 @@ public function testParseHeadersNoParams1(): void public function testParseHeadersNoParams2(): void { /** @var MediaTypeInterface $contentType */ - $contentType = $this->parser->parseContentTypeHeader(self::TYPE); - $this->assertEquals(self::TYPE, $contentType->getMediaType()); - $this->assertNull($contentType->getParameters()); + $contentType = $this->parser->parseContentTypeHeader(self::MEDIA_TYPE); + self::assertEquals(self::MEDIA_TYPE, $contentType->getMediaType()); + self::assertNull($contentType->getParameters()); /** @var AcceptMediaTypeInterface $accept */ - $accept = $this->first($this->parser->parseAcceptHeader(self::TYPE . ';')); - $this->assertEquals(self::TYPE, $accept->getMediaType()); - $this->assertNull($accept->getParameters()); + $accept = $this->first($this->parser->parseAcceptHeader(self::MEDIA_TYPE . ';')); + self::assertEquals(self::MEDIA_TYPE, $accept->getMediaType()); + self::assertNull($accept->getParameters()); } /** @@ -89,17 +92,17 @@ public function testParseHeadersNoParams2(): void */ public function testParseHeadersWithParamsNoExtraParams(): void { - $contentType = $this->parser->parseContentTypeHeader(self::TYPE . ';ext="ext1,ext2"'); - $this->assertEquals(self::TYPE, $contentType->getMediaType()); + $contentType = $this->parser->parseContentTypeHeader(self::MEDIA_TYPE . ';ext="ext1,ext2"'); + self::assertEquals(self::MEDIA_TYPE, $contentType->getMediaType()); /** @var AcceptMediaTypeInterface $accept */ - $accept = $this->first($this->parser->parseAcceptHeader(self::TYPE . ';ext=ext1')); - $this->assertEquals(self::TYPE, $accept->getMediaType()); + $accept = $this->first($this->parser->parseAcceptHeader(self::MEDIA_TYPE . ';ext=ext1')); + self::assertEquals(self::MEDIA_TYPE, $accept->getMediaType()); - $this->assertEquals(self::TYPE, $contentType->getMediaType()); - $this->assertEquals(self::TYPE, $accept->getMediaType()); - $this->assertEquals(['ext' => 'ext1,ext2'], $contentType->getParameters()); - $this->assertEquals(['ext' => 'ext1'], $accept->getParameters()); + self::assertEquals(self::MEDIA_TYPE, $contentType->getMediaType()); + self::assertEquals(self::MEDIA_TYPE, $accept->getMediaType()); + self::assertEquals(['ext' => 'ext1,ext2'], $contentType->getParameters()); + self::assertEquals(['ext' => 'ext1'], $accept->getParameters()); } /** @@ -109,19 +112,21 @@ public function testParseHeadersWithParamsWithExtraParams(): void { /** @var AcceptMediaTypeInterface $accept */ $contentType = $this->parser->parseContentTypeHeader( - self::TYPE . ' ; boo = foo; ext="ext1,ext2"; foo = boo ' + self::MEDIA_TYPE . ' ; boo = foo; ext="ext1,ext2"; foo = boo ' + ); + $accept = $this->first( + $this->parser->parseAcceptHeader( + self::MEDIA_TYPE . ' ; boo = foo; ext=ext1; foo = boo' + ) ); - $accept = $this->first($this->parser->parseAcceptHeader( - self::TYPE . ' ; boo = foo; ext=ext1; foo = boo' - )); - $this->assertEquals(self::TYPE, $contentType->getMediaType()); - $this->assertEquals(self::TYPE, $accept->getMediaType()); - $this->assertEquals( + self::assertEquals(self::MEDIA_TYPE, $contentType->getMediaType()); + self::assertEquals(self::MEDIA_TYPE, $accept->getMediaType()); + self::assertEquals( ['boo' => 'foo', 'ext' => 'ext1,ext2', 'foo' => 'boo'], $contentType->getParameters() ); - $this->assertEquals( + self::assertEquals( ['boo' => 'foo', 'ext' => 'ext1', 'foo' => 'boo'], $accept->getParameters() ); @@ -130,7 +135,7 @@ public function testParseHeadersWithParamsWithExtraParams(): void /** * Test parse empty header. * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ public function testParseEmptyHeader1(): void { @@ -140,7 +145,7 @@ public function testParseEmptyHeader1(): void /** * Test parse empty header. * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ public function testParseEmptyHeader2(): void { @@ -150,27 +155,27 @@ public function testParseEmptyHeader2(): void /** * Test parse invalid headers. * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testParseIvalidHeaders1(): void + public function testParseInvalidHeaders1(): void { - $this->parser->parseContentTypeHeader(self::TYPE . ';foo'); + $this->parser->parseContentTypeHeader(self::MEDIA_TYPE . ';foo'); } /** * Test parse invalid headers. * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testParseIvalidHeaders2() + public function testParseInvalidHeaders2(): void { - $this->first($this->parser->parseAcceptHeader(self::TYPE . ';foo')); + $this->first($this->parser->parseAcceptHeader(self::MEDIA_TYPE . ';foo')); } /** * Test rfc2616 #3.9 (3 meaningful digits for quality) */ - public function testParserHeaderRfc2616P3p9Part1() + public function testParserHeaderRfc2616P3p9Part1(): void { $input = 'type1/*;q=0.5001, type2/*;q=0.5009'; @@ -180,13 +185,13 @@ public function testParserHeaderRfc2616P3p9Part1() $types[1]->getMediaType() => $types[1]->getQuality(), ]; - $this->assertCount(2, array_intersect(['type1/*' => 0.5, 'type2/*' => 0.5], $params)); + self::assertCount(2, array_intersect(['type1/*' => 0.5, 'type2/*' => 0.5], $params)); } /** * Test rfc2616 #3.9 (3 meaningful digits for quality) */ - public function testParserHeaderRfc2616P3p9Part2() + public function testParserHeaderRfc2616P3p9Part2(): void { $input = 'type1/*;q=0.501, type2/*;q=0.509'; @@ -196,13 +201,13 @@ public function testParserHeaderRfc2616P3p9Part2() $types[1]->getMediaType() => $types[1]->getQuality(), ]; - $this->assertCount(2, array_intersect(['type1/*' => 0.501, 'type2/*' => 0.509], $params)); + self::assertCount(2, array_intersect(['type1/*' => 0.501, 'type2/*' => 0.509], $params)); } /** * Test parsing multiple params. */ - public function testParserHeaderWithMultipleParameters() + public function testParserHeaderWithMultipleParameters(): void { $input = ' foo/bar.baz;media=param;q=0.5;ext="ext1,ext2", type/*'; @@ -215,114 +220,114 @@ public function testParserHeaderWithMultipleParameters() asort($params); - $this->assertEquals(['type/*' => null, 'foo/bar.baz' => ['media' => 'param']], $params); + self::assertEquals(['type/*' => null, 'foo/bar.baz' => ['media' => 'param']], $params); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample1() + public function testParseHeaderRfcSample1(): void { $input = 'audio/*; q=0.2, audio/basic'; /** @var AcceptMediaTypeInterface[] $types */ $types = $this->iterableToArray($this->parser->parseAcceptHeader($input)); - $this->assertCount(2, $types); - $this->assertEquals('audio/*', $types[0]->getMediaType()); - $this->assertEquals(0.2, $types[0]->getQuality()); - $this->assertEquals(0, $types[0]->getPosition()); - $this->assertEquals('audio/basic', $types[1]->getMediaType()); - $this->assertEquals(1.0, $types[1]->getQuality()); - $this->assertEquals(1, $types[1]->getPosition()); + self::assertEquals(2, count($types)); + self::assertEquals('audio/*', $types[0]->getMediaType()); + self::assertEquals(0.2, $types[0]->getQuality()); + self::assertEquals(0, $types[0]->getPosition()); + self::assertEquals('audio/basic', $types[1]->getMediaType()); + self::assertEquals(1.0, $types[1]->getQuality()); + self::assertEquals(1, $types[1]->getPosition()); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample2() + public function testParseHeaderRfcSample2(): void { $input = 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c'; /** @var AcceptMediaTypeInterface[] $types */ $types = $this->iterableToArray($this->parser->parseAcceptHeader($input)); - $this->assertCount(4, $types); - $this->assertEquals('text/plain', $types[0]->getMediaType()); - $this->assertEquals(0.5, $types[0]->getQuality()); - $this->assertEquals('text/html', $types[1]->getMediaType()); - $this->assertEquals(1.0, $types[1]->getQuality()); - $this->assertEquals('text/x-dvi', $types[2]->getMediaType()); - $this->assertEquals(0.8, $types[2]->getQuality()); - $this->assertEquals('text/x-c', $types[3]->getMediaType()); - $this->assertEquals(1.0, $types[3]->getQuality()); + self::assertEquals(4, count($types)); + self::assertEquals('text/plain', $types[0]->getMediaType()); + self::assertEquals(0.5, $types[0]->getQuality()); + self::assertEquals('text/html', $types[1]->getMediaType()); + self::assertEquals(1.0, $types[1]->getQuality()); + self::assertEquals('text/x-dvi', $types[2]->getMediaType()); + self::assertEquals(0.8, $types[2]->getQuality()); + self::assertEquals('text/x-c', $types[3]->getMediaType()); + self::assertEquals(1.0, $types[3]->getQuality()); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample3() + public function testParseHeaderRfcSample3(): void { $input = 'text/*, text/html, text/html;level=1, */*'; /** @var AcceptMediaTypeInterface[] $types */ $types = $this->iterableToArray($this->parser->parseAcceptHeader($input)); - $this->assertCount(4, $types); - $this->assertEquals('text/*', $types[0]->getMediaType()); - $this->assertEquals(1.0, $types[0]->getQuality()); - $this->assertEquals('text/html', $types[1]->getMediaType()); - $this->assertEquals(null, $types[0]->getParameters()); - $this->assertEquals(1.0, $types[1]->getQuality()); - $this->assertEquals(null, $types[1]->getParameters()); - $this->assertEquals('text/html', $types[2]->getMediaType()); - $this->assertEquals(1.0, $types[2]->getQuality()); - $this->assertEquals(['level' => '1'], $types[2]->getParameters()); - $this->assertEquals('*/*', $types[3]->getMediaType()); - $this->assertEquals(1.0, $types[3]->getQuality()); - $this->assertEquals(null, $types[3]->getParameters()); + self::assertEquals(4, count($types)); + self::assertEquals('text/*', $types[0]->getMediaType()); + self::assertEquals(1.0, $types[0]->getQuality()); + self::assertEquals('text/html', $types[1]->getMediaType()); + self::assertEquals(null, $types[0]->getParameters()); + self::assertEquals(1.0, $types[1]->getQuality()); + self::assertEquals(null, $types[1]->getParameters()); + self::assertEquals('text/html', $types[2]->getMediaType()); + self::assertEquals(1.0, $types[2]->getQuality()); + self::assertEquals(['level' => '1'], $types[2]->getParameters()); + self::assertEquals('*/*', $types[3]->getMediaType()); + self::assertEquals(1.0, $types[3]->getQuality()); + self::assertEquals(null, $types[3]->getParameters()); } /** * Test sample from RFC. */ - public function testParseHeaderRfcSample4() + public function testParseHeaderRfcSample4(): void { $input = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'; /** @var AcceptMediaTypeInterface[] $types */ $types = $this->iterableToArray($this->parser->parseAcceptHeader($input)); - $this->assertCount(5, $types); - $this->assertEquals('text/*', $types[0]->getMediaType()); - $this->assertEquals(0.3, $types[0]->getQuality()); - $this->assertEquals(null, $types[0]->getParameters()); - $this->assertEquals('text/html', $types[1]->getMediaType()); - $this->assertEquals(0.7, $types[1]->getQuality()); - $this->assertEquals(null, $types[1]->getParameters()); - $this->assertEquals('text/html', $types[2]->getMediaType()); - $this->assertEquals(1.0, $types[2]->getQuality()); - $this->assertEquals(['level' => '1'], $types[2]->getParameters()); - $this->assertEquals('text/html', $types[3]->getMediaType()); - $this->assertEquals(0.4, $types[3]->getQuality()); - $this->assertEquals(['level' => '2'], $types[3]->getParameters()); - $this->assertEquals('*/*', $types[4]->getMediaType()); - $this->assertEquals(0.5, $types[4]->getQuality()); - $this->assertEquals(null, $types[4]->getParameters()); + self::assertEquals(5, count($types)); + self::assertEquals('text/*', $types[0]->getMediaType()); + self::assertEquals(0.3, $types[0]->getQuality()); + self::assertEquals(null, $types[0]->getParameters()); + self::assertEquals('text/html', $types[1]->getMediaType()); + self::assertEquals(0.7, $types[1]->getQuality()); + self::assertEquals(null, $types[1]->getParameters()); + self::assertEquals('text/html', $types[2]->getMediaType()); + self::assertEquals(1.0, $types[2]->getQuality()); + self::assertEquals(['level' => '1'], $types[2]->getParameters()); + self::assertEquals('text/html', $types[3]->getMediaType()); + self::assertEquals(0.4, $types[3]->getQuality()); + self::assertEquals(['level' => '2'], $types[3]->getParameters()); + self::assertEquals('*/*', $types[4]->getMediaType()); + self::assertEquals(0.5, $types[4]->getQuality()); + self::assertEquals(null, $types[4]->getParameters()); } /** - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testInvalidHeader1() + public function testInvalidHeader1(): void { $this->parser->parseContentTypeHeader(''); } /** - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testInvalidHeader2() + public function testInvalidHeader2(): void { $this->parser->parseContentTypeHeader('foo/bar; baz'); } @@ -330,9 +335,9 @@ public function testInvalidHeader2() /** * @see https://github.com/neomerx/json-api/issues/193 * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testInvalidHeader3() + public function testInvalidHeader3(): void { $this->parser->parseContentTypeHeader('application/vnd.api+json;q=0.5,text/html;q=0.8;*/*;q=0.1'); } @@ -340,9 +345,9 @@ public function testInvalidHeader3() /** * Test invalid parse parameters. * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testInvalidParseParams1() + public function testInvalidParseParams1(): void { $this->first($this->parser->parseAcceptHeader('boo.bar+baz')); } @@ -350,13 +355,28 @@ public function testInvalidParseParams1() /** * Test invalid parse parameters. * - * @expectedException InvalidArgumentException + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException */ - public function testInvalidParseParams2() + public function testInvalidParseParams2(): void { $this->first($this->parser->parseAcceptHeader('boo/bar+baz;param')); } + /** + * Test parse parameters. + */ + public function testParseAcceptHeaderWithJsonApiProfile(): void + { + /** @var AcceptMediaTypeInterface $accept */ + $accept = $this->first( + $this->parser->parseAcceptHeader( + 'application/vnd.api+json;profile="http://example.com/last-modified",application/vnd.api+json' + ) + ); + self::assertEquals(self::MEDIA_TYPE, $accept->getMediaType()); + self::assertEquals(['profile' => 'http://example.com/last-modified'], $accept->getParameters()); + } + /** * @param iterable $iterable * diff --git a/tests/Http/Headers/MediaTypeTest.php b/tests/Http/Headers/MediaTypeTest.php index b4c69d84..186158fd 100644 --- a/tests/Http/Headers/MediaTypeTest.php +++ b/tests/Http/Headers/MediaTypeTest.php @@ -1,7 +1,9 @@ - 'utf-8']); - $this->assertEquals('text/html', $type->getMediaType()); + self::assertEquals('text/html', $type->getMediaType()); } /** - * Test compare media types + * Test compare media types (case insensitive) * * @return void */ @@ -74,11 +76,26 @@ public function testCompareMediaTypes(): void $type5 = new MediaType('text', 'html', ['charset' => 'UTF-8']); $type6 = new MediaType('text', 'html', ['charset' => 'UTF-8', 'oneMore' => 'param']); - $this->assertTrue($type1->equalsTo($type2)); - $this->assertFalse($type1->equalsTo($type3)); - $this->assertFalse($type1->equalsTo($type4)); - $this->assertTrue($type1->equalsTo($type5)); - $this->assertFalse($type1->equalsTo($type6)); + self::assertTrue($type1->equalsTo($type2)); + self::assertFalse($type1->equalsTo($type3)); + self::assertFalse($type1->equalsTo($type4)); + self::assertTrue($type1->equalsTo($type5)); + self::assertFalse($type1->equalsTo($type6)); + } + + /** + * Test compare media types (case sensitive) + * + * @return void + */ + public function testCompareMediaTypes2(): void + { + $type1 = new MediaType('text', 'html', ['case-sensitive-value' => 'whatever']); + $type2 = new MediaType('text', 'html', ['case-sensitive-value' => 'WHATEVER']); + $type3 = new MediaType('text', 'html', ['CASE-SENSITIVE-VALUE' => 'whatever']); + + self::assertFalse($type1->equalsTo($type2)); + self::assertTrue($type1->equalsTo($type3)); } /** @@ -93,9 +110,9 @@ public function testMatchMediaTypes(): void $type3 = new MediaType('text', '*', ['charset' => 'utf-8']); $type4 = new MediaType('whatever', '*', ['charset' => 'utf-8']); - $this->assertTrue($type1->matchesTo($type2)); - $this->assertTrue($type1->matchesTo($type3)); - $this->assertFalse($type1->matchesTo($type4)); + self::assertTrue($type1->matchesTo($type2)); + self::assertTrue($type1->matchesTo($type3)); + self::assertFalse($type1->matchesTo($type4)); } /** @@ -108,6 +125,6 @@ public function testMatchMediaTypesWithoutParameters(): void $type1 = new MediaType('text', 'html'); $type2 = new MediaType('Text', 'HTML'); - $this->assertTrue($type1->matchesTo($type2)); + self::assertTrue($type1->matchesTo($type2)); } } diff --git a/tests/Http/Query/BaseQueryParserTest.php b/tests/Http/Query/BaseQueryParserTest.php index 55911b16..75266666 100644 --- a/tests/Http/Query/BaseQueryParserTest.php +++ b/tests/Http/Query/BaseQueryParserTest.php @@ -1,7 +1,9 @@ -createParser($queryParameters); - $this->assertEquals([], $this->iterableToArray($parser->getIncludes())); - $this->assertEquals([], $this->iterableToArray($parser->getFields())); + self::assertEquals([], $this->iterableToArray($parser->getIncludes())); + self::assertEquals([], $this->iterableToArray($parser->getFields())); } /** @@ -56,10 +57,13 @@ public function testIncludes(): void $parser = $this->createParser($queryParameters); - $this->assertEquals([ - 'comments' => ['comments'], - 'comments.author' => ['comments', 'author'], - ], $this->iterableToArray($parser->getIncludes())); + self::assertEquals( + [ + 'comments' => ['comments'], + 'comments.author' => ['comments', 'author'], + ], + $this->iterableToArray($parser->getIncludes()) + ); } /** @@ -73,9 +77,12 @@ public function testIncludesForStringWithZeroes1(): void $parser = $this->createParser($queryParameters); - $this->assertEquals([ - '0' => ['0'], - ], $this->iterableToArray($parser->getIncludes())); + self::assertEquals( + [ + '0' => ['0'], + ], + $this->iterableToArray($parser->getIncludes()) + ); } /** @@ -89,10 +96,13 @@ public function testIncludesForStringWithZeroes2(): void $parser = $this->createParser($queryParameters); - $this->assertEquals([ - '0' => ['0'], - '1' => ['1'], - ], $this->iterableToArray($parser->getIncludes())); + self::assertEquals( + [ + '0' => ['0'], + '1' => ['1'], + ], + $this->iterableToArray($parser->getIncludes()) + ); } /** @@ -109,10 +119,13 @@ public function testFields(): void $parser = $this->createParser($queryParameters); - $this->assertEquals([ - 'articles' => ['title', 'body'], - 'people' => ['name'], - ], $this->iterableToArray($parser->getFields())); + self::assertEquals( + [ + 'articles' => ['title', 'body'], + 'people' => ['name'], + ], + $this->iterableToArray($parser->getFields()) + ); } /** @@ -126,11 +139,14 @@ public function testSorts(): void $parser = $this->createParser($queryParameters); - $this->assertEquals([ - 'created' => false, - 'title' => true, - 'updated' => true, - ], $this->iterableToArray($parser->getSorts())); + self::assertEquals( + [ + 'created' => false, + 'title' => true, + 'updated' => true, + ], + $this->iterableToArray($parser->getSorts()) + ); } /** @@ -226,6 +242,9 @@ public function testInvalidFields(): void */ public function testIntegrationWithEncodingParameters(): void { + $profileUrl1 = 'http://example1.com/foo'; + $profileUrl2 = 'http://example2.com/boo'; + $queryParameters = [ BaseQueryParser::PARAM_FIELDS => [ 'comments' => Comment::LINK_AUTHOR . ', ' . Comment::ATTRIBUTE_BODY . ' ', @@ -234,6 +253,7 @@ public function testIntegrationWithEncodingParameters(): void BaseQueryParser::PARAM_SORT => '-created,title,+updated', BaseQueryParser::PARAM_INCLUDE => Comment::LINK_AUTHOR . ', ' . Comment::LINK_AUTHOR . '.' . Author::LINK_COMMENTS, + BaseQueryParser::PARAM_PROFILE => urlencode(implode(' ', [$profileUrl1, $profileUrl2])), ]; // It is expected that classes that encapsulate/extend BaseQueryParser would add features @@ -256,6 +276,11 @@ public function testIntegrationWithEncodingParameters(): void */ private $includes = null; + /** + * @var null|array + */ + private $profile = null; + /** * @return array */ @@ -280,6 +305,18 @@ public function getSorts(): array return $this->sorts; } + /** + * @return array + */ + public function getProfileUrls(): array + { + if ($this->profile === null) { + $this->profile = $this->iterableToArray(parent::getProfileUrls()); + } + + return $this->profile; + } + /** * @return array */ @@ -310,19 +347,35 @@ private function iterableToArray(iterable $iterable): array }; // Check parsing works fine - $this->assertSame([ - 'comments' => [Comment::LINK_AUTHOR, Comment::ATTRIBUTE_BODY], - 'people' => [Author::ATTRIBUTE_FIRST_NAME], - ], $parser->getFields()); - $this->assertSame([ - 'created' => false, - 'title' => true, - 'updated' => true, - ], $parser->getSorts()); - $this->assertSame([ - Comment::LINK_AUTHOR, - Comment::LINK_AUTHOR . '.' . Author::LINK_COMMENTS, - ], $parser->getIncludes()); + self::assertSame( + [ + 'comments' => [Comment::LINK_AUTHOR, Comment::ATTRIBUTE_BODY], + 'people' => [Author::ATTRIBUTE_FIRST_NAME], + ], + $parser->getFields() + ); + self::assertSame( + [ + 'created' => false, + 'title' => true, + 'updated' => true, + ], + $parser->getSorts() + ); + self::assertSame( + [ + Comment::LINK_AUTHOR, + Comment::LINK_AUTHOR . '.' . Author::LINK_COMMENTS, + ], + $parser->getIncludes() + ); + self::assertSame( + [ + $profileUrl1, + $profileUrl2, + ], + $parser->getProfileUrls() + ); // // Now the main purpose of the test. Will it work with EncodingParameters? @@ -337,11 +390,15 @@ private function iterableToArray(iterable $iterable): array $author->{Author::LINK_COMMENTS} = $comments; // and encode with params taken from the parser - $encodingParams = new EncodingParameters($parser->getIncludes(), $parser->getFields()); - $actual = Encoder::instance([ - Author::class => AuthorSchema::class, - Comment::class => CommentSchema::class, - ])->encodeData($comments, $encodingParams); + $actual = Encoder::instance( + [ + Author::class => AuthorSchema::class, + Comment::class => CommentSchema::class, + ] + ) + ->withIncludedPaths($parser->getIncludes()) + ->withFieldSets($parser->getFields()) + ->encodeData($comments); $expected = <<assertEquals($expected, $actual); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** diff --git a/tests/Http/Query/EncodingParametersTest.php b/tests/Http/Query/EncodingParametersTest.php deleted file mode 100644 index 4d604696..00000000 --- a/tests/Http/Query/EncodingParametersTest.php +++ /dev/null @@ -1,55 +0,0 @@ -assertNull($parameters->getFieldSets()); - $this->assertNull($parameters->getIncludePaths()); - $this->assertNull($parameters->getFieldSet('whatever')); - } - - /** - * Test not null encoding parameters. - */ - public function testNotNulls() - { - $type = 'type'; - $parameters = new EncodingParameters( - $paths = [$type => ['some.path']], - $fieldsets = [$type => ['field1', 'field2']] - ); - - $this->assertEquals($fieldsets, $parameters->getFieldSets()); - $this->assertEquals($paths, $parameters->getIncludePaths()); - $this->assertEquals(null, $parameters->getFieldSet('typeNotInSet')); - $this->assertEquals($fieldsets[$type], $parameters->getFieldSet($type)); - } -} diff --git a/tests/Http/Query/FactoryTest.php b/tests/Http/Query/FactoryTest.php deleted file mode 100644 index 268f75b1..00000000 --- a/tests/Http/Query/FactoryTest.php +++ /dev/null @@ -1,84 +0,0 @@ -factory = new Factory(); - } - - /** - * Test create media type. - */ - public function testCreateMediaType() - { - $this->assertNotNull($type = $this->factory->createMediaType( - $mediaType = 'media', - $mediaSubType = 'type.abc', - $parameters = ['someKey' => 'ext1,ext2'] - )); - - $this->assertEquals("$mediaType/$mediaSubType", $type->getMediaType()); - $this->assertEquals($parameters, $type->getParameters()); - } - - /** - * Test create parameters. - */ - public function testCreateParameters() - { - $this->assertNotNull($parameters = $this->factory->createQueryParameters( - $includePaths = ['some-type' => ['p1', 'p2']], - $fieldSets = ['s1' => ['value11', 'value12']] - )); - - $this->assertEquals($includePaths, $parameters->getIncludePaths()); - $this->assertEquals($fieldSets, $parameters->getFieldSets()); - } - - /** - * Test create encoding parameters. - */ - public function testCreateEncodingParameters() - { - $this->assertNotNull($parameters = $this->factory->createQueryParameters( - $includePaths = ['some-type' => ['p1', 'p2']], - $fieldSets = ['s1' => ['value11', 'value12']] - )); - - $this->assertEquals($includePaths, $parameters->getIncludePaths()); - $this->assertEquals($fieldSets, $parameters->getFieldSets()); - } -} diff --git a/tests/Http/ResponsesTest.php b/tests/Http/ResponsesTest.php index 3796e4e2..809565cb 100644 --- a/tests/Http/ResponsesTest.php +++ b/tests/Http/ResponsesTest.php @@ -1,7 +1,9 @@ -willBeCalledCreateResponse(null, 123, $expectedHeaders, 'some response'); - $this->assertEquals('some response', $this->responses->getCodeResponse(123)); + self::assertEquals('some response', $this->responses->getCodeResponse(123)); } /** * Test response. */ - public function testContentResponse1() + public function testContentResponse1(): void { - $data = new stdClass(); - $links = ['some' => 'links']; - $meta = ['some' => 'meta']; + $data = new stdClass(); $this->willBeCalledGetMediaType('some', 'type'); - $this->willBeCalledEncoderForData($data, 'some json api', $links, $meta); + $this->willBeCalledEncoderForData($data, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getContentResponse($data, 321, $links, $meta)); + self::assertEquals('some response', $this->responses->getContentResponse($data, 321)); } /** * Test content response, with custom headers. */ - public function testContentResponse2() + public function testContentResponse2(): void { - $data = new stdClass(); - $links = ['some' => 'links']; - $meta = ['some' => 'meta']; + $data = new stdClass(); $this->willBeCalledGetMediaType('some', 'type'); - $this->willBeCalledEncoderForData($data, 'some json api', $links, $meta); + $this->willBeCalledEncoderForData($data, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type', 'X-Custom' => 'Custom-Header']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getContentResponse($data, 321, $links, $meta, [ - 'X-Custom' => 'Custom-Header', - ])); + self::assertEquals( + 'some response', + $this->responses->getContentResponse( + $data, + 321, + [ + 'X-Custom' => 'Custom-Header', + ] + ) + ); } /** * Test response. */ - public function testCreatedResponse1() + public function testCreatedResponse1(): void { $resource = new stdClass(); - $links = ['some' => 'links']; - $meta = ['some' => 'meta']; + $location = 'http://server.tld/resource-type/123'; $this->willBeCalledGetMediaType('some', 'type'); - $this->willBeCalledEncoderForData($resource, 'some json api', $links, $meta); - $this->willBeCreatedResourceLocationUrl($resource, 'http://server.tld', '/resource-type/123'); + $this->willBeCalledEncoderForData($resource, 'some json api'); $headers = [ BaseResponses::HEADER_CONTENT_TYPE => 'some/type', - BaseResponses::HEADER_LOCATION => 'http://server.tld/resource-type/123', + BaseResponses::HEADER_LOCATION => $location, ]; $this->willBeCalledCreateResponse('some json api', BaseResponses::HTTP_CREATED, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getCreatedResponse($resource, $links, $meta)); + self::assertEquals('some response', $this->responses->getCreatedResponse($resource, $location)); } /** * Test response, with custom headers */ - public function testCreatedResponse2() + public function testCreatedResponse2(): void { $resource = new stdClass(); - $links = ['some' => 'links']; - $meta = ['some' => 'meta']; + $location = 'http://server.tld'; $this->willBeCalledGetMediaType('some', 'type'); - $this->willBeCalledEncoderForData($resource, 'some json api', $links, $meta); - $this->willBeCreatedResourceLocationUrl($resource, 'http://server.tld', '/resource-type/123'); + $this->willBeCalledEncoderForData($resource, 'some json api'); $headers = [ BaseResponses::HEADER_CONTENT_TYPE => 'some/type', - BaseResponses::HEADER_LOCATION => 'http://server.tld/resource-type/123', + BaseResponses::HEADER_LOCATION => $location, 'X-Custom' => 'Custom-Header', ]; $this->willBeCalledCreateResponse('some json api', BaseResponses::HTTP_CREATED, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getCreatedResponse($resource, $links, $meta, [ - 'X-Custom' => 'Custom-Header', - ])); + self::assertEquals( + 'some response', + $this->responses->getCreatedResponse( + $resource, + $location, + [ + 'X-Custom' => 'Custom-Header', + ] + ) + ); } /** * Test response. */ - public function testMetaResponse1() + public function testMetaResponse1(): void { $meta = new stdClass(); $this->willBeCalledGetMediaType('some', 'type'); $this->willBeCalledEncoderForMeta($meta, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getMetaResponse($meta, 321)); + self::assertEquals('some response', $this->responses->getMetaResponse($meta, 321)); } /** * Test response, with custom headers */ - public function testMetaResponse2() + public function testMetaResponse2(): void { $meta = new stdClass(); $this->willBeCalledGetMediaType('some', 'type'); $this->willBeCalledEncoderForMeta($meta, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type', 'X-Custom' => 'Custom-Header']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getMetaResponse($meta, 321, [ - 'X-Custom' => 'Custom-Header', - ])); + self::assertEquals( + 'some response', + $this->responses->getMetaResponse( + $meta, + 321, + [ + 'X-Custom' => 'Custom-Header', + ] + ) + ); } /** * Test identifiers response. */ - public function testIdentifiersResponse1() + public function testIdentifiersResponse1(): void { - $data = new stdClass(); - $links = ['some' => 'links']; - $meta = ['some' => 'meta']; + $data = new stdClass(); $this->willBeCalledGetMediaType('some', 'type'); - $this->willBeCalledEncoderForIdentifiers($data, 'some json api', $links, $meta); + $this->willBeCalledEncoderForIdentifiers($data, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getIdentifiersResponse($data, 321, $links, $meta)); + self::assertEquals('some response', $this->responses->getIdentifiersResponse($data, 321)); } /** * Test identifiers response, with custom headers. */ - public function testIdentifiersResponse2() + public function testIdentifiersResponse2(): void { - $data = new stdClass(); - $links = ['some' => 'links']; - $meta = ['some' => 'meta']; + $data = new stdClass(); $this->willBeCalledGetMediaType('some', 'type'); - $this->willBeCalledEncoderForIdentifiers($data, 'some json api', $links, $meta); + $this->willBeCalledEncoderForIdentifiers($data, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type', 'X-Custom' => 'Custom-Header']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getIdentifiersResponse($data, 321, $links, $meta, [ - 'X-Custom' => 'Custom-Header', - ])); + self::assertEquals( + 'some response', + $this->responses->getIdentifiersResponse( + $data, + 321, + [ + 'X-Custom' => 'Custom-Header', + ] + ) + ); } /** * Test response. */ - public function testErrorResponse1() + public function testErrorResponse1(): void { $error = new Error(); $this->willBeCalledGetMediaType('some', 'type'); $this->willBeCalledEncoderForError($error, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getErrorResponse($error, 321)); + self::assertEquals('some response', $this->responses->getErrorResponse($error, 321)); } /** * Test response. */ - public function testErrorResponse2() + public function testErrorResponse2(): void { $errors = [new Error()]; $this->willBeCalledGetMediaType('some', 'type'); $this->willBeCalledEncoderForErrors($errors, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getErrorResponse($errors, 321)); + self::assertEquals('some response', $this->responses->getErrorResponse($errors, 321)); } /** * Test response. */ - public function testErrorResponse3() + public function testErrorResponse3(): void { $errors = new ErrorCollection(); $errors->add(new Error()); @@ -242,22 +251,29 @@ public function testErrorResponse3() $this->willBeCalledEncoderForErrors($errors, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getErrorResponse($errors, 321)); + self::assertEquals('some response', $this->responses->getErrorResponse($errors, 321)); } /** * Test response, with custom headers. */ - public function testErrorResponse4() + public function testErrorResponse4(): void { $error = new Error(); $this->willBeCalledGetMediaType('some', 'type'); $this->willBeCalledEncoderForError($error, 'some json api'); $headers = [BaseResponses::HEADER_CONTENT_TYPE => 'some/type', 'X-Custom' => 'Custom-Header']; $this->willBeCalledCreateResponse('some json api', 321, $headers, 'some response'); - $this->assertEquals('some response', $this->responses->getErrorResponse($error, 321, [ - 'X-Custom' => 'Custom-Header', - ])); + self::assertEquals( + 'some response', + $this->responses->getErrorResponse( + $error, + 321, + [ + 'X-Custom' => 'Custom-Header', + ] + ) + ); } /** @@ -267,7 +283,7 @@ public function testErrorResponse4() * * @return void */ - private function willBeCalledGetMediaType($type, $subType, array $parameters = null) + private function willBeCalledGetMediaType(string $type, string $subType, array $parameters = null): void { $mediaType = new MediaType($type, $subType, $parameters); @@ -283,7 +299,7 @@ private function willBeCalledGetMediaType($type, $subType, array $parameters = n * * @return void */ - private function willBeCalledCreateResponse($content, $httpCode, array $headers, $response) + private function willBeCalledCreateResponse(?string $content, int $httpCode, array $headers, $response): void { /** @noinspection PhpMethodParametersCountMismatchInspection */ $this->mock->shouldReceive('createResponse')->once() @@ -291,143 +307,88 @@ private function willBeCalledCreateResponse($content, $httpCode, array $headers, } /** - * @param bool $withEncodingParams - * * @return MockInterface */ - private function willBeCalledGetEncoder($withEncodingParams) + private function willBeCalledGetEncoder(): MockInterface { $encoderMock = Mockery::mock(EncoderInterface::class); /** @noinspection PhpMethodParametersCountMismatchInspection */ $this->mock->shouldReceive('getEncoder')->once()->withNoArgs()->andReturn($encoderMock); - if ($withEncodingParams === true) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->mock->shouldReceive('getEncodingParameters') - ->once()->withNoArgs()->andReturn($this->encodingencodingParameters); - } return $encoderMock; } /** - * @param mixed $data - * @param string $result - * @param array|null $links - * @param mixed $meta + * @param mixed $data + * @param string $result * * @return void */ - private function willBeCalledEncoderForData($data, $result, array $links = null, $meta = null) + private function willBeCalledEncoderForData($data, string $result): void { - $encoderMock = $this->willBeCalledGetEncoder(true); - - if ($links !== null) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $encoderMock->shouldReceive('withLinks')->once()->with($links)->andReturnSelf(); - } - - if ($meta !== null) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $encoderMock->shouldReceive('withMeta')->once()->with($meta)->andReturnSelf(); - } + $encoderMock = $this->willBeCalledGetEncoder(); /** @noinspection PhpMethodParametersCountMismatchInspection */ $encoderMock->shouldReceive('encodeData') ->once() - ->withArgs([$data, $this->encodingencodingParameters]) + ->withArgs([$data]) ->andReturn($result); } - /** - * @param mixed $resource - * @param string|null $prefix - * @param string $subUrl - * - * @return void - */ - private function willBeCreatedResourceLocationUrl($resource, $prefix, $subUrl) - { - $linkMock = Mockery::mock(LinkInterface::class); - - $containerMock = Mockery::mock(ContainerInterface::class); - $providerMock = Mockery::mock(SchemaInterface::class); - - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $this->mock->shouldReceive('getSchemaContainer')->once()->withNoArgs()->andReturn($containerMock); - - $containerMock->shouldReceive('getSchema')->once()->with($resource)->andReturn($providerMock); - $providerMock->shouldReceive('getSelfSubLink')->once()->with($resource)->andReturn($linkMock); - $linkMock->shouldReceive('getSubHref')->once()->withNoArgs()->andReturn($subUrl); - - $this->mock->shouldReceive('getUrlPrefix')->once()->withNoArgs()->andReturn($prefix); - } - /** * @param mixed $meta * @param string $result * * @return void */ - private function willBeCalledEncoderForMeta($meta, $result) + private function willBeCalledEncoderForMeta($meta, string $result): void { - $encoderMock = $this->willBeCalledGetEncoder(false); + $encoderMock = $this->willBeCalledGetEncoder(); /** @noinspection PhpMethodParametersCountMismatchInspection */ $encoderMock->shouldReceive('encodeMeta')->once()->with($meta)->andReturn($result); } /** - * @param mixed $data - * @param string $result - * @param array|null $links - * @param mixed $meta + * @param mixed $data + * @param string $result * * @return void */ - private function willBeCalledEncoderForIdentifiers($data, $result, array $links = null, $meta = null) + private function willBeCalledEncoderForIdentifiers($data, string $result): void { - $encoderMock = $this->willBeCalledGetEncoder(true); - - if ($links !== null) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $encoderMock->shouldReceive('withLinks')->once()->with($links)->andReturnSelf(); - } - - if ($meta !== null) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $encoderMock->shouldReceive('withMeta')->once()->with($meta)->andReturnSelf(); - } + $encoderMock = $this->willBeCalledGetEncoder(); /** @noinspection PhpMethodParametersCountMismatchInspection */ $encoderMock->shouldReceive('encodeIdentifiers') ->once() - ->withArgs([$data, $this->encodingencodingParameters]) + ->withArgs([$data]) ->andReturn($result); } /** - * @param mixed|Error $error - * @param string $result + * @param mixed $error + * @param string $result * * @return void */ - private function willBeCalledEncoderForError($error, $result) + private function willBeCalledEncoderForError($error, string $result): void { - $encoderMock = $this->willBeCalledGetEncoder(false); + $encoderMock = $this->willBeCalledGetEncoder(); /** @noinspection PhpMethodParametersCountMismatchInspection */ $encoderMock->shouldReceive('encodeError')->once()->with($error)->andReturn($result); } /** - * @param mixed|Error[] $errors - * @param string $result + * @param iterable $errors + * @param string $result * * @return void */ - private function willBeCalledEncoderForErrors($errors, $result) + private function willBeCalledEncoderForErrors(iterable $errors, string $result): void { - $encoderMock = $this->willBeCalledGetEncoder(false); + $encoderMock = $this->willBeCalledGetEncoder(); /** @noinspection PhpMethodParametersCountMismatchInspection */ $encoderMock->shouldReceive('encodeErrors')->once()->with($errors)->andReturn($result); diff --git a/tests/I18n/MessagesTest.php b/tests/I18n/MessagesTest.php new file mode 100644 index 00000000..8fcab98c --- /dev/null +++ b/tests/I18n/MessagesTest.php @@ -0,0 +1,47 @@ + 'boo', + 'foo %d %s' => '%d %s boo', + ]); + self::assertEquals('boo', _('foo')); + self::assertEquals('123 text boo', _('foo %d %s', 123, 'text')); + } +} diff --git a/tests/Parser/RelationshipDataTest.php b/tests/Parser/RelationshipDataTest.php new file mode 100644 index 00000000..a09d9a05 --- /dev/null +++ b/tests/Parser/RelationshipDataTest.php @@ -0,0 +1,152 @@ +createRelationshipData(RelationshipDataIsCollection::class, []); + + $this->assertTrue($data->isCollection()); + $this->assertFalse($data->isNull()); + $this->assertFalse($data->isResource()); + $this->assertFalse($data->isIdentifier()); + $this->assertMethodThrowsLogicException($data, 'getIdentifier'); + $this->assertMethodThrowsLogicException($data, 'getResource'); + } + + /** + * Test relationship data. + */ + public function testIsIdentifier(): void + { + $data = $this->createRelationshipData( + RelationshipDataIsIdentifier::class, + Mockery::mock(IdentifierInterface::class) + ); + + $this->assertFalse($data->isCollection()); + $this->assertFalse($data->isNull()); + $this->assertFalse($data->isResource()); + $this->assertTrue($data->isIdentifier()); + $this->assertMethodThrowsLogicException($data, 'getIdentifiers'); + $this->assertMethodThrowsLogicException($data, 'getResource'); + $this->assertMethodThrowsLogicException($data, 'getResources'); + } + + /** + * Test relationship data. + */ + public function testIsNull(): void + { + $data = $this->createRelationshipData( + RelationshipDataIsNull::class, + null + ); + + $this->assertFalse($data->isCollection()); + $this->assertTrue($data->isNull()); + $this->assertFalse($data->isResource()); + $this->assertFalse($data->isIdentifier()); + $this->assertMethodThrowsLogicException($data, 'getIdentifier'); + $this->assertMethodThrowsLogicException($data, 'getIdentifiers'); + $this->assertMethodThrowsLogicException($data, 'getResource'); + $this->assertMethodThrowsLogicException($data, 'getResources'); + } + + /** + * Test relationship data. + */ + public function testIsResource(): void + { + $data = $this->createRelationshipData( + RelationshipDataIsResource::class, + new stdClass() + ); + + $this->assertFalse($data->isCollection()); + $this->assertFalse($data->isNull()); + $this->assertTrue($data->isResource()); + $this->assertFalse($data->isIdentifier()); + $this->assertMethodThrowsLogicException($data, 'getIdentifier'); + $this->assertMethodThrowsLogicException($data, 'getIdentifiers'); + $this->assertMethodThrowsLogicException($data, 'getResources'); + } + + /** + * @param string $className + * @param mixed $specificParam + * + * @return RelationshipDataInterface + */ + private function createRelationshipData(string $className, $specificParam): RelationshipDataInterface + { + $factory = $this->createFactory(); + $container = $factory->createSchemaContainer([]); + $position = $factory->createPosition( + ParserInterface::ROOT_LEVEL, + ParserInterface::ROOT_PATH, + null, + null + ); + + $data = new $className($factory, $container, $position, $specificParam); + + return $data; + } + + /** + * @param RelationshipDataInterface $data + * @param string $method + * + * @return void + */ + private function assertMethodThrowsLogicException(RelationshipDataInterface $data, string $method): void + { + $this->assertTrue(method_exists($data, $method)); + + $wasThrown = false; + try { + call_user_func([$data, $method]); + } catch (LogicException $exception) { + $wasThrown = true; + } + + $this->assertTrue($wasThrown); + } +} diff --git a/tests/Sample/EncodeTest.php b/tests/Sample/EncodeTest.php index e2143e8c..fc01e176 100644 --- a/tests/Sample/EncodeTest.php +++ b/tests/Sample/EncodeTest.php @@ -1,7 +1,9 @@ -assertEquals($this->normalize($expected), $this->normalize($actual)); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -79,13 +81,14 @@ public function testIncludedObjectsExample() }, "relationships" : { "posts" : { - "data" : [ - { "type" : "posts", "id" : "321" } - ], "links" : { "some-sublink" : "http://example.com/sites/1/resource-sublink", - "external-link" : "www.example.com" - } + "external-link" : "www.example.com", + "self" : "http://example.com/sites/1/relationships/posts" + }, + "data" : [ + { "type" : "posts", "id" : "321" } + ] } }, "links" : { @@ -94,11 +97,35 @@ public function testIncludedObjectsExample() }, "included" : [ { + "type" : "posts", + "id" : "321", + "attributes" : { + "title" : "Included objects", + "body" : "Yes, it is supported" + }, + "relationships" : { + "author" : { + "data" : { "type" : "people", "id" : "123" } + }, + "comments" : { + "data" : [ + { "type" : "comments", "id" : "456" }, + { "type" : "comments", "id" : "789" } + ] + } + }, + "links" : { + "self" : "http://example.com/posts/321" + } + }, { "type" : "people", "id" : "123", "attributes" : { "first_name" : "John", "last_name" : "Dow" + }, + "links" : { + "self" : "http://example.com/people/123" } }, { "type" : "comments", @@ -110,6 +137,9 @@ public function testIncludedObjectsExample() "author" : { "data" : { "type" : "people", "id" : "123" } } + }, + "links" : { + "self" : "http://example.com/comments/456" } }, { "type" : "comments", @@ -121,31 +151,16 @@ public function testIncludedObjectsExample() "author" : { "data" : { "type" : "people", "id" : "123" } } - } - }, { - "type" : "posts", - "id" : "321", - "attributes" : { - "title" : "Included objects", - "body" : "Yes, it is supported" }, - "relationships" : { - "author" : { - "data" : { "type" : "people", "id" : "123" } - }, - "comments" : { - "data" : [ - { "type" : "comments", "id" : "456" }, - { "type" : "comments", "id" : "789" } - ] - } + "links" : { + "self" : "http://example.com/comments/789" } } ] } EOL; - $this->assertEquals($this->normalize($expected), $this->normalize($actual)); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -164,6 +179,9 @@ public function testSparseAndFieldSetsExample() }, "relationships" : { "posts" : { + "links": { + "self" : "/sites/1/relationships/posts" + }, "data" : [ { "type" : "posts", "id" : "321" } ] @@ -175,25 +193,31 @@ public function testSparseAndFieldSetsExample() }, "included":[ { - "type" : "people", - "id" : "123", - "attributes" : { - "first_name" : "John" - } - }, { "type" : "posts", "id" : "321", "relationships" : { "author" : { "data" : { "type" : "people", "id" : "123" } } + }, + "links" : { + "self" : "/posts/321" + } + }, { + "type" : "people", + "id" : "123", + "attributes" : { + "first_name" : "John" + }, + "links" : { + "self" : "/people/123" } } ] } EOL; - $this->assertEquals($this->normalize($expected), $this->normalize($actual)); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -232,7 +256,7 @@ public function testTopLevelMetaAndLinksExample() } EOL; - $this->assertEquals($this->normalize($expected), $this->normalize($actual)); + self::assertJsonStringEqualsJsonString($expected, $actual); } /** @@ -251,6 +275,9 @@ public function testDynamicSchemaExample() }, "relationships" : { "posts" : { + "links": { + "self" : "/sites/1/relationships/posts" + }, "data" : [] } }, @@ -261,7 +288,7 @@ public function testDynamicSchemaExample() } EOL; - $this->assertEquals($this->normalize($expected), $this->normalize($actual[0])); + self::assertJsonStringEqualsJsonString($expected, $actual[0]); $expected = <<assertEquals($this->normalize($expected), $this->normalize($actual[1])); + self::assertJsonStringEqualsJsonString($expected, $actual[1]); } /** @@ -295,7 +323,7 @@ public function testDynamicSchemaExample() */ public function testPerformanceTestForSmallNestedResources() { - $this->assertGreaterThan(0, $this->samples->runPerformanceTestForSmallNestedResources(10)[0]); + self::assertGreaterThan(0, $this->samples->runPerformanceTestForSmallNestedResources(10)[0]); } /** @@ -303,16 +331,6 @@ public function testPerformanceTestForSmallNestedResources() */ public function testPerformanceTestForBigCollection() { - $this->assertGreaterThan(0, $this->samples->runPerformanceTestForBigCollection(10)[0]); - } - - /** - * @param string $json - * - * @return string - */ - private function normalize($json) - { - return json_encode(json_decode($json)); + self::assertGreaterThan(0, $this->samples->runPerformanceTestForBigCollection(10)[0]); } } diff --git a/tests/Schema/ContainerTest.php b/tests/Schema/ContainerTest.php deleted file mode 100644 index fc71bbd2..00000000 --- a/tests/Schema/ContainerTest.php +++ /dev/null @@ -1,179 +0,0 @@ -factory = new Factory(); - } - - /** - * Test container. - */ - public function testGetSchemaByType() - { - $this->assertNotNull($container = $this->factory->createContainer([ - Author::class => AuthorSchema::class, - ])); - - $this->assertNotNull($container->getSchemaByType(Author::class)); - - $gotException = false; - try { - $container->getSchemaByType(Post::class); - } catch (InvalidArgumentException $exception) { - $gotException = true; - } - - $this->assertTrue($gotException); - } - - /** - * Test container. - */ - public function testGetSchemaByResourceType() - { - $this->assertNotNull($container = $this->factory->createContainer([ - Author::class => AuthorSchema::class, - ])); - - $this->assertNotNull($container->getSchemaByResourceType('people')); - - $gotException = false; - try { - $container->getSchemaByResourceType('posts'); - } catch (InvalidArgumentException $exception) { - $gotException = true; - } - - $this->assertTrue($gotException); - } - - /** - * Test container. - * - * @expectedException \InvalidArgumentException - */ - public function testRegisterInvalidSchemaMapping1() - { - $this->factory->createContainer([ - '' => AuthorSchema::class, - ]); - } - - /** - * Test container. - * - * @expectedException \InvalidArgumentException - */ - public function testRegisterInvalidSchemaMapping2() - { - $this->factory->createContainer([ - Author::class => '', - ]); - } - - /** - * Test container. - * - * @expectedException \InvalidArgumentException - */ - public function testRegisterInvalidSchemaMapping3() - { - $container = new Container($this->factory, [ - Author::class => AuthorSchema::class, - ]); - - $container->register(Author::class, AuthorSchema::class); - } - - /** - * Test container. - * - * @link https://github.com/neomerx/json-api/issues/168 - */ - public function testRegisterSchemaInstance() - { - $authorSchema = new AuthorSchema($this->factory); - $container = new Container($this->factory, [ - Author::class => $authorSchema, - ]); - - $this->assertSame($authorSchema, $container->getSchema(new Author())); - } - - /** - * Test container. - * - * @link https://github.com/neomerx/json-api/issues/177 - */ - public function testRegisterCallableSchemaFactory() - { - $container = new Container($this->factory, [ - Author::class => [static::class, 'authorSchemaFactory'], - ]); - - $this->assertNotNull($container->getSchema(new Author())); - } - - /** - * Test container. - * - * @link https://github.com/neomerx/json-api/issues/188 - */ - public function testForNullResourceShouldReturnNull() - { - $container = new Container($this->factory, [ - Author::class => [static::class, 'authorSchemaFactory'], - ]); - - $this->assertNull($container->getSchema(null)); - } - - /** - * @param SchemaFactoryInterface $factory - * - * @return AuthorSchema - */ - public static function authorSchemaFactory(SchemaFactoryInterface $factory) - { - return new AuthorSchema($factory); - } -} diff --git a/tests/Schema/EmptySchema.php b/tests/Schema/EmptySchema.php deleted file mode 100644 index 8586a149..00000000 --- a/tests/Schema/EmptySchema.php +++ /dev/null @@ -1,64 +0,0 @@ -selfSubUrl = static::$subUrl; - $this->resourceType = static::$type; - - parent::__construct($factory); - } - - /** - * @inheritdoc - */ - public function getId($resource): ?string - { - throw new LogicException(); - } - - /** - * @inheritdoc - */ - public function getAttributes($resource, array $fieldKeysFilter = null): ?array - { - throw new LogicException(); - } -} diff --git a/tests/Exceptions/ErrorCollectionTest.php b/tests/Schema/ErrorCollectionTest.php similarity index 76% rename from tests/Exceptions/ErrorCollectionTest.php rename to tests/Schema/ErrorCollectionTest.php index c3be4ffd..93532e8b 100644 --- a/tests/Exceptions/ErrorCollectionTest.php +++ b/tests/Schema/ErrorCollectionTest.php @@ -1,7 +1,9 @@ -collection = new ErrorCollection(); } - public function testBasicCollectionMethods() + public function testBasicCollectionMethods(): void { $this->assertCount(0, $this->collection); $title1 = 'some title 1'; @@ -79,7 +81,7 @@ public function testBasicCollectionMethods() $this->assertEquals($title2, $this->collection[0]->getTitle()); foreach ($this->collection as $error) { - $this->assertInstanceOf(Error::class, $error); + $this->assertInstanceOf(ErrorInterface::class, $error); } $serialized = $this->collection->serialize(); @@ -96,120 +98,120 @@ public function testBasicCollectionMethods() /** * Test adding error. */ - public function testAddDataError() + public function testAddDataError(): void { $this->collection->addDataError('some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::DATA_PATH + ErrorInterface::SOURCE_POINTER => self::DATA_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddDataTypeError() + public function testAddDataTypeError(): void { $this->collection->addDataTypeError('some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::DATA_TYPE_PATH + ErrorInterface::SOURCE_POINTER => self::DATA_TYPE_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddDataIdError() + public function testAddDataIdError(): void { $this->collection->addDataIdError('some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::DATA_ID_PATH + ErrorInterface::SOURCE_POINTER => self::DATA_ID_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddAttributesError() + public function testAddAttributesError(): void { $this->collection->addAttributesError('some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::ATTR_PATH + ErrorInterface::SOURCE_POINTER => self::ATTR_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddDataAttributeError() + public function testAddDataAttributeError(): void { $this->collection->addDataAttributeError('name', 'some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::ATTR_PATH . '/name' + ErrorInterface::SOURCE_POINTER => self::ATTR_PATH . '/name' ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddRelationshipsError() + public function testAddRelationshipsError(): void { $this->collection->addRelationshipsError('some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::RELS_PATH + ErrorInterface::SOURCE_POINTER => self::RELS_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddRelationshipError() + public function testAddRelationshipError(): void { $this->collection->addRelationshipError('name', 'some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::RELS_PATH . '/name' + ErrorInterface::SOURCE_POINTER => self::RELS_PATH . '/name' ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddRelationshipTypeError() + public function testAddRelationshipTypeError(): void { $this->collection->addRelationshipTypeError('name', 'some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::RELS_PATH . '/name' . self::DATA_TYPE_PATH + ErrorInterface::SOURCE_POINTER => self::RELS_PATH . '/name' . self::DATA_TYPE_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddRelationshipIdError() + public function testAddRelationshipIdError(): void { $this->collection->addRelationshipIdError('name', 'some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_POINTER => self::RELS_PATH . '/name' . self::DATA_ID_PATH + ErrorInterface::SOURCE_POINTER => self::RELS_PATH . '/name' . self::DATA_ID_PATH ], $this->collection[0]->getSource()); } /** * Test adding error. */ - public function testAddQueryParameterError() + public function testAddQueryParameterError(): void { $this->collection->addQueryParameterError('name', 'some title'); $this->assertNotEmpty($this->collection); $this->assertEquals([ - Error::SOURCE_PARAMETER => 'name' + ErrorInterface::SOURCE_PARAMETER => 'name' ], $this->collection[0]->getSource()); } } diff --git a/tests/Schema/FactoryTest.php b/tests/Schema/FactoryTest.php deleted file mode 100644 index 48a3404d..00000000 --- a/tests/Schema/FactoryTest.php +++ /dev/null @@ -1,102 +0,0 @@ -factory = new Factory(); - } - - /** - * Test create schema provider container. - */ - public function testCreateContainer() - { - $this->assertNotNull($this->factory->createContainer()); - } - - /** - * Test create resource object. - */ - public function testCreateResourceObject() - { - $linkMock = Mockery::mock(LinkInterface::class); - - /** @var Mock $schema */ - $schema = Mockery::mock(SchemaInterface::class); - $schema->shouldReceive('getResourceType')->once()->andReturn('some-type'); - $schema->shouldReceive('getSelfSubLink')->once()->andReturn($linkMock); - - /** @var SchemaInterface $schema */ - - $this->assertNotNull($resource = $this->factory->createResourceObject( - $schema, - $resource = new stdClass(), - $isInArray = false, - $attributeKeysFilter = ['field1', 'field2'] - )); - - $this->assertEquals($isInArray, $resource->isInArray()); - $this->assertSame('some-type', $resource->getType()); - $this->assertSame($linkMock, $resource->getSelfSubLink()); - } - - /** - * Test create relationship object. - */ - public function testCreateRelationshipObject() - { - $this->assertNotNull($relationship = $this->factory->createRelationshipObject( - $name = 'link-name', - $data = new stdClass(), - $links = [LinkInterface::SELF => $this->factory->createLink('selfSubUrl')], - $meta = ['some' => 'meta'], - $isShowData = true, - $isRoot = false - )); - - $this->assertEquals($name, $relationship->getName()); - $this->assertEquals($data, $relationship->getData()); - $this->assertEquals($links, $relationship->getLinks()); - $this->assertEquals($meta, $relationship->getMeta()); - $this->assertEquals($isShowData, $relationship->isShowData()); - $this->assertEquals($isRoot, $relationship->isRoot()); - } -} diff --git a/tests/Schema/IdentitySchemaTest.php b/tests/Schema/IdentitySchemaTest.php deleted file mode 100644 index fff6c900..00000000 --- a/tests/Schema/IdentitySchemaTest.php +++ /dev/null @@ -1,81 +0,0 @@ -shouldReceive('getResourceType')->once()->withAnyArgs()->andReturn('fake-type'); - $providerMock->shouldReceive('getSelfSubUrl')->once()->withAnyArgs()->andReturn('/fake-sub-url'); - - /** @var SchemaInterface $providerMock */ - - /** @var SchemaFactoryInterface $factory */ - $factory = Mockery::mock(SchemaFactoryInterface::class); - - /** @var Mock $container */ - $container = Mockery::mock(ContainerInterface::class); - $container->shouldReceive('getSchemaByType')->once()->withAnyArgs()->andReturn($providerMock); - - /** @var ContainerInterface $container */ - - $this->schema = new IdentitySchema($factory, $container, 'fake class name', function () { - throw new LogicException(); - }); - } - - /** - * @expectedException \LogicException - */ - public function testGetAttributes() - { - $this->schema->getAttributes((object)[]); - } - - /** - * @expectedException \LogicException - */ - public function testGetRelationships() - { - $this->schema->getRelationships((object)[], true, []); - } -} diff --git a/tests/Schema/ResourceIdentifierContainerAdapterTest.php b/tests/Schema/ResourceIdentifierContainerAdapterTest.php deleted file mode 100644 index 8a8c27e2..00000000 --- a/tests/Schema/ResourceIdentifierContainerAdapterTest.php +++ /dev/null @@ -1,57 +0,0 @@ -shouldReceive('getSchema')->once()->withAnyArgs()->andReturn($provider); - $container->shouldReceive('getSchemaByType')->once()->withAnyArgs()->andReturn($provider); - $container->shouldReceive('getSchemaByResourceType')->once()->withAnyArgs()->andReturn($provider); - $factory->shouldReceive('createResourceIdentifierSchemaAdapter')->times(3)->withAnyArgs() - ->andReturn($provider); - - /** @var FactoryInterface $factory */ - /** @var ContainerInterface $container */ - - $adapter = new ResourceIdentifierContainerAdapter($factory, $container); - - $resource = (object)['whatever']; - $this->assertNotNull($adapter->getSchema($resource)); - $this->assertNotNull($adapter->getSchemaByType('does not matter 1')); - $this->assertNotNull($adapter->getSchemaByResourceType('does not matter2')); - } -} diff --git a/tests/Schema/ResourceIdentifierSchemaAdapterTest.php b/tests/Schema/ResourceIdentifierSchemaAdapterTest.php deleted file mode 100644 index 5d768b3c..00000000 --- a/tests/Schema/ResourceIdentifierSchemaAdapterTest.php +++ /dev/null @@ -1,75 +0,0 @@ -shouldReceive('getSelfSubLink')->once()->withAnyArgs()->andReturn($linkMock1); - $schema->shouldReceive('getSelfSubUrl')->once()->withAnyArgs()->andReturn($linkMock2); - $schema->shouldReceive('getLinkageMeta')->once()->withAnyArgs()->andReturn(['some' => 'meta']); - $schema->shouldReceive('getPrimaryMeta')->once()->withAnyArgs()->andReturn(['some' => 'meta']); - $schema->shouldReceive('getRelationshipSelfLink')->once()->withAnyArgs()->andReturn($linkMock3); - $schema->shouldReceive('getRelationshipRelatedLink')->once()->withAnyArgs()->andReturn($linkMock4); - - /** @var FactoryInterface $factory */ - /** @var ContainerInterface $container */ - /** @var SchemaInterface $schema */ - - $adapter = new ResourceIdentifierSchemaAdapter($factory, $schema); - - $resource = (object)['whatever']; - $adapter->getSelfSubLink($resource); - $adapter->getSelfSubUrl(); - $this->assertEmpty($adapter->getResourceLinks($resource)); - $this->assertEmpty($adapter->getIncludedResourceLinks($resource)); - $this->assertEmpty($adapter->getAttributes($resource)); - $this->assertEmpty($adapter->getRelationships($resource, true, [])); - $this->assertEmpty($adapter->getIncludePaths()); - $this->assertNotEmpty($adapter->getPrimaryMeta($resource)); - $this->assertFalse($adapter->isShowAttributesInIncluded()); - $this->assertNull($adapter->getInclusionMeta($resource)); - $this->assertNotNull($adapter->getRelationshipObjectIterator($resource, true, [])); - $this->assertNull($adapter->getRelationshipsPrimaryMeta($resource)); - $this->assertNull($adapter->getRelationshipsInclusionMeta($resource)); - $this->assertNotNull($adapter->getRelationshipSelfLink($resource, 'relationship')); - $this->assertNotNull($adapter->getRelationshipRelatedLink($resource, 'relationship')); - $adapter->getLinkageMeta($resource); - } -} diff --git a/tests/Schema/SchemaContainerTest.php b/tests/Schema/SchemaContainerTest.php new file mode 100644 index 00000000..f31c0c18 --- /dev/null +++ b/tests/Schema/SchemaContainerTest.php @@ -0,0 +1,139 @@ +createFactory(); + $commentSchema = new CommentSchema($factory); + $postSchema = new PostSchema($factory); + $container = $factory->createSchemaContainer([ + Author::class => AuthorSchema::class, + Comment::class => $commentSchema, + Post::class => function () use ($postSchema): SchemaInterface { + return $postSchema; + }, + ]); + + $author = $this->createAuthor(); + $comment = $this->createComment(); + $post = $this->createPost(); + + self::assertTrue($container->hasSchema($author)); + self::assertNotNull($container->getSchema($author)); + self::assertTrue($container->hasSchema($comment)); + self::assertSame($commentSchema, $container->getSchema($comment)); + self::assertTrue($container->hasSchema($post)); + self::assertSame($postSchema, $container->getSchema($post)); + } + + /** + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException + */ + public function testInvalidModelClass(): void + { + $notExistingClass = self::class . 'xxx'; + + $this->createFactory()->createSchemaContainer([$notExistingClass => AuthorSchema::class]); + } + + /** + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException + */ + public function testInvalidSchemaClass(): void + { + $notSchemaClass = self::class; + + $this->createFactory()->createSchemaContainer([Author::class => $notSchemaClass]); + } + + /** + * @expectedException \Neomerx\JsonApi\Exceptions\InvalidArgumentException + */ + public function testModelCannotHaveTwoSchemas(): void + { + $container = $this->createFactory()->createSchemaContainer([Author::class => AuthorSchema::class]); + + assert($container instanceof SchemaContainer); + + $container->register(Author::class, CommentSchema::class); + } + + /** + * @expectedException \Neomerx\JsonApi\Exceptions\LogicException + */ + public function testDefaultSchemaDoNotProvideIdentifierMeta(): void + { + $schema = new CommentSchema($this->createFactory()); + + $schema->getIdentifierMeta($this->createComment()); + } + + /** + * @expectedException \Neomerx\JsonApi\Exceptions\LogicException + */ + public function testDefaultSchemaDoNotProvideResourceMeta(): void + { + $schema = new CommentSchema($this->createFactory()); + + $schema->getResourceMeta($this->createComment()); + } + + /** + * @return Author + */ + private function createAuthor(): Author + { + return Author::instance(1, 'FirstName', 'LastName'); + } + + /** + * @return Comment + */ + private function createComment(): Comment + { + return Comment::instance(321, 'Comment body'); + } + + /** + * @return Post + */ + private function createPost(): Post + { + return Post::instance(321, 'Post Title', 'Post body'); + } +} diff --git a/tests/Schema/SchemaProviderTest.php b/tests/Schema/SchemaProviderTest.php deleted file mode 100644 index a6b85987..00000000 --- a/tests/Schema/SchemaProviderTest.php +++ /dev/null @@ -1,54 +0,0 @@ -factory = new Factory(); - } - - /** - * Test schema provider. - */ - public function testNoTrailingSlashInGetSelfSubUrl() - { - EmptySchema::$type = 'some-type'; - EmptySchema::$subUrl = null; - - $schema = new EmptySchema($this->factory); - - $this->assertEquals('/some-type', $schema->getSelfSubUrl()); - } -}