Improve spec compliance for resource creation; add ResourceType::url()
This commit is contained in:
parent
7405e07b93
commit
d015070569
|
|
@ -13,6 +13,8 @@ namespace Tobyz\JsonApiServer\Endpoint\Concerns;
|
||||||
|
|
||||||
use Tobyz\JsonApiServer\Context;
|
use Tobyz\JsonApiServer\Context;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ConflictException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||||
|
|
@ -43,16 +45,22 @@ trait SavesData
|
||||||
throw new BadRequestException('data must be an object');
|
throw new BadRequestException('data must be an object');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! isset($body['data']['type']) || $body['data']['type'] !== $resourceType->getType()) {
|
if (! isset($body['data']['type'])) {
|
||||||
throw new BadRequestException('data.type does not match the resource type');
|
throw new BadRequestException('data.type must be present');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($body['data']['type'] !== $resourceType->getType()) {
|
||||||
|
throw new ConflictException('data.type does not match the resource type');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($model) {
|
if ($model) {
|
||||||
$id = $resourceType->getAdapter()->getId($model);
|
$id = $resourceType->getAdapter()->getId($model);
|
||||||
|
|
||||||
if (! isset($body['data']['id']) || $body['data']['id'] !== $id) {
|
if (! isset($body['data']['id']) || $body['data']['id'] !== $id) {
|
||||||
throw new BadRequestException('data.id does not match the resource ID');
|
throw new ConflictException('data.id does not match the resource ID');
|
||||||
}
|
}
|
||||||
|
} elseif (isset($body['data']['id'])) {
|
||||||
|
throw new ForbiddenException('Client-generated IDs are not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($body['data']['attributes']) && ! is_array($body['data']['attributes'])) {
|
if (isset($body['data']['attributes']) && ! is_array($body['data']['attributes'])) {
|
||||||
|
|
@ -156,7 +164,11 @@ trait SavesData
|
||||||
|
|
||||||
$value = get_value($data, $field);
|
$value = get_value($data, $field);
|
||||||
|
|
||||||
if (isset($value['data'])) {
|
if (! array_key_exists('data', $value)) {
|
||||||
|
throw new BadRequestException('relationship does not include data key');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value['data'] !== null) {
|
||||||
$allowedTypes = (array) $field->getType();
|
$allowedTypes = (array) $field->getType();
|
||||||
|
|
||||||
if ($field instanceof HasOne) {
|
if ($field instanceof HasOne) {
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ class Create
|
||||||
|
|
||||||
return (new Show())
|
return (new Show())
|
||||||
->handle($context, $resourceType, $model)
|
->handle($context, $resourceType, $model)
|
||||||
->withStatus(201);
|
->withStatus(201)
|
||||||
|
->withHeader('Location', $resourceType->url($model, $context));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function newModel(ResourceType $resourceType, Context $context)
|
private function newModel(ResourceType $resourceType, Context $context)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of tobyz/json-api-server.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class ConflictException extends DomainException implements ErrorProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Conflict'),
|
||||||
|
new Error\Status($this->getJsonApiStatus()),
|
||||||
|
...($this->message ? [new Error\Detail($this->message)] : [])
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJsonApiStatus(): string
|
||||||
|
{
|
||||||
|
return '409';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,6 +54,13 @@ final class ResourceType
|
||||||
return $this->schema;
|
return $this->schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function url($model, Context $context): string
|
||||||
|
{
|
||||||
|
$id = $this->adapter->getId($model);
|
||||||
|
|
||||||
|
return $context->getApi()->getBasePath()."/$this->type/$id";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the resource type's scopes to a query.
|
* Apply the resource type's scopes to a query.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ final class Serializer
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'fields' => [],
|
'fields' => [],
|
||||||
'links' => [
|
'links' => [
|
||||||
'self' => new Structure\Link\SelfLink($url = $this->resourceUrl($type, $id)),
|
'self' => new Structure\Link\SelfLink($url = $resourceType->url($model, $this->context)),
|
||||||
],
|
],
|
||||||
'meta' => $this->meta($schema->getMeta(), $model)
|
'meta' => $this->meta($schema->getMeta(), $model)
|
||||||
];
|
];
|
||||||
|
|
@ -107,11 +107,6 @@ final class Serializer
|
||||||
return $type.':'.$id;
|
return $type.':'.$id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resourceUrl(string $type, string $id): string
|
|
||||||
{
|
|
||||||
return $this->context->getApi()->getBasePath()."/$type/$id";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Structure\Internal\RelationshipMember[]
|
* @return Structure\Internal\RelationshipMember[]
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,17 @@
|
||||||
|
|
||||||
namespace Tobyz\Tests\JsonApiServer\specification;
|
namespace Tobyz\Tests\JsonApiServer\specification;
|
||||||
|
|
||||||
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ConflictException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
||||||
use Tobyz\JsonApiServer\JsonApi;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://jsonapi.org/format/1.0/#crud-creating
|
* @see https://jsonapi.org/format/1.1/#crud-creating
|
||||||
*/
|
*/
|
||||||
class CreatingResourcesTest extends AbstractTestCase
|
class CreatingResourcesTest extends AbstractTestCase
|
||||||
{
|
{
|
||||||
|
|
@ -25,55 +30,117 @@ class CreatingResourcesTest extends AbstractTestCase
|
||||||
*/
|
*/
|
||||||
private $api;
|
private $api;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var MockAdapter
|
|
||||||
*/
|
|
||||||
private $adapter;
|
|
||||||
|
|
||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->api = new JsonApi('http://example.com');
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$this->adapter = new MockAdapter();
|
$this->api->resourceType('users', new MockAdapter(), function (Type $type) {
|
||||||
|
$type->creatable();
|
||||||
|
$type->attribute('name')->writable();
|
||||||
|
$type->hasOne('pet')->writable();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_bad_request_error_if_body_does_not_contain_data_type()
|
public function test_bad_request_error_if_body_does_not_contain_data_type()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [],
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_bad_request_error_if_relationship_does_not_contain_data()
|
public function test_bad_request_error_if_relationship_does_not_contain_data()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'relationships' => [
|
||||||
|
'pet' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_forbidden_error_if_client_generated_id_provided()
|
public function test_forbidden_error_if_client_generated_id_provided()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$this->expectException(ForbiddenException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_created_response_if_resource_successfully_created()
|
public function test_created_response_includes_created_data_and_location_header()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$response = $this->api->handle(
|
||||||
}
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
public function test_created_response_includes_created_data()
|
$this->assertEquals(201, $response->getStatusCode());
|
||||||
{
|
$this->assertEquals('http://example.com/users/1', $response->getHeaderLine('location'));
|
||||||
$this->markTestIncomplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_created_response_includes_location_header_and_matches_self_link()
|
$this->assertJsonApiDocumentSubset([
|
||||||
{
|
'data' => [
|
||||||
$this->markTestIncomplete();
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], $response->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_not_found_error_if_references_resource_that_does_not_exist()
|
public function test_not_found_error_if_references_resource_that_does_not_exist()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$this->expectException(ResourceNotFoundException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'relationships' => [
|
||||||
|
'pet' => [
|
||||||
|
'data' => ['type' => 'pets', 'id' => '1'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_conflict_error_if_type_does_not_match_endpoint()
|
public function test_conflict_error_if_type_does_not_match_endpoint()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$this->expectException(ConflictException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'pets',
|
||||||
|
'id' => '1',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue