diff --git a/.codeclimate.yml b/.codeclimate.yml index 7205c36..00cf4c1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -2,9 +2,7 @@ engines: phpmd: enabled: true checks: - Controversial/CamelCasePropertyName: - enabled: false Naming/ShortVariable: enabled: false CleanCode/StaticAccess: - enabled: false \ No newline at end of file + enabled: false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..66a983e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: "https://www.paypal.me/f3ath" + diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..eae93ab --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,36 @@ +name: PHP Composer + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run test suite + run: composer run-script test diff --git a/.gitignore b/.gitignore index d136198..c195581 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ composer.phar +/doc-test/ /vendor/ /composer.lock /.php_cs.cache +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..acd1414 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +exclude('somedir') + ->notPath('src/Symfony/Component/Translation/Tests/fixtures/resources.php') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +return $config->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'braces' => [ + 'allow_single_line_closure' => true, + 'position_after_functions_and_oop_constructs' => 'same'], +]) + ->setFinder($finder); \ No newline at end of file diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index 9c1b5d4..0000000 --- a/.php_cs.dist +++ /dev/null @@ -1,51 +0,0 @@ -files() - ->name('*.php') - ->in(__DIR__ . '/src') - ->in(__DIR__ . '/test'); -return PhpCsFixer\Config::create() - ->setUsingCache(true) - ->setRiskyAllowed(true) - ->setRules([ - '@PSR2' => true, - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => true, - 'cast_spaces' => true, - 'concat_space' => true, - 'declare_strict_types' => true, - 'include' => true, - 'is_null' => true, - 'lowercase_cast' => true, - 'mb_str_functions' => true, - 'method_separation' => true, - 'native_function_casing' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_unused_imports' => true, - 'no_whitespace_in_blank_line' => true, - 'object_operator_without_whitespace' => true, - 'ordered_imports' => true, - 'phpdoc_align' => true, - 'phpdoc_indent' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_package' => true, - 'phpdoc_order' => true, - 'phpdoc_scalar' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'psr0' => true, - 'short_scalar_cast' => true, - 'single_blank_line_before_namespace' => true, - 'standardize_not_equals' => true, - 'strict_comparison' => true, - 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline_array' => true, - 'whitespace_after_comma_in_array' => true, - ]) - ->setFinder($finder); diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8fa35d0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: php -php: - - '7.0' - - '7.1' - -before_script: - - composer install - -script: - - vendor/bin/php-cs-fixer fix -v --dry-run - - phpunit --coverage-clover build/logs/clover.xml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..22fd9fe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.0.0] - 2022-07-26 +### Changed +- The package is migrated to PHP 8.1 + +### Removed +- Support for PHP 7 and older versions +- Compound document validation logic is dropped + +## [2.2.0] - 2020-10-12 +### Added +- `NewResourceObject` to allow omitting `id` in resources to-be-created (#108) + +## [2.1.2] - 2020-03-16 +### Fixed +- Related links must be allowed inside relationship documents (#104) + +## [2.1.1] - 2019-12-19 +### Fixed +- `ResourceIdentifier` does not allow multiple meta members (#99) + +## [2.1.0] - 2019-02-25 +### Fixed +- Relationship without data property (#92) + +## [2.0.1] - 2018-12-31 +### Changed +- Downgraded min required php version to 7.1 + +## 2.0.0 - 2018-02-26 +### Added +- v2 initial release + +[Unreleased]: https://github.com/json-api-php/json-api/compare/3.0.0...HEAD +[3.0.0]: https://github.com/json-api-php/json-api/compare/2.2.2...3.0.0 +[2.2.0]: https://github.com/json-api-php/json-api/compare/2.1.2...2.2.0 +[2.1.2]: https://github.com/json-api-php/json-api/compare/2.1.1...2.1.2 +[2.1.1]: https://github.com/json-api-php/json-api/compare/2.1.0...2.1.1 +[2.1.0]: https://github.com/json-api-php/json-api/compare/2.0.1...2.1.0 +[2.0.1]: https://github.com/json-api-php/json-api/compare/2.0.0...2.0.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e232deb..761757a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,5 @@ Thank you for you interest! Here are some key guidelines. ## PSR We want to follow the latest actual [PSR](http://www.php-fig.org/psr/) standards. -## Tests are documentation -The use cases and library's API must be expressed in a form of tests. We use PHPUnit, but it does not mean all tests -are expected to be unit tests. No logic must be written without a supporting test. +## Tests are the documentation +The use cases and library's API must be expressed as tests. No logic must be written without a supporting test. diff --git a/LICENSE b/LICENSE index c10e57a..c581e9c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Alexey +Copyright (c) Alexey Karapetov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4e43a2e..2272090 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -# Implementation of [JSON API](http://jsonapi.org) in PHP 7 -This library is an attempt to express business rules of JSON API specification in a set of PHP 7 classes. +# [JSON API](http://jsonapi.org) spec implemented in PHP 7. Immutable -A simple example to illustrate the general idea. This (slightly modified) JSON representation from -[the documentation](http://jsonapi.org/format/#document-resource-objects) +The goal of this library is to ensure strict validity of JSON API documents being produced. +JSON: ```json { "data": { @@ -19,37 +18,63 @@ A simple example to illustrate the general idea. This (slightly modified) JSON r "id": "9" }, "links": { - "self": "\/articles\/1\/relationships\/author", - "related": "\/articles\/1\/author" + "self": "/articles/1/relationships/author", + "related": "/articles/1/author" } } } } } ``` -can be built with the following php code: +PHP: ```php -$articles = new ResourceObject('articles', '1'); -$author = Relationship::fromLinkage( - Linkage::fromSingleIdentifier( - new ResourceIdentifier('people', '9') - ) +setLink('self', '/articles/1/relationships/author'); -$author->setLink('related', '/articles/1/author'); -$articles->setRelationship('author', $author); -$articles->setAttribute('title', 'Rails is Omakase'); -$doc = Document::fromResource($articles); -echo json_encode($doc, JSON_PRETTY_PRINT); ``` +## Installation +`composer require json-api-php/json-api` -Please refer to [the tests](./test) for the full API documentation: -* [Documents](./test/Document/DocumentTest.php) - * [Compound Documents](./test/Document/CompoundDocumentTest.php) -* [Errors](./test/Document/ErrorTest.php) -* [Resources](./test/Document/Resource/ResourceTest.php) -* [Relationships](./test/Document/Resource/Relationship/RelationshipTest.php) -* [Linkage](./test/Document/Resource/Relationship/LinkageTest.php) +## Documentation -## Installation -With [composer](https://getcomposer.org/): `json-api-php/json-api`. +First, take a look at the examples. All of them are runnable. +- [Simple Document](./examples/simple_doc.php) (the same as above) +- [Extensive Compound Document](./examples/compound_doc.php) + +The library API and use-cases are expressed in a comprehensive suite of tests. +- Data Documents (containing primary data) + - [with a single Resource Object](./test/DataDocument/SingleResourceObjectTest.php) + - [with a single Resource Identifier](./test/DataDocument/SingleResourceIdentifierTest.php) + - [with null data](./test/DataDocument/NullDataTest.php) + - [with multiple Resource Objects](./test/DataDocument/ManyResourceObjectsTest.php) + - [with multiple Resource Identifiers](./test/DataDocument/ManyResourceIdentifiersTest.php) + - [with a new Resource (no id)](./test/NewResourceObjectTest.php) +- [Compound Documents](./test/CompoundDocumentTest.php) +- [Error Documents](./test/ErrorDocumentTest.php) +- [Meta Documents (containing neither data nor errors)](./test/MetaDocumentTest.php) +- [Pagination](./test/PaginationTest.php) +- [Link Objects](./test/LinkObjectTest.php) +- [JSON API Object](./test/JsonApiTest.php) +- [Meta Objects](./test/MetaTest.php) diff --git a/composer.json b/composer.json index 8ce2d74..5a7e2b9 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "json-api-php/json-api", - "description": "An attempt to express JSON API specs (jsonapi.org) in object-oriented way as a set of PHP 7 classes", + "description": "JSON API specs (jsonapi.org) as a set of PHP classes", "type": "library", - + "prefer-stable": true, "license": "MIT", "authors": [ { @@ -11,16 +11,20 @@ } ], "require": { - "php": ">=7.0" + "php": ">=8.1", + "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^6.0", - "friendsofphp/php-cs-fixer": "^2.2" + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "^3.9" }, "autoload": { "psr-4": { "JsonApiPhp\\JsonApi\\": "src/" - } + }, + "files": [ + "src/functions.php" + ] }, "autoload-dev": { "psr-4": { @@ -28,6 +32,6 @@ } }, "scripts": { - "test": "php-cs-fixer fix -v --dry-run --ansi && phpunit --colors=always" + "test": "php-cs-fixer fix -v --dry-run --ansi && phpunit --colors=always --coverage-text" } } diff --git a/examples/compound_doc.php b/examples/compound_doc.php new file mode 100644 index 0000000..1d0cbbb --- /dev/null +++ b/examples/compound_doc.php @@ -0,0 +1,82 @@ +identifier()) +); + +$document = new CompoundDocument( + new PaginatedCollection( + new Pagination( + new NextLink('http://example.com/articles?page[offset]=2'), + new LastLink('http://example.com/articles?page[offset]=10') + ), + new ResourceCollection( + new ResourceObject( + 'articles', + '1', + new Attribute('title', 'JSON API paints my bikeshed!'), + new SelfLink('http://example.com/articles/1'), + new ToOne( + 'author', + $dan->identifier(), + new SelfLink('http://example.com/articles/1/relationships/author'), + new RelatedLink('http://example.com/articles/1/author') + ), + new ToMany( + 'comments', + new ResourceIdentifierCollection( + $comment05->identifier(), + $comment12->identifier() + ), + new SelfLink('http://example.com/articles/1/relationships/comments'), + new RelatedLink('http://example.com/articles/1/comments') + ) + ) + ) + ), + new Included($dan, $comment05, $comment12), + new SelfLink('http://example.com/articles') +); + +echo json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); diff --git a/examples/simple_doc.php b/examples/simple_doc.php new file mode 100644 index 0000000..eb7c5f4 --- /dev/null +++ b/examples/simple_doc.php @@ -0,0 +1,29 @@ + - - - - - - ./test - - - - - ./src/ - - + + + + + ./src/ + + + + + ./test + + diff --git a/src/Attribute.php b/src/Attribute.php new file mode 100644 index 0000000..8276746 --- /dev/null +++ b/src/Attribute.php @@ -0,0 +1,31 @@ +validateFieldName($name); + $this->name = $name; + $this->val = $val; + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + child($o, 'attributes')->{$this->name} = $this->val; + } +} diff --git a/src/CompoundDocument.php b/src/CompoundDocument.php new file mode 100644 index 0000000..7a15492 --- /dev/null +++ b/src/CompoundDocument.php @@ -0,0 +1,25 @@ +doc = combine($data, $included, ...$members); + } + + public function jsonSerialize(): object { + return $this->doc; + } +} diff --git a/src/DataDocument.php b/src/DataDocument.php new file mode 100644 index 0000000..20d0e7c --- /dev/null +++ b/src/DataDocument.php @@ -0,0 +1,25 @@ +value = combine($data, ...$members); + } + + public function jsonSerialize(): object { + return $this->value; + } +} diff --git a/src/Document.php b/src/Document.php deleted file mode 100644 index fdec9ce..0000000 --- a/src/Document.php +++ /dev/null @@ -1,150 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi; - -use JsonApiPhp\JsonApi\Document\Error; -use JsonApiPhp\JsonApi\Document\LinksTrait; -use JsonApiPhp\JsonApi\Document\Meta; -use JsonApiPhp\JsonApi\Document\MetaTrait; -use JsonApiPhp\JsonApi\Document\Resource\ResourceInterface; -use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; - -class Document implements \JsonSerializable -{ - const MEDIA_TYPE = 'application/vnd.api+json'; - const DEFAULT_API_VERSION = '1.0'; - - use LinksTrait; - use MetaTrait; - - private $data; - private $errors; - private $api; - private $included; - private $is_sparse = false; - - private function __construct() - { - } - - public static function fromMeta(Meta $meta): self - { - $doc = new self; - $doc->setMeta($meta); - return $doc; - } - - public static function fromErrors(Error ...$errors): self - { - $doc = new self; - $doc->errors = $errors; - return $doc; - } - - public static function fromResource(ResourceInterface $data): self - { - $doc = new self; - $doc->data = $data; - return $doc; - } - - public static function fromResources(ResourceInterface ...$data): self - { - $doc = new self; - $doc->data = $data; - return $doc; - } - - public function setApiVersion(string $version = self::DEFAULT_API_VERSION) - { - $this->api['version'] = $version; - } - - public function setApiMeta(array $meta) - { - $this->api['meta'] = $meta; - } - - public function setIncluded(ResourceObject ...$included) - { - $this->included = $included; - } - - public function markSparse() - { - $this->is_sparse = true; - } - - public function jsonSerialize() - { - $this->enforceFullLinkage(); - return array_filter( - [ - 'data' => $this->data, - 'errors' => $this->errors, - 'meta' => $this->meta, - 'jsonapi' => $this->api, - 'links' => $this->links, - 'included' => $this->included, - ], - function ($v) { - return null !== $v; - } - ); - } - - private function enforceFullLinkage() - { - if ($this->is_sparse || empty($this->included)) { - return; - } - foreach ($this->included as $included_resource) { - if ($this->hasLinkTo($included_resource) || $this->anotherIncludedResourceIdentifies($included_resource)) { - continue; - } - throw new \LogicException("Full linkage is required for $included_resource"); - } - } - - private function anotherIncludedResourceIdentifies(ResourceObject $resource): bool - { - /** @var ResourceObject $included_resource */ - foreach ($this->included as $included_resource) { - if ($included_resource !== $resource && $included_resource->identifies($resource)) { - return true; - } - } - return false; - } - - private function hasLinkTo(ResourceObject $resource): bool - { - /** @var ResourceInterface $my_resource */ - foreach ($this->toResources() as $my_resource) { - if ($my_resource->identifies($resource)) { - return true; - } - } - return false; - } - - private function toResources(): \Iterator - { - if ($this->data instanceof ResourceInterface) { - yield $this->data; - } elseif (is_array($this->data)) { - foreach ($this->data as $datum) { - yield $datum; - } - } - } -} diff --git a/src/Document/ArrayMeta.php b/src/Document/ArrayMeta.php deleted file mode 100644 index 91473a4..0000000 --- a/src/Document/ArrayMeta.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document; - -class ArrayMeta extends Meta -{ - public function __construct(array $array) - { - parent::__construct((object) $array); - } -} diff --git a/src/Document/Error.php b/src/Document/Error.php deleted file mode 100644 index 8537ee7..0000000 --- a/src/Document/Error.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document; - -final class Error implements \JsonSerializable -{ - use MetaTrait; - - private $id; - private $links; - private $status; - private $code; - private $title; - private $detail; - private $source; - - public function setId(string $id) - { - $this->id = $id; - } - - public function setAboutLink(string $link) - { - $this->links['about'] = $link; - } - - public function setStatus(string $status) - { - $this->status = $status; - } - - public function setCode(string $code) - { - $this->code = $code; - } - - public function setTitle(string $title) - { - $this->title = $title; - } - - public function setDetail(string $detail) - { - $this->detail = $detail; - } - - public function setSourcePointer(string $pointer) - { - $this->source['pointer'] = $pointer; - } - - public function setSourceParameter(string $parameter) - { - $this->source['parameter'] = $parameter; - } - - public function jsonSerialize() - { - return array_filter( - [ - 'id' => $this->id, - 'links' => $this->links, - 'status' => $this->status, - 'code' => $this->code, - 'title' => $this->title, - 'detail' => $this->detail, - 'source' => $this->source, - 'meta' => $this->meta, - ], - function ($v) { - return null !== $v; - } - ) ?: (object) []; - } -} diff --git a/src/Document/LinksTrait.php b/src/Document/LinksTrait.php deleted file mode 100644 index de2bf3a..0000000 --- a/src/Document/LinksTrait.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document; - -trait LinksTrait -{ - protected $links; - - public function setLink(string $name, string $value, array $meta = null) - { - $this->links[$name] = $meta ? ['href' => $value, 'meta' => $meta] : $value; - } -} diff --git a/src/Document/Meta.php b/src/Document/Meta.php deleted file mode 100644 index f4d060a..0000000 --- a/src/Document/Meta.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document; - -class Meta implements \JsonSerializable -{ - private $data; - - public function __construct(\stdClass $data) - { - $this->data = $data; - } - - public function jsonSerialize() - { - return $this->data; - } -} diff --git a/src/Document/MetaTrait.php b/src/Document/MetaTrait.php deleted file mode 100644 index 9379a12..0000000 --- a/src/Document/MetaTrait.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document; - -trait MetaTrait -{ - protected $meta; - - public function setMeta(Meta $meta) - { - $this->meta = $meta; - } -} diff --git a/src/Document/Resource/NullResource.php b/src/Document/Resource/NullResource.php deleted file mode 100644 index 8a92014..0000000 --- a/src/Document/Resource/NullResource.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource; - -final class NullResource implements ResourceInterface -{ - public function jsonSerialize() - { - return null; - } - - public function identifies(ResourceInterface $resource): bool - { - return false; - } - - public function __toString(): string - { - return 'null'; - } -} diff --git a/src/Document/Resource/Relationship/Linkage.php b/src/Document/Resource/Relationship/Linkage.php deleted file mode 100644 index 54c5b76..0000000 --- a/src/Document/Resource/Relationship/Linkage.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource\Relationship; - -use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; -use JsonApiPhp\JsonApi\Document\Resource\ResourceInterface; - -final class Linkage implements \JsonSerializable -{ - private $data; - - private function __construct() - { - } - - public static function nullLinkage(): self - { - return new self; - } - - public static function emptyArrayLinkage(): self - { - $linkage = new self; - $linkage->data = []; - return $linkage; - } - - public static function fromSingleIdentifier(ResourceIdentifier $data): self - { - $linkage = new self; - $linkage->data = $data; - return $linkage; - } - - public static function fromManyIdentifiers(ResourceIdentifier ...$data): self - { - $linkage = new self; - $linkage->data = $data; - return $linkage; - } - - public function isLinkedTo(ResourceInterface $resource): bool - { - foreach ($this->toLinkages() as $linkage) { - if ($linkage->identifies($resource)) { - return true; - } - } - return false; - } - - private function toLinkages(): \Generator - { - if ($this->data instanceof ResourceIdentifier) { - yield $this->data; - } elseif (is_array($this->data)) { - foreach ($this->data as $resource) { - yield $resource; - } - } - } - - public function jsonSerialize() - { - return $this->data; - } -} diff --git a/src/Document/Resource/Relationship/Relationship.php b/src/Document/Resource/Relationship/Relationship.php deleted file mode 100644 index 84b47e6..0000000 --- a/src/Document/Resource/Relationship/Relationship.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource\Relationship; - -use JsonApiPhp\JsonApi\Document\LinksTrait; -use JsonApiPhp\JsonApi\Document\Meta; -use JsonApiPhp\JsonApi\Document\MetaTrait; -use JsonApiPhp\JsonApi\Document\Resource\ResourceInterface; - -final class Relationship implements \JsonSerializable -{ - use LinksTrait; - use MetaTrait; - - /** - * @var Linkage - */ - private $linkage = null; - - private function __construct() - { - } - - public static function fromMeta(Meta $meta): self - { - $r = new self; - $r->setMeta($meta); - return $r; - } - - public static function fromSelfLink(string $link, array $meta = null): self - { - $r = new self; - $r->setLink('self', $link, $meta); - return $r; - } - - public static function fromRelatedLink(string $link, array $meta = null): self - { - $r = new self; - $r->setLink('related', $link, $meta); - return $r; - } - - public static function fromLinkage(Linkage $linkage): self - { - $r = new self; - $r->linkage = $linkage; - return $r; - } - - public function hasLinkageTo(ResourceInterface $resource): bool - { - return ($this->linkage && $this->linkage->isLinkedTo($resource)); - } - - public function jsonSerialize() - { - return array_filter( - [ - 'data' => $this->linkage, - 'links' => $this->links, - 'meta' => $this->meta, - ], - function ($v) { - return null !== $v; - } - ); - } -} diff --git a/src/Document/Resource/ResourceIdentifier.php b/src/Document/Resource/ResourceIdentifier.php deleted file mode 100644 index 7153a11..0000000 --- a/src/Document/Resource/ResourceIdentifier.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource; - -use JsonApiPhp\JsonApi\Document\Meta; -use JsonApiPhp\JsonApi\Document\MetaTrait; - -class ResourceIdentifier implements ResourceInterface -{ - use MetaTrait; - - protected $type; - protected $id; - - public function __construct(string $type, string $id = null, Meta $meta = null) - { - $this->type = $type; - $this->id = $id; - if ($meta) { - $this->setMeta($meta); - } - } - - public function jsonSerialize() - { - return array_filter( - [ - 'type' => $this->type, - 'id' => $this->id, - 'meta' => $this->meta, - ], - function ($v) { - return null !== $v; - } - ); - } - - public function __toString(): string - { - return sprintf("%s:%s", $this->type, $this->id ?: 'null'); - } - - public function identifies(ResourceInterface $resource): bool - { - return $resource instanceof self - && $this->type === $resource->type - && $this->id !== null - && $this->id === $resource->id; - } -} diff --git a/src/Document/Resource/ResourceInterface.php b/src/Document/Resource/ResourceInterface.php deleted file mode 100644 index aadbdf8..0000000 --- a/src/Document/Resource/ResourceInterface.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource; - -interface ResourceInterface extends \JsonSerializable -{ - public function identifies(ResourceInterface $resource): bool; - - public function __toString(): string; -} diff --git a/src/Document/Resource/ResourceObject.php b/src/Document/Resource/ResourceObject.php deleted file mode 100644 index ad21ba0..0000000 --- a/src/Document/Resource/ResourceObject.php +++ /dev/null @@ -1,89 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document\Resource; - -use JsonApiPhp\JsonApi\Document\LinksTrait; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; - -class ResourceObject extends ResourceIdentifier -{ - use LinksTrait; - - private $attributes; - private $relationships; - - public function setAttribute(string $name, $value) - { - if ($this->isReservedName($name)) { - throw new \InvalidArgumentException('Can not use a reserved name'); - } - if (isset($this->relationships[$name])) { - throw new \LogicException("Field $name already exists in relationships"); - } - $this->attributes[$name] = $value; - } - - public function setRelationship(string $name, Relationship $relationship) - { - if ($this->isReservedName($name)) { - throw new \InvalidArgumentException('Can not use a reserved name'); - } - if (isset($this->attributes[$name])) { - throw new \LogicException("Field $name already exists in attributes"); - } - $this->relationships[$name] = $relationship; - } - - public function toId(): ResourceIdentifier - { - return new ResourceIdentifier($this->type, $this->id); - } - - public function jsonSerialize() - { - return array_filter( - [ - 'type' => $this->type, - 'id' => $this->id, - 'attributes' => $this->attributes, - 'relationships' => $this->relationships, - 'links' => $this->links, - 'meta' => $this->meta, - ], - function ($v) { - return null !== $v; - } - ); - } - - public function identifies(ResourceInterface $resource): bool - { - if ($this->relationships) { - /** @var Relationship $relationship */ - foreach ($this->relationships as $relationship) { - if ($relationship->hasLinkageTo($resource)) { - return true; - } - } - } - return false; - } - - /** - * @param string $name - * @return bool - */ - private function isReservedName(string $name): bool - { - return in_array($name, ['id', 'type']); - } -} diff --git a/src/EmptyRelationship.php b/src/EmptyRelationship.php new file mode 100644 index 0000000..2c123bb --- /dev/null +++ b/src/EmptyRelationship.php @@ -0,0 +1,34 @@ +obj = combine($member, ...$members); + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + child($o, 'relationships')->{$this->name} = $this->obj; + } +} diff --git a/src/Error.php b/src/Error.php new file mode 100644 index 0000000..8a222fe --- /dev/null +++ b/src/Error.php @@ -0,0 +1,31 @@ +error = (object)[]; + foreach ($members as $member) { + $member->attachTo($this->error); + } + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->errors[] = $this->error; + } +} diff --git a/src/Error/Code.php b/src/Error/Code.php new file mode 100644 index 0000000..40455dd --- /dev/null +++ b/src/Error/Code.php @@ -0,0 +1,23 @@ +code = $this->code; + } +} diff --git a/src/Error/Detail.php b/src/Error/Detail.php new file mode 100644 index 0000000..217c43d --- /dev/null +++ b/src/Error/Detail.php @@ -0,0 +1,23 @@ +detail = $this->detail; + } +} diff --git a/src/Error/Id.php b/src/Error/Id.php new file mode 100644 index 0000000..371884f --- /dev/null +++ b/src/Error/Id.php @@ -0,0 +1,23 @@ +id = $this->id; + } +} diff --git a/src/Error/SourceParameter.php b/src/Error/SourceParameter.php new file mode 100644 index 0000000..d344d64 --- /dev/null +++ b/src/Error/SourceParameter.php @@ -0,0 +1,25 @@ +parameter = $this->parameter; + } +} diff --git a/src/Error/SourcePointer.php b/src/Error/SourcePointer.php new file mode 100644 index 0000000..c33d0b1 --- /dev/null +++ b/src/Error/SourcePointer.php @@ -0,0 +1,25 @@ +pointer = $this->pointer; + } +} diff --git a/src/Error/Status.php b/src/Error/Status.php new file mode 100644 index 0000000..2083373 --- /dev/null +++ b/src/Error/Status.php @@ -0,0 +1,23 @@ +status = $this->status; + } +} diff --git a/src/Error/Title.php b/src/Error/Title.php new file mode 100644 index 0000000..1352eb5 --- /dev/null +++ b/src/Error/Title.php @@ -0,0 +1,24 @@ +title = $this->title; + } +} diff --git a/src/ErrorDocument.php b/src/ErrorDocument.php new file mode 100644 index 0000000..4cb70b5 --- /dev/null +++ b/src/ErrorDocument.php @@ -0,0 +1,27 @@ +obj = (object)[]; + foreach ($members as $member) { + $member->attachTo($this->obj); + } + } + + public function jsonSerialize(): object { + return $this->obj; + } +} diff --git a/src/Included.php b/src/Included.php new file mode 100644 index 0000000..96a1edb --- /dev/null +++ b/src/Included.php @@ -0,0 +1,39 @@ +key(); + if (isset($this->resources[$key])) { + throw new LogicException("Resource $resource is already included"); + } + $this->resources[$key] = $resource; + } + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + foreach ($this->resources as $resource) { + $resource->attachAsIncludedTo($o); + } + } +} diff --git a/src/Internal/Attachable.php b/src/Internal/Attachable.php new file mode 100644 index 0000000..01de494 --- /dev/null +++ b/src/Internal/Attachable.php @@ -0,0 +1,17 @@ +obj = (object)['type' => $type]; + + $this->addMembers(...$members); + } + + /** + * @param ResourceMember ...$members + * @internal + */ + protected function addMembers(ResourceMember ...$members): void { + $fields = []; + foreach ($members as $member) { + if ($member instanceof ResourceField) { + $name = $member->name(); + if (isset($fields[$name])) { + throw new LogicException("Field '$name' already exists'"); + } + $fields[$name] = true; + } + $member->attachTo($this->obj); + } + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->data = $this->obj; + } +} diff --git a/src/Internal/Collection.php b/src/Internal/Collection.php new file mode 100644 index 0000000..cd8b8d6 --- /dev/null +++ b/src/Internal/Collection.php @@ -0,0 +1,8 @@ +link = (object)['href' => $url]; + foreach ($metas as $meta) { + $meta->attachTo($this->link); + } + } else { + $this->link = $url; + } + } +} diff --git a/src/Internal/MetaDocumentMember.php b/src/Internal/MetaDocumentMember.php new file mode 100644 index 0000000..e204a70 --- /dev/null +++ b/src/Internal/MetaDocumentMember.php @@ -0,0 +1,11 @@ +name; + } +} diff --git a/src/Internal/ResourceMember.php b/src/Internal/ResourceMember.php new file mode 100644 index 0000000..ba4918d --- /dev/null +++ b/src/Internal/ResourceMember.php @@ -0,0 +1,11 @@ +obj = (object)[ + 'version' => $version, + ]; + $meta?->attachTo($this->obj); + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->jsonapi = $this->obj; + } +} diff --git a/src/Link/AboutLink.php b/src/Link/AboutLink.php new file mode 100644 index 0000000..fdafefd --- /dev/null +++ b/src/Link/AboutLink.php @@ -0,0 +1,22 @@ +about = $this->link; + } +} diff --git a/src/Link/FirstLink.php b/src/Link/FirstLink.php new file mode 100644 index 0000000..dfc5b6a --- /dev/null +++ b/src/Link/FirstLink.php @@ -0,0 +1,22 @@ +first = $this->link; + } +} diff --git a/src/Link/LastLink.php b/src/Link/LastLink.php new file mode 100644 index 0000000..a2215b8 --- /dev/null +++ b/src/Link/LastLink.php @@ -0,0 +1,22 @@ +last = $this->link; + } +} diff --git a/src/Link/NextLink.php b/src/Link/NextLink.php new file mode 100644 index 0000000..082fbcc --- /dev/null +++ b/src/Link/NextLink.php @@ -0,0 +1,22 @@ +next = $this->link; + } +} diff --git a/src/Link/PrevLink.php b/src/Link/PrevLink.php new file mode 100644 index 0000000..d38d025 --- /dev/null +++ b/src/Link/PrevLink.php @@ -0,0 +1,22 @@ +prev = $this->link; + } +} diff --git a/src/Link/RelatedLink.php b/src/Link/RelatedLink.php new file mode 100644 index 0000000..501c2ec --- /dev/null +++ b/src/Link/RelatedLink.php @@ -0,0 +1,23 @@ +related = $this->link; + } +} diff --git a/src/Link/SelfLink.php b/src/Link/SelfLink.php new file mode 100644 index 0000000..0168cb1 --- /dev/null +++ b/src/Link/SelfLink.php @@ -0,0 +1,24 @@ +self = $this->link; + } +} diff --git a/src/Meta.php b/src/Meta.php new file mode 100644 index 0000000..ebec4dc --- /dev/null +++ b/src/Meta.php @@ -0,0 +1,38 @@ +{$this->key} = $this->value; + } + } diff --git a/src/MetaDocument.php b/src/MetaDocument.php new file mode 100644 index 0000000..14f0334 --- /dev/null +++ b/src/MetaDocument.php @@ -0,0 +1,20 @@ +doc = combine($meta, ...$members); + } + + public function jsonSerialize(): object { + return $this->doc; + } +} diff --git a/src/NewResourceObject.php b/src/NewResourceObject.php new file mode 100644 index 0000000..44364f1 --- /dev/null +++ b/src/NewResourceObject.php @@ -0,0 +1,20 @@ +data = null; + } +} diff --git a/src/PaginatedCollection.php b/src/PaginatedCollection.php new file mode 100644 index 0000000..ffc9050 --- /dev/null +++ b/src/PaginatedCollection.php @@ -0,0 +1,22 @@ +collection->attachTo($o); + $this->pagination->attachTo($o); + } +} diff --git a/src/Pagination.php b/src/Pagination.php new file mode 100644 index 0000000..dc2c642 --- /dev/null +++ b/src/Pagination.php @@ -0,0 +1,29 @@ +links = $links; + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + foreach ($this->links as $link) { + $link->attachTo($o); + } + } +} diff --git a/src/ResourceCollection.php b/src/ResourceCollection.php new file mode 100644 index 0000000..592dd21 --- /dev/null +++ b/src/ResourceCollection.php @@ -0,0 +1,30 @@ +resources = $resources; + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->data = []; + foreach ($this->resources as $resource) { + $resource->attachToCollection($o); + } + } +} diff --git a/src/ResourceIdentifier.php b/src/ResourceIdentifier.php new file mode 100644 index 0000000..cf1cf88 --- /dev/null +++ b/src/ResourceIdentifier.php @@ -0,0 +1,41 @@ +obj = (object)[ + 'type' => $type, + 'id' => $id, + ]; + foreach ($metas as $meta) { + $meta->attachTo($this->obj); + } + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->data = $this->obj; + } + + /** + * @param object $o + * @internal + */ + public function attachToCollection(object $o): void { + $o->data[] = $this->obj; + } +} diff --git a/src/ResourceIdentifierCollection.php b/src/ResourceIdentifierCollection.php new file mode 100644 index 0000000..794789e --- /dev/null +++ b/src/ResourceIdentifierCollection.php @@ -0,0 +1,30 @@ +identifiers = $identifiers; + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->data = []; + foreach ($this->identifiers as $identifier) { + $identifier->attachToCollection($o); + } + } +} diff --git a/src/ResourceObject.php b/src/ResourceObject.php new file mode 100644 index 0000000..f65d575 --- /dev/null +++ b/src/ResourceObject.php @@ -0,0 +1,52 @@ +obj->id = $id; + $this->type = $type; + } + + public function identifier(): ResourceIdentifier { + return new ResourceIdentifier($this->type, $this->id); + } + + public function key(): string { + return compositeKey($this->type, $this->id); + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $o->data = $this->obj; + } + + /** + * @param object $o + */ + public function attachAsIncludedTo(object $o): void { + $o->included[] = $this->obj; + } + + /** + * @param object $o + * @internal + */ + public function attachToCollection(object $o): void { + $o->data[] = $this->obj; + } + + public function __toString(): string { + return $this->key(); + } +} diff --git a/src/ToMany.php b/src/ToMany.php new file mode 100644 index 0000000..afe4987 --- /dev/null +++ b/src/ToMany.php @@ -0,0 +1,42 @@ +validateFieldName($name); + $this->name = $name; + $this->members = $members; + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $rel = child(child($o, 'relationships'), $this->name); + $rel->data = []; + $this->collection->attachTo($rel); + foreach ($this->members as $member) { + $member->attachTo($rel); + } + } +} diff --git a/src/ToNull.php b/src/ToNull.php new file mode 100644 index 0000000..cbaec93 --- /dev/null +++ b/src/ToNull.php @@ -0,0 +1,34 @@ +validateFieldName($name); + $this->name = $name; + $this->members = $members; + } + + /** + * @param object $o + * @internal + */ + public function attachTo(object $o): void { + $obj = combine(...$this->members); + $obj->data = null; + child($o, 'relationships')->{$this->name} = $obj; + } +} diff --git a/src/ToOne.php b/src/ToOne.php new file mode 100644 index 0000000..eb86853 --- /dev/null +++ b/src/ToOne.php @@ -0,0 +1,30 @@ +validateFieldName($name); + $this->name = $name; + $this->obj = combine($identifier, ...$members); + } + + /** + * @param object $o + * @internal + */ + public function attachTo($o): void { + child($o, 'relationships')->{$this->name} = $this->obj; + } +} diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..5a08291 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,30 @@ +attachTo($obj); + } + return $obj; +} + +function child($o, string $name): mixed { + if (!isset($o->{$name})) { + $o->{$name} = (object)[]; + } + return $o->{$name}; +} + +function isValidName(string $name): bool { + return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1; +} + +function compositeKey(string $type, string $id): string { + return "{$type}:{$id}"; +} diff --git a/test/BaseTestCase.php b/test/BaseTestCase.php index ce01e3d..e649b2c 100644 --- a/test/BaseTestCase.php +++ b/test/BaseTestCase.php @@ -1,22 +1,17 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ + declare(strict_types=1); namespace JsonApiPhp\JsonApi\Test; use PHPUnit\Framework\TestCase; -abstract class BaseTestCase extends TestCase -{ - public static function assertEqualsAsJson($expected, $actual, string $message = '') - { - self::assertEquals(json_encode($expected), json_encode($actual), $message); +abstract class BaseTestCase extends TestCase { + public static function assertEncodesTo(string $expected, $obj, string $message = '') { + self::assertEquals( + json_decode($expected), + json_decode(json_encode($obj, JSON_UNESCAPED_SLASHES)), + $message + ); } } diff --git a/test/CompoundDocumentTest.php b/test/CompoundDocumentTest.php new file mode 100644 index 0000000..cb20ac6 --- /dev/null +++ b/test/CompoundDocumentTest.php @@ -0,0 +1,204 @@ +identifier()) + ); + + $document = new CompoundDocument( + new PaginatedCollection( + new Pagination( + new NextLink('http://example.com/articles?page[offset]=2'), + new LastLink('http://example.com/articles?page[offset]=10') + ), + new ResourceCollection( + new ResourceObject( + 'articles', + '1', + new Attribute('title', 'JSON API paints my bikeshed!'), + new SelfLink('http://example.com/articles/1'), + new ToOne( + 'author', + $dan->identifier(), + new SelfLink('http://example.com/articles/1/relationships/author'), + new RelatedLink('http://example.com/articles/1/author') + ), + new ToMany( + 'comments', + new ResourceIdentifierCollection( + $comment05->identifier(), + $comment12->identifier() + ), + new SelfLink('http://example.com/articles/1/relationships/comments'), + new RelatedLink('http://example.com/articles/1/comments') + ) + ) + ) + ), + new Included($dan, $comment05, $comment12), + new SelfLink('http://example.com/articles') + ); + $this->assertEncodesTo( + ' + { + "links": { + "self": "http://example.com/articles", + "next": "http://example.com/articles?page[offset]=2", + "last": "http://example.com/articles?page[offset]=10" + }, + "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": { + "first-name": "Dan", + "last-name": "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" + } + }] + } + ', + $document + ); + } + + public function testIncludedResourceMayBeIdentifiedByLinkageInPrimaryData() { + $author = new ResourceObject('people', '9'); + $article = new ResourceObject( + 'articles', + '1', + new ToOne('author', $author->identifier()) + ); + $doc = new CompoundDocument($article, new Included($author)); + $this->assertNotEmpty($doc); + } + + public function testIncludedResourceMayBeIdentifiedByAnotherLinkedResource() { + $writer = new ResourceObject('writers', '3', new Attribute('name', 'Eric Evans')); + $book = new ResourceObject( + 'books', + '2', + new Attribute('name', 'Domain Driven Design'), + new ToOne('author', $writer->identifier()) + ); + $cart = new ResourceObject( + 'shopping-carts', + '1', + new ToMany('contents', new ResourceIdentifierCollection($book->identifier())) + ); + $doc = new CompoundDocument($cart, new Included($book, $writer)); + $this->assertNotEmpty($doc); + } + + /** + * A compound document MUST NOT include more than one resource object for each type and id pair. + */ + public function testCanNotBeManyIncludedResourcesWithEqualIdentifiers() { + $this->expectException('LogicException'); + $this->expectExceptionMessage('Resource apples:1 is already included'); + $apple = new ResourceObject('apples', '1'); + new CompoundDocument($apple->identifier(), new Included($apple, $apple)); + } +} diff --git a/test/DataDocument/ManyResourceIdentifiersTest.php b/test/DataDocument/ManyResourceIdentifiersTest.php new file mode 100644 index 0000000..2d4728d --- /dev/null +++ b/test/DataDocument/ManyResourceIdentifiersTest.php @@ -0,0 +1,70 @@ +assertEncodesTo( + ' + { + "data": [] + } + ', + new DataDocument( + new ResourceIdentifierCollection() + ) + ); + } + + public function testExtendedDocument() { + $this->assertEncodesTo( + ' + { + "data": [{ + "type": "apples", + "id": "1", + "meta": {"apple_meta": "foo"} + },{ + "type": "apples", + "id": "2", + "meta": {"apple_meta": "foo"} + }], + "links": { + "self": "/apples" + }, + "jsonapi": { + "version": "1.0" + }, + "meta": {"document_meta": "bar"} + } + ', + new DataDocument( + new ResourceIdentifierCollection( + new ResourceIdentifier( + 'apples', + '1', + new Meta('apple_meta', 'foo') + ), + new ResourceIdentifier( + 'apples', + '2', + new Meta('apple_meta', 'foo') + ) + ), + new SelfLink('/apples'), + new JsonApi(), + new Meta('document_meta', 'bar') + ) + ); + } +} diff --git a/test/DataDocument/ManyResourceObjectsTest.php b/test/DataDocument/ManyResourceObjectsTest.php new file mode 100644 index 0000000..a555e0c --- /dev/null +++ b/test/DataDocument/ManyResourceObjectsTest.php @@ -0,0 +1,82 @@ +assertEncodesTo( + ' + { + "data": [] + } + ', + new DataDocument( + new ResourceCollection() + ) + ); + } + + public function testExtendedDocument() { + $this->assertEncodesTo( + ' + { + "data": [{ + "type": "people", + "id": "1", + "attributes": { + "name": "Martin Fowler" + }, + "meta": {"apple_meta": "foo"} + },{ + "type": "people", + "id": "2", + "attributes": { + "name": "Kent Beck" + }, + "meta": {"apple_meta": "foo"} + }], + "links": { + "self": "/books/123/relationships/authors", + "related": "/books/123/authors" + }, + "jsonapi": { + "version": "1.0" + }, + "meta": {"document_meta": "bar"} + } + ', + new DataDocument( + new ResourceCollection( + new ResourceObject( + 'people', + '1', + new Attribute('name', 'Martin Fowler'), + new Meta('apple_meta', 'foo') + ), + new ResourceObject( + 'people', + '2', + new Attribute('name', 'Kent Beck'), + new Meta('apple_meta', 'foo') + ) + ), + new SelfLink('/books/123/relationships/authors'), + new RelatedLink('/books/123/authors'), + new JsonApi(), + new Meta('document_meta', 'bar') + ) + ); + } +} diff --git a/test/DataDocument/NullDataTest.php b/test/DataDocument/NullDataTest.php new file mode 100644 index 0000000..4bb759d --- /dev/null +++ b/test/DataDocument/NullDataTest.php @@ -0,0 +1,50 @@ +assertEncodesTo( + ' + { + "data": null + } + ', + new DataDocument( + new NullData() + ) + ); + } + + public function testExtendedDocument() { + $this->assertEncodesTo( + ' + { + "data": null, + "links": { + "self": "/apples/1" + }, + "jsonapi": { + "version": "1.0" + }, + "meta": {"document_meta": "bar"} + } + ', + new DataDocument( + new NullData(), + new SelfLink('/apples/1'), + new JsonApi(), + new Meta('document_meta', 'bar') + ) + ); + } +} diff --git a/test/DataDocument/SingleResourceIdentifierTest.php b/test/DataDocument/SingleResourceIdentifierTest.php new file mode 100644 index 0000000..3eea202 --- /dev/null +++ b/test/DataDocument/SingleResourceIdentifierTest.php @@ -0,0 +1,68 @@ +assertEncodesTo( + ' + { + "data": { + "type": "companies", + "id": "1" + } + } + ', + new DataDocument( + new ResourceIdentifier('companies', '1') + ) + ); + } + + public function testExtendedDocument() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "companies", + "id": "1", + "meta": { + "apple_meta": "foo", + "bar": [42] + } + }, + "links": { + "self": "/books/123/relationships/publisher", + "related": "/books/123/publisher" + }, + "jsonapi": { + "version": "1.0" + }, + "meta": {"document_meta": "bar"} + } + ', + new DataDocument( + new ResourceIdentifier( + 'companies', + '1', + new Meta('apple_meta', 'foo'), + new Meta('bar', [42]) + ), + new SelfLink('/books/123/relationships/publisher'), + new RelatedLink('/books/123/publisher'), + new JsonApi(), + new Meta('document_meta', 'bar') + ) + ); + } +} diff --git a/test/DataDocument/SingleResourceObjectTest.php b/test/DataDocument/SingleResourceObjectTest.php new file mode 100644 index 0000000..0d19dde --- /dev/null +++ b/test/DataDocument/SingleResourceObjectTest.php @@ -0,0 +1,68 @@ +assertEncodesTo( + ' + { + "data": { + "type": "apples", + "id": "1" + } + } + ', + new DataDocument( + new ResourceObject('apples', '1') + ) + ); + } + + public function testExtendedDocument() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "apples", + "id": "1", + "attributes": { + "color": "red", + "sort": "Fuji" + }, + "meta": {"apple_meta": "foo"} + }, + "links": { + "self": "/apples/1" + }, + "jsonapi": { + "version": "1.0" + }, + "meta": {"document_meta": "bar"} + } + ', + new DataDocument( + new ResourceObject( + 'apples', + '1', + new Attribute('color', 'red'), + new Attribute('sort', 'Fuji'), + new Meta('apple_meta', 'foo') + ), + new SelfLink('/apples/1'), + new JsonApi(), + new Meta('document_meta', 'bar') + ) + ); + } +} diff --git a/test/Document/CompoundDocumentTest.php b/test/Document/CompoundDocumentTest.php deleted file mode 100644 index 6255a32..0000000 --- a/test/Document/CompoundDocumentTest.php +++ /dev/null @@ -1,141 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document; - -use JsonApiPhp\JsonApi\Document; -use JsonApiPhp\JsonApi\Document\Resource\NullResource; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; -use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; -use JsonApiPhp\JsonApi\Test\BaseTestCase; - -/** - * 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. - * - * @see http://jsonapi.org/format/#document-compound-documents - */ -class CompoundDocumentTest extends BaseTestCase -{ - public function testIncludedResourcesRepresentedAsArray() - { - $apple = new ResourceObject('apples', '1'); - $apple->setAttribute('color', 'red'); - $orange = new ResourceObject('oranges', '1'); - $orange->setAttribute('color', 'orange'); - $basket = new ResourceObject('basket', '1'); - $basket->setRelationship( - 'fruits', - Relationship::fromLinkage( - Linkage::fromManyIdentifiers( - $apple->toId(), - $orange->toId() - ) - ) - ); - $doc = \JsonApiPhp\JsonApi\Document::fromResource($basket); - $doc->setIncluded($apple, $orange); - $this->assertEquals( - [ - [ - 'type' => 'apples', - 'id' => '1', - 'attributes' => [ - 'color' => 'red', - ], - ], - [ - 'type' => 'oranges', - 'id' => '1', - 'attributes' => [ - 'color' => 'orange', - ], - ], - ], - $this->convertToArray($doc)['included'] - ); - } - - /** - * @expectedException \LogicException - * @expectedExceptionMessage Full linkage is required for apples:1 - */ - public function testFullLinkageIsRequired() - { - $doc = Document::fromResource(new NullResource); - $doc->setIncluded(new ResourceObject('apples', '1')); - json_encode($doc); - } - - public function testFullLinkageIsNotRequiredIfSparse() - { - $doc = \JsonApiPhp\JsonApi\Document::fromResource(new NullResource); - $doc->markSparse(); - $doc->setIncluded(new ResourceObject('apples', '1')); - $this->assertCanBeBuilt($doc); - } - - public function testIncludedResourceMayBeIdentifiedByPrimaryData() - { - $apple = new ResourceObject('apples', '1'); - $apple->setAttribute('color', 'red'); - $doc = Document::fromResource($apple->toId()); - $doc->setIncluded($apple); - $this->assertCanBeBuilt($doc); - } - - public function testIncludedResourceMayBeIdentifiedByAnotherIncludedResource() - { - /** - * BasketID identifies included BasketObject - * BasketObject identifies included AppleObject - */ - $apple = new ResourceObject('apples', '1'); - $apple->setAttribute('color', 'red'); - $basket = new ResourceObject('basket', '1'); - $basket->setRelationship( - 'fruits', - Relationship::fromLinkage( - Linkage::fromManyIdentifiers( - $apple->toId() - ) - ) - ); - $doc = \JsonApiPhp\JsonApi\Document::fromResource($basket->toId()); - $doc->setIncluded($apple, $basket); - $this->assertCanBeBuilt($doc); - } - - private function convertToArray($object): array - { - return json_decode(json_encode($object), true); - } - - private function assertCanBeBuilt($doc) - { - $this->assertInternalType('string', json_encode($doc)); - } -} diff --git a/test/Document/DocumentTest.php b/test/Document/DocumentTest.php deleted file mode 100644 index d8b63e9..0000000 --- a/test/Document/DocumentTest.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document; - -use JsonApiPhp\JsonApi\Document; -use JsonApiPhp\JsonApi\Document\ArrayMeta; -use JsonApiPhp\JsonApi\Document\Error; -use JsonApiPhp\JsonApi\Document\Resource\NullResource; -use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; -use JsonApiPhp\JsonApi\Test\BaseTestCase; - -class DocumentTest extends BaseTestCase -{ - public function testCanCreateFromMeta() - { - $this->assertEqualsAsJson( - ['meta' => ['foo' => 'bar']], - Document::fromMeta(new ArrayMeta(['foo' => 'bar'])) - ); - } - - public function testCanCreateFromErrors() - { - $this->assertEqualsAsJson( - ['errors' => []], - Document::fromErrors() - ); - - $this->assertEqualsAsJson( - [ - 'errors' => [ - ['id' => 'first'], - ], - ], - $this->createErrorDoc() - ); - } - - public function testCanCreateNullDocument() - { - $this->assertEqualsAsJson( - ['data' => null], - $this->createNullDoc() - ); - } - - public function testCanCreateFromResourceId() - { - $this->assertEqualsAsJson( - [ - 'data' => [ - 'type' => 'books', - 'id' => 'abc123', - ], - ], - Document::fromResource(new ResourceIdentifier('books', 'abc123')) - ); - } - - public function testCanCreateFromMultipleItems() - { - $this->assertEqualsAsJson( - [ - 'data' => [], - ], - Document::fromResources() - ); - - $this->assertEqualsAsJson( - [ - 'data' => [ - [ - 'type' => 'books', - 'id' => '12', - ], - [ - 'type' => 'carrots', - 'id' => '42', - ], - ], - ], - Document::fromResources( - new ResourceIdentifier('books', '12'), - new ResourceIdentifier('carrots', '42') - ) - ); - } - - public function testDocumentMayContainVersion() - { - $doc = $this->createNullDoc(); - $doc->setApiVersion('1.2.3'); - $doc->setApiMeta(['a' => 'b']); - $this->assertEqualsAsJson( - [ - 'data' => null, - 'jsonapi' => [ - 'version' => '1.2.3', - 'meta' => ['a' => 'b'], - ], - ], - $doc - ); - } - - public function testDocumentMayContainLinks() - { - $doc = $this->createNullDoc(); - $doc->setLink('self', 'http://example.com/self'); - $doc->setLink('related', 'http://example.com/rel', ['a' => 'b']); - $this->assertEqualsAsJson( - [ - 'data' => null, - 'links' => [ - 'self' => 'http://example.com/self', - 'related' => [ - 'href' => 'http://example.com/rel', - 'meta' => ['a' => 'b'], - ], - ], - ], - $doc - ); - } - - private function createErrorDoc(): Document - { - $e = new Error(); - $e->setId('first'); - return Document::fromErrors($e); - } - - private function createNullDoc(): Document - { - return Document::fromResource(new NullResource); - } -} diff --git a/test/Document/ErrorTest.php b/test/Document/ErrorTest.php deleted file mode 100644 index 0f76612..0000000 --- a/test/Document/ErrorTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Document; - -use JsonApiPhp\JsonApi\Test\BaseTestCase; - -class ErrorTest extends BaseTestCase -{ - public function testEmptyErrorIsEmptyObject() - { - $this->assertEquals('{}', json_encode(new Error())); - } - - public function testErrorWithFullSetOfProperties() - { - $e = new Error(); - $e->setId('test_id'); - $e->setAboutLink('http://localhost'); - $e->setStatus('404'); - $e->setCode('OMG'); - $e->setTitle('Error'); - $e->setDetail('Nothing is found'); - $e->setSourcePointer('/data'); - $e->setSourceParameter('test_param'); - $e->setMeta(new ArrayMeta(['foo' => 'bar'])); - - $this->assertEqualsAsJson( - [ - 'id' => 'test_id', - 'links' => [ - 'about' => 'http://localhost', - ], - 'status' => '404', - 'code' => 'OMG', - 'title' => 'Error', - 'detail' => 'Nothing is found', - 'source' => [ - 'pointer' => '/data', - 'parameter' => 'test_param', - ], - 'meta' => [ - 'foo' => 'bar', - ], - ], - $e - ); - } -} diff --git a/test/Document/MetaTest.php b/test/Document/MetaTest.php deleted file mode 100644 index 928dba0..0000000 --- a/test/Document/MetaTest.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document; - -use JsonApiPhp\JsonApi\Document\ArrayMeta; -use PHPUnit\Framework\TestCase; - -class MetaTest extends TestCase -{ - public function testPhpArraysAreConvertedToObjects() - { - $this->assertEquals('{"0":"foo"}', json_encode(new ArrayMeta(['foo']))); - } -} diff --git a/test/Document/Resource/Relationship/LinkageTest.php b/test/Document/Resource/Relationship/LinkageTest.php deleted file mode 100644 index b71222e..0000000 --- a/test/Document/Resource/Relationship/LinkageTest.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document\Resource\Relationship; - -use JsonApiPhp\JsonApi\Document\Resource\NullResource; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; -use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; -use JsonApiPhp\JsonApi\Test\BaseTestCase; - -/** - * 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. - * - * @see http://jsonapi.org/format/#document-resource-object-linkage - * @see LinkageTest::testCanCreateNullLinkage() - * @see LinkageTest::testCanCreateEmptyArrayLinkage() - * @see LinkageTest::testCanCreateFromSingleResourceId() - * @see LinkageTest::testCanCreateFromArrayOfResourceIds() - */ -class LinkageTest extends BaseTestCase -{ - public function testCanCreateNullLinkage() - { - $this->assertEqualsAsJson( - null, - Linkage::nullLinkage() - ); - } - - public function testCanCreateEmptyArrayLinkage() - { - $this->assertEqualsAsJson( - [], - Linkage::emptyArrayLinkage() - ); - } - - public function testCanCreateFromSingleResourceId() - { - $this->assertEqualsAsJson( - [ - 'type' => 'books', - 'id' => 'abc', - ], - Linkage::fromSingleIdentifier(new ResourceIdentifier('books', 'abc')) - ); - } - - public function testCanCreateFromArrayOfResourceIds() - { - $this->assertEqualsAsJson( - [ - [ - 'type' => 'books', - 'id' => 'abc', - ], - [ - 'type' => 'squirrels', - 'id' => '123', - ], - ], - Linkage::fromManyIdentifiers(new ResourceIdentifier('books', 'abc'), new ResourceIdentifier('squirrels', '123')) - ); - } - - public function testNullLinkageIsLinkedToNothing() - { - $apple = new ResourceIdentifier('apples', '1'); - $this->assertFalse(Linkage::nullLinkage()->isLinkedTo($apple)); - $this->assertFalse(Linkage::nullLinkage()->isLinkedTo(new NullResource)); - } - - public function testEmptyArrayLinkageIsLinkedToNothing() - { - $apple = new ResourceIdentifier('apples', '1'); - $this->assertFalse(Linkage::emptyArrayLinkage()->isLinkedTo($apple)); - $this->assertFalse(Linkage::emptyArrayLinkage()->isLinkedTo(new NullResource)); - } - - public function testSingleLinkageIsLinkedOnlyToItself() - { - $apple = new ResourceIdentifier('apples', '1'); - $orange = new ResourceIdentifier('oranges', '1'); - - $linkage = Linkage::fromSingleIdentifier($apple); - - $this->assertTrue($linkage->isLinkedTo($apple)); - $this->assertFalse($linkage->isLinkedTo($orange)); - } - - public function testMultiLinkageIsLinkedOnlyToItsMembers() - { - $apple = new ResourceIdentifier('apples', '1'); - $orange = new ResourceIdentifier('oranges', '1'); - $banana = new ResourceIdentifier('bananas', '1'); - - $linkage = Linkage::fromManyIdentifiers($apple, $orange); - - $this->assertTrue($linkage->isLinkedTo($apple)); - $this->assertTrue($linkage->isLinkedTo($orange)); - $this->assertFalse($linkage->isLinkedTo($banana)); - } -} diff --git a/test/Document/Resource/Relationship/RelationshipTest.php b/test/Document/Resource/Relationship/RelationshipTest.php deleted file mode 100644 index c98948d..0000000 --- a/test/Document/Resource/Relationship/RelationshipTest.php +++ /dev/null @@ -1,97 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document\Resource\Relationship; - -use JsonApiPhp\JsonApi\Document\ArrayMeta; -use JsonApiPhp\JsonApi\Document\Meta; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; -use JsonApiPhp\JsonApi\Test\BaseTestCase; - -/** - * 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. - * - * @see http://jsonapi.org/format/#document-resource-object-relationships - * @see RelationshipTest::testCanCreateFromSelfLink() - * @see RelationshipTest::testCanCreateFromRelatedLink()) - * @see RelationshipTest::testCanCreateFromLinkage()) - * @see RelationshipTest::testCanCreateFromMeta()) - */ -class RelationshipTest extends BaseTestCase -{ - public function testCanCreateFromSelfLink() - { - $this->assertEqualsAsJson( - [ - 'links' => [ - 'self' => 'http://localhost', - ], - ], - Relationship::fromSelfLink('http://localhost') - ); - } - - public function testCanCreateFromRelatedLink() - { - $this->assertEqualsAsJson( - [ - 'links' => [ - 'related' => 'http://localhost', - ], - ], - Relationship::fromRelatedLink('http://localhost') - ); - } - - public function testCanCreateFromLinkage() - { - $this->assertEqualsAsJson( - [ - 'data' => null, - ], - Relationship::fromLinkage(Linkage::nullLinkage()) - ); - } - - public function testCanCreateFromMeta() - { - $this->assertEqualsAsJson( - [ - 'meta' => [ - 'a' => 'b', - ], - ], - Relationship::fromMeta(new ArrayMeta(['a' => 'b'])) - ); - } -} diff --git a/test/Document/Resource/ResourceFieldsTest.php b/test/Document/Resource/ResourceFieldsTest.php deleted file mode 100644 index e14e0b2..0000000 --- a/test/Document/Resource/ResourceFieldsTest.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document\Resource; - -use JsonApiPhp\JsonApi\Document\ArrayMeta; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; -use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; -use PHPUnit\Framework\TestCase; - -/** - * 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. - * - * @see http://jsonapi.org/format/#document-resource-object-fields - */ -class ResourceFieldsTest extends TestCase -{ - /** - * @expectedException \LogicException - * @expectedExceptionMessage Field foo already exists in attributes - */ - public function testCanNotSetRelationshipIfAttributeExists() - { - $res = new ResourceObject('books', '1'); - $res->setAttribute('foo', 'bar'); - $res->setRelationship('foo', Relationship::fromMeta(new ArrayMeta(['a' => 'b']))); - } - - /** - * @expectedException \LogicException - * @expectedExceptionMessage Field foo already exists in relationships - */ - public function testCanNotSetAttributeIfRelationshipExists() - { - $res = new ResourceObject('books', '1'); - $res->setRelationship('foo', Relationship::fromMeta(new ArrayMeta(['a' => 'b']))); - $res->setAttribute('foo', 'bar'); - } - - /** - * @param string $name - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Can not use a reserved name - * @dataProvider invalidAttributeNames - */ - public function testAttributeCanNotHaveReservedNames(string $name) - { - $res = new ResourceObject('books', 'abc'); - $res->setAttribute($name, 1); - } - - /** - * @param string $name - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Can not use a reserved name - * @dataProvider invalidAttributeNames - */ - public function testRelationshipCanNotHaveReservedNames(string $name) - { - $res = new ResourceObject('books', 'abc'); - $res->setRelationship($name, Relationship::fromMeta(new ArrayMeta(['a' => 'b']))); - } - - public function invalidAttributeNames(): array - { - return [ - ['id'], - ['type'], - ]; - } -} diff --git a/test/Document/Resource/ResourceTest.php b/test/Document/Resource/ResourceTest.php deleted file mode 100644 index 48957ca..0000000 --- a/test/Document/Resource/ResourceTest.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test\Document\Resource; - -use JsonApiPhp\JsonApi\Document\ArrayMeta; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; -use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; -use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; -use JsonApiPhp\JsonApi\Test\BaseTestCase; - -/** - * Resource Objects - * - * @see http://jsonapi.org/format/#document-resource-objects - */ -class ResourceTest extends BaseTestCase -{ - /** - * @param array $expected - * @param mixed $data - * @dataProvider resourceProvider - */ - public function testSerialization(array $expected, $data) - { - $this->assertEqualsAsJson($expected, $data); - } - - public function resourceProvider() - { - return [ - [ - [ - 'type' => 'books', - ], - new ResourceIdentifier('books'), - ], - [ - [ - 'type' => 'books', - 'id' => '42abc', - ], - new ResourceIdentifier('books', '42abc'), - ], - [ - [ - 'type' => 'books', - 'id' => '42abc', - 'meta' => [ - 'foo' => 'bar', - ], - ], - new ResourceIdentifier('books', '42abc', new ArrayMeta(['foo' => 'bar'])), - ], - [ - [ - 'type' => 'books', - 'id' => '42abc', - 'attributes' => [ - 'attr' => 'val', - ], - 'relationships' => [ - 'author' => [ - 'meta' => [ - 'a' => 'b', - ], - ], - ], - 'links' => [ - 'self' => 'http://localhost', - ], - 'meta' => [ - 'foo' => 'bar', - ], - ], - (function () { - $resource = new ResourceObject('books', '42abc'); - $resource->setMeta(new ArrayMeta(['foo' => 'bar'])); - $resource->setAttribute('attr', 'val'); - $resource->setLink('self', 'http://localhost'); - $resource->setRelationship('author', Relationship::fromMeta(new ArrayMeta(['a' => 'b']))); - return $resource; - })(), - ], - ]; - } -} diff --git a/test/ErrorDocumentTest.php b/test/ErrorDocumentTest.php new file mode 100644 index 0000000..cb0bc4c --- /dev/null +++ b/test/ErrorDocumentTest.php @@ -0,0 +1,116 @@ +assertEncodesTo( + ' + { + "errors": [{}] + } + ', + new ErrorDocument( + new Error() + ) + ); + } + + public function testExtensiveExample() { + $this->assertEncodesTo( + ' + { + "errors": [{ + "id": "1", + "links": { + "about":"/errors/not_found" + }, + "status": "404", + "code": "not_found", + "title": "Resource not found", + "detail": "We tried hard but could not find anything", + "source": { + "pointer": "/data", + "parameter": "query_string" + }, + "meta": { + "purpose":"test" + } + }], + "meta": {"purpose": "test"}, + "jsonapi": { + "version": "1.0" + } + } + ', + new ErrorDocument( + new Error( + new Id('1'), + new AboutLink('/errors/not_found'), + new Status('404'), + new Code('not_found'), + new Title('Resource not found'), + new Detail('We tried hard but could not find anything'), + new SourcePointer('/data'), + new SourceParameter('query_string'), + new Meta('purpose', 'test') + ), + new Meta('purpose', 'test'), + new JsonApi() + ) + ); + } + + public function testMultipleErrors() { + $this->assertEncodesTo( + ' + { + "errors": [{ + "id": "1", + "code": "invalid_parameter", + "source": { + "parameter": "foo" + } + },{ + "id": "2", + "code": "invalid_parameter", + "source": { + "parameter": "bar" + } + }] + } + ', + new ErrorDocument( + new Error( + new Id('1'), + new Code('invalid_parameter'), + new SourceParameter('foo') + ), + new Error( + new Id('2'), + new Code('invalid_parameter'), + new SourceParameter('bar') + ) + ) + ); + } +} diff --git a/test/ExamplesTest.php b/test/ExamplesTest.php new file mode 100644 index 0000000..7dc5da4 --- /dev/null +++ b/test/ExamplesTest.php @@ -0,0 +1,24 @@ +assertJson(`php $file`); + } + + public function examples() { + return [ + [__DIR__.'/../examples/compound_doc.php'], + [__DIR__.'/../examples/simple_doc.php'], + ]; + } +} diff --git a/test/IntegrationTest.php b/test/IntegrationTest.php deleted file mode 100644 index d81c007..0000000 --- a/test/IntegrationTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -declare(strict_types=1); - -namespace JsonApiPhp\JsonApi\Test; - -use JsonApiPhp\JsonApi\Document; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Linkage; -use JsonApiPhp\JsonApi\Document\Resource\Relationship\Relationship; -use JsonApiPhp\JsonApi\Document\Resource\ResourceIdentifier; -use JsonApiPhp\JsonApi\Document\Resource\ResourceObject; -use PHPUnit\Framework\TestCase; - -class IntegrationTest extends TestCase -{ - public function testFromTheReadmeFile() - { - $json = <<setLink('self', '/articles/1/relationships/author'); - $author->setLink('related', '/articles/1/author'); - $articles->setRelationship('author', $author); - $articles->setAttribute('title', 'Rails is Omakase'); - $doc = Document::fromResource($articles); - - $this->assertEquals( - $json, - json_encode($doc, JSON_PRETTY_PRINT) - ); - } -} diff --git a/test/JsonApiTest.php b/test/JsonApiTest.php new file mode 100644 index 0000000..8a5ce91 --- /dev/null +++ b/test/JsonApiTest.php @@ -0,0 +1,33 @@ +assertEncodesTo( + ' + { + "meta": { + "test": "test" + }, + "jsonapi": { + "version": "1.0", + "meta": { + "foo": "bar" + } + } + } + ', + new MetaDocument( + new Meta('test', 'test'), + new JsonApi('1.0', new Meta('foo', 'bar')) + ) + ); + } +} diff --git a/test/LinkObjectTest.php b/test/LinkObjectTest.php new file mode 100644 index 0000000..00a9a78 --- /dev/null +++ b/test/LinkObjectTest.php @@ -0,0 +1,34 @@ +assertEncodesTo( + '{ + "data": {"type": "apples", "id": "1"}, + "links": { + "self": { + "href": "http://example.com", + "meta": { + "foo": "bar", + "test": true + } + } + } + } + ', + new DataDocument( + new ResourceIdentifier('apples', '1'), + new SelfLink('http://example.com', new Meta('foo', 'bar'), new Meta('test', true)) + ) + ); + } +} diff --git a/test/MetaDocumentTest.php b/test/MetaDocumentTest.php new file mode 100644 index 0000000..1837f3e --- /dev/null +++ b/test/MetaDocumentTest.php @@ -0,0 +1,49 @@ +assertEncodesTo( + ' + { + "meta": { + "foo": "bar" + } + } + ', + new MetaDocument(new Meta('foo', 'bar')) + ); + } + + /** + * A meta document may contain jsonapi member + */ + public function testMetaDocumentWithExtraMembers() { + $this->assertEncodesTo( + ' + { + "meta": { + "foo": "bar" + }, + "jsonapi": { + "version": "1.0" + } + } + ', + new MetaDocument( + new Meta('foo', 'bar'), + new JsonApi() + ) + ); + } +} diff --git a/test/MetaTest.php b/test/MetaTest.php new file mode 100644 index 0000000..c8d8d93 --- /dev/null +++ b/test/MetaTest.php @@ -0,0 +1,16 @@ +expectException(\DomainException::class); + $this->expectExceptionMessage("Invalid character in a member name 'invalid:name'"); + new Meta('invalid:name', '1'); + } +} diff --git a/test/NewResourceObjectTest.php b/test/NewResourceObjectTest.php new file mode 100644 index 0000000..a2fc94e --- /dev/null +++ b/test/NewResourceObjectTest.php @@ -0,0 +1,201 @@ +assertEncodesTo( + ' + { + "data": { + "type": "apples", + "attributes": { + "title": "Rails is Omakase" + }, + "meta": {"foo": "bar"}, + "relationships": { + "author": { + "meta": {"foo": "bar"}, + "data": null + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'apples', + new Meta('foo', 'bar'), + new Attribute('title', 'Rails is Omakase'), + new ToNull( + 'author', + new Meta('foo', 'bar') + ) + ) + ) + ); + } + + public function testRelationshipWithSingleIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "content": { + "data": {"type": "apples", "id": "1"} + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new ToOne('content', new ResourceIdentifier('apples', '1')) + ) + ) + ); + } + + public function testRelationshipWithMultiIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "content": { + "data": [{ + "type": "apples", + "id": "1" + },{ + "type": "pears", + "id": "2" + }] + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new ToMany( + 'content', + new ResourceIdentifierCollection( + new ResourceIdentifier('apples', '1'), + new ResourceIdentifier('pears', '2') + ) + ) + ) + ) + ); + } + + public function testRelationshipWithEmptyMultiIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "content": { + "data": [] + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new ToMany('content', new ResourceIdentifierCollection()) + ) + ) + ); + } + + public function testRelationshipWithNoData() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "empty": { + "links": { + "related": "/foo" + } + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new EmptyRelationship('empty', new RelatedLink('/foo')) + ) + ) + ); + + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "relationships": { + "empty": { + "links": { + "related": "/foo", + "self": "/bar" + }, + "meta": { + "foo": "bar" + } + } + } + } + } + ', + new DataDocument( + new NewResourceObject( + 'basket', + new EmptyRelationship('empty', new RelatedLink('/foo'), new SelfLink('/bar'), new Meta('foo', 'bar')) + ) + ) + ); + } + + public function testResourceFieldsMustBeUnique() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage("Field 'foo' already exists"); + new NewResourceObject( + 'apples', + new Attribute('foo', 'bar'), + new ToOne('foo', new ResourceIdentifier('apples', '1')) + ); + } + + public function testNameValidation() { + $this->expectException(\DomainException::class); + new NewResourceObject('invalid:id'); + } +} diff --git a/test/PaginationTest.php b/test/PaginationTest.php new file mode 100644 index 0000000..a9307d8 --- /dev/null +++ b/test/PaginationTest.php @@ -0,0 +1,113 @@ +assertEncodesTo( + ' + { + "data": [ + {"type": "apples", "id": "1"}, + {"type": "apples", "id": "2"} + ], + "links": { + "first": "http://example.com/fruits?page=first", + "last": "http://example.com/fruits?page=last", + "prev": "http://example.com/fruits?page=3", + "next": "http://example.com/fruits?page=5", + "self": "http://example.com/fruits?page=4" + } + } + ', + new DataDocument( + new PaginatedCollection( + new Pagination( + new FirstLink('http://example.com/fruits?page=first'), + new PrevLink('http://example.com/fruits?page=3'), + new NextLink('http://example.com/fruits?page=5'), + new LastLink('http://example.com/fruits?page=last') + ), + new ResourceCollection( + new ResourceObject('apples', '1'), + new ResourceObject('apples', '2') + ) + ), + new SelfLink('http://example.com/fruits?page=4') + ) + ); + } + + public function testPaginatedResourceIdentifierCollection() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "baskets", + "id": "1", + "relationships": { + "fruits": { + "data": [ + {"type": "apples", "id": "1"}, + {"type": "apples", "id": "2"} + ], + "links": { + "first": "http://example.com/basket/1/fruits?page=first", + "last": "http://example.com/basket/1/fruits?page=last", + "prev": "http://example.com/basket/1/fruits?page=3", + "next": "http://example.com/basket/1/fruits?page=5" + } + } + } + }, + "included": [ + {"type": "apples", "id": "1", "attributes": {"color": "red"}}, + {"type": "apples", "id": "2", "attributes": {"color": "yellow"}} + ] + } + ', + new CompoundDocument( + new ResourceObject( + 'baskets', + '1', + new ToMany( + 'fruits', + new ResourceIdentifierCollection( + new ResourceIdentifier('apples', '1'), + new ResourceIdentifier('apples', '2') + ), + new Pagination( + new FirstLink('http://example.com/basket/1/fruits?page=first'), + new PrevLink('http://example.com/basket/1/fruits?page=3'), + new NextLink('http://example.com/basket/1/fruits?page=5'), + new LastLink('http://example.com/basket/1/fruits?page=last') + ) + ) + ), + new Included( + new ResourceObject('apples', '1', new Attribute('color', 'red')), + new ResourceObject('apples', '2', new Attribute('color', 'yellow')) + ) + ) + ); + } +} diff --git a/test/ResourceIdentifierTest.php b/test/ResourceIdentifierTest.php new file mode 100644 index 0000000..eb479ee --- /dev/null +++ b/test/ResourceIdentifierTest.php @@ -0,0 +1,14 @@ +expectException(\DomainException::class); + new ResourceIdentifier('invalid:id', 'foo'); + } +} diff --git a/test/ResourceObjectTest.php b/test/ResourceObjectTest.php new file mode 100644 index 0000000..b565d7d --- /dev/null +++ b/test/ResourceObjectTest.php @@ -0,0 +1,278 @@ +assertEncodesTo( + ' + { + "data": { + "type": "apples", + "id": "1", + "attributes": { + "title": "Rails is Omakase" + }, + "meta": {"foo": "bar"}, + "links": { + "self": "http://self" + }, + "relationships": { + "author": { + "meta": {"foo": "bar"}, + "links": { + "self": "http://rel/author", + "related": "http://author" + }, + "data": null + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'apples', + '1', + new Meta('foo', 'bar'), + new Attribute('title', 'Rails is Omakase'), + new SelfLink('http://self'), + new ToNull( + 'author', + new Meta('foo', 'bar'), + new SelfLink('http://rel/author'), + new RelatedLink('http://author') + ) + ) + ) + ); + } + + public function testRelationshipWithSingleIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "content": { + "data": {"type": "apples", "id": "1"} + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new ToOne('content', new ResourceIdentifier('apples', '1')) + ) + ) + ); + } + + public function testRelationshipWithMultiIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "content": { + "data": [{ + "type": "apples", + "id": "1" + },{ + "type": "pears", + "id": "2" + }] + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new ToMany( + 'content', + new ResourceIdentifierCollection( + new ResourceIdentifier('apples', '1'), + new ResourceIdentifier('pears', '2') + ) + ) + ) + ) + ); + } + + public function testRelationshipWithEmptyMultiIdLinkage() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "content": { + "data": [] + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new ToMany('content', new ResourceIdentifierCollection()) + ) + ) + ); + } + + public function testRelationshipWithNoData() { + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "empty": { + "links": { + "related": "/foo" + } + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new EmptyRelationship('empty', new RelatedLink('/foo')) + ) + ) + ); + + $this->assertEncodesTo( + ' + { + "data": { + "type": "basket", + "id": "1", + "relationships": { + "empty": { + "links": { + "related": "/foo", + "self": "/bar" + }, + "meta": { + "foo": "bar" + } + } + } + } + } + ', + new DataDocument( + new ResourceObject( + 'basket', + '1', + new EmptyRelationship('empty', new RelatedLink('/foo'), new SelfLink('/bar'), new Meta('foo', 'bar')) + ) + ) + ); + } + + public function testCanNotCreateIdAttribute() { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage("Can not use 'id' as a resource field"); + new Attribute('id', 'foo'); + } + + public function testCanNotCreateTypeAttribute() { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage("Can not use 'type' as a resource field"); + new Attribute('type', 'foo'); + } + + public function testCanNotCreateIdRelationship() { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage("Can not use 'id' as a resource field"); + new ToOne('id', new ResourceIdentifier('apples', '1')); + } + + public function testCanNotCreateTypeRelationship() { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage("Can not use 'type' as a resource field"); + new ToOne('type', new ResourceIdentifier('apples', '1')); + } + + /** + * @dataProvider invalidCharacters + * @param string $invalid_char + */ + public function testAttributeMustOnlyHaveAllowedCharacters(string $invalid_char) { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('Invalid character in a member name'); + new Attribute("foo{$invalid_char}bar", 'plus can not be used'); + } + + /** + * @dataProvider invalidCharacters + * @param string $invalid_char + */ + public function testRelationshipMustOnlyHaveAllowedCharacters(string $invalid_char) { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('Invalid character in a member name'); + new ToNull("foo{$invalid_char}bar"); + } + + public function invalidCharacters() { + return [ + ['+'], + ['!'], + ['@'], + ['/'], + ['}'], + ]; + } + + public function testResourceFieldsMustBeUnique() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage("Field 'foo' already exists"); + new ResourceObject( + 'apples', + '1', + new Attribute('foo', 'bar'), + new ToOne('foo', new ResourceIdentifier('apples', '1')) + ); + } + + public function testNameValidation() { + $this->expectException(\DomainException::class); + new ResourceObject('invalid:id', 'foo'); + } +} diff --git a/test/benchmarks/compound10k.php b/test/benchmarks/compound10k.php new file mode 100644 index 0000000..44664fd --- /dev/null +++ b/test/benchmarks/compound10k.php @@ -0,0 +1,122 @@ +identifier()) + ); + + $document = new CompoundDocument( + new PaginatedCollection( + new Pagination( + new NextLink('http://example.com/articles?page[offset]=2'), + new LastLink('http://example.com/articles?page[offset]=10') + ), + new ResourceCollection( + new ResourceObject( + 'articles', + '1', + new Attribute('title', 'JSON API paints my bikeshed!'), + new SelfLink('http://example.com/articles/1'), + new ToOne( + 'author', + $dan->identifier(), + new SelfLink('http://example.com/articles/1/relationships/author'), + new RelatedLink('http://example.com/articles/1/author') + ), + new ToMany( + 'comments', + new ResourceIdentifierCollection( + $comment05->identifier(), + $comment12->identifier() + ), + new SelfLink('http://example.com/articles/1/relationships/comments'), + new RelatedLink('http://example.com/articles/1/comments') + ) + ) + ) + ), + new Included($dan, $comment05, $comment12), + new SelfLink('http://example.com/articles') + ); + + $data_doc_json = json_encode($document); +} + +for ($count = 0; $count < 1000; $count++) { + $error = new ErrorDocument( + new Error( + new Id('1'), + new AboutLink('/errors/not_found'), + new Status('404'), + new Code('not_found'), + new Title('Resource not found'), + new Detail('We tried hard but could not find anything'), + new SourcePointer('/data'), + new SourceParameter('query_string'), + new Meta('purpose', 'test') + ), + new Meta('purpose', 'test'), + new JsonApi() + ); + + $error_doc_json = json_encode($error); +} + +echo $data_doc_json; +echo "\n\n"; +echo $error_doc_json;