Skip to content

Commit c1dc91c

Browse files
committed
Add jsonapi object, more spec tests
1 parent d678a2e commit c1dc91c

File tree

11 files changed

+285
-124
lines changed

11 files changed

+285
-124
lines changed

src/Endpoint/Concerns/BuildsMeta.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Tobyz\JsonApiServer\Endpoint\Concerns;
1313

14+
use JsonApiPhp\JsonApi\JsonApi;
1415
use JsonApiPhp\JsonApi\Meta;
1516
use Tobyz\JsonApiServer\Context;
1617

@@ -26,4 +27,9 @@ private function buildMeta(Context $context): array
2627

2728
return $meta;
2829
}
30+
31+
private function buildJsonApiObject(Context $context): JsonApi
32+
{
33+
return new JsonApi('1.1');
34+
}
2935
}

src/Endpoint/Concerns/SavesData.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,22 @@ private function parseData(ResourceType $resourceType, $body, $model = null): ar
4949
throw new BadRequestException('data.type must be present');
5050
}
5151

52-
if ($body['data']['type'] !== $resourceType->getType()) {
53-
throw new ConflictException('data.type does not match the resource type');
54-
}
55-
5652
if ($model) {
57-
$id = $resourceType->getAdapter()->getId($model);
53+
if (! isset($body['data']['id'])) {
54+
throw new BadRequestException('data.id must be present');
55+
}
5856

59-
if (! isset($body['data']['id']) || $body['data']['id'] !== $id) {
57+
if ($body['data']['id'] !== $resourceType->getAdapter()->getId($model)) {
6058
throw new ConflictException('data.id does not match the resource ID');
6159
}
6260
} elseif (isset($body['data']['id'])) {
6361
throw new ForbiddenException('Client-generated IDs are not supported');
6462
}
6563

64+
if ($body['data']['type'] !== $resourceType->getType()) {
65+
throw new ConflictException('data.type does not match the resource type');
66+
}
67+
6668
if (isset($body['data']['attributes']) && ! is_array($body['data']['attributes'])) {
6769
throw new BadRequestException('data.attributes must be an object');
6870
}

src/Endpoint/Delete.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public function handle(Context $context, ResourceType $resourceType, $model): Re
5454
run_callbacks($schema->getListeners('deleted'), [&$model, $context]);
5555

5656
if (count($meta = $this->buildMeta($context))) {
57+
$meta[] = $this->buildJsonApiObject($context);
58+
5759
return json_api_response(
5860
new MetaDocument(...$meta)
5961
);

src/Endpoint/Index.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public function handle(Context $context, ResourceType $resourceType): ResponseIn
112112
),
113113
new Structure\Included(...$included),
114114
new Structure\Link\SelfLink($this->buildUrl($context->getRequest())),
115+
$this->buildJsonApiObject($context),
115116
...$meta
116117
)
117118
);
@@ -133,6 +134,8 @@ private function buildUrl(Request $request, array $overrideParams = []): string
133134
}
134135
}
135136

137+
ksort($queryParams);
138+
136139
$queryString = http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986);
137140

138141
return $selfUrl.($queryString ? '?'.$queryString : '');

src/Endpoint/Show.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function handle(Context $context, ResourceType $resourceType, $model): Re
4343
new CompoundDocument(
4444
$primary[0],
4545
new Included(...$included),
46+
$this->buildJsonApiObject($context),
4647
...$this->buildMeta($context)
4748
)
4849
);

tests/MockAdapter.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@ public function find($query, string $id)
4545

4646
public function get($query): array
4747
{
48-
return array_values($this->models);
48+
$results = array_values($this->models);
49+
50+
if (isset($query->paginate)) {
51+
$results = array_slice($results, $query->paginate['offset'], $query->paginate['limit']);
52+
}
53+
54+
return $results;
4955
}
5056

5157
public function getId($model): string
@@ -124,7 +130,7 @@ public function sortByAttribute($query, Attribute $attribute, string $direction)
124130

125131
public function paginate($query, int $limit, int $offset): void
126132
{
127-
$query->paginate[] = [$limit, $offset];
133+
$query->paginate = compact('limit', 'offset');
128134
}
129135

130136
public function load(array $models, array $relationships, $scope, bool $linkage): void

tests/specification/FetchingResourcesTest.php

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212
namespace Tobyz\Tests\JsonApiServer\specification;
1313

14+
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
1415
use Tobyz\JsonApiServer\JsonApi;
1516
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
1617
use Tobyz\Tests\JsonApiServer\MockAdapter;
1718

1819
/**
19-
* @see https://jsonapi.org/format/#fetching-resources
20+
* @see https://jsonapi.org/format/1.1/#fetching-resources
2021
*/
2122
class FetchingResourcesTest extends AbstractTestCase
2223
{
@@ -25,50 +26,80 @@ class FetchingResourcesTest extends AbstractTestCase
2526
*/
2627
private $api;
2728

28-
/**
29-
* @var MockAdapter
30-
*/
31-
private $adapter;
32-
3329
public function setUp(): void
3430
{
3531
$this->api = new JsonApi('http://example.com');
36-
37-
$this->adapter = new MockAdapter();
3832
}
3933

4034
public function test_data_for_resource_collection_is_array_of_resource_objects()
4135
{
42-
$this->markTestIncomplete();
36+
$adapter = new MockAdapter([
37+
(object) ['id' => '1'],
38+
(object) ['id' => '2'],
39+
]);
40+
41+
$this->api->resourceType('articles', $adapter);
42+
43+
$response = $this->api->handle(
44+
$this->buildRequest('GET', '/articles')
45+
);
46+
47+
$this->assertJsonApiDocumentSubset([
48+
'data' => [
49+
['type' => 'articles', 'id' => '1'],
50+
['type' => 'articles', 'id' => '2'],
51+
]
52+
], $response->getBody());
4353
}
4454

4555
public function test_data_for_empty_resource_collection_is_empty_array()
4656
{
47-
$this->markTestIncomplete();
57+
$this->api->resourceType('articles', new MockAdapter());
58+
59+
$response = $this->api->handle(
60+
$this->buildRequest('GET', '/articles')
61+
);
62+
63+
$data = json_decode($response->getBody(), true)['data'] ?? null;
64+
65+
$this->assertIsArray($data);
66+
$this->assertEmpty($data);
4867
}
4968

5069
public function test_data_for_individual_resource_is_resource_object()
5170
{
52-
$this->markTestIncomplete();
71+
$adapter = new MockAdapter([
72+
(object) ['id' => '1'],
73+
]);
74+
75+
$this->api->resourceType('articles', $adapter);
76+
77+
$response = $this->api->handle(
78+
$this->buildRequest('GET', '/articles/1')
79+
);
80+
81+
$this->assertJsonApiDocumentSubset([
82+
'data' => ['type' => 'articles', 'id' => '1'],
83+
], $response->getBody());
5384
}
5485

5586
public function test_not_found_error_if_resource_type_does_not_exist()
5687
{
57-
$this->markTestIncomplete();
88+
$this->expectException(ResourceNotFoundException::class);
89+
90+
$this->api->handle(
91+
$this->buildRequest('GET', '/articles/1')
92+
);
5893
}
5994

6095
public function test_not_found_error_if_resource_does_not_exist()
6196
{
62-
$this->markTestIncomplete();
63-
}
97+
$this->expectException(ResourceNotFoundException::class);
6498

65-
public function test_resource_collection_document_contains_self_link()
66-
{
67-
$this->markTestIncomplete();
68-
}
99+
$this->api->resourceType('articles', new MockAdapter());
69100

70-
public function test_resource_document_contains_self_link()
71-
{
72-
$this->markTestIncomplete();
101+
$this->api->handle(
102+
$this->buildRequest('GET', '/articles/404')
103+
);
73104
}
74105
}

tests/specification/JsonApiTest.php renamed to tests/specification/JsonApiObjectTest.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,32 @@
1616
use Tobyz\Tests\JsonApiServer\MockAdapter;
1717

1818
/**
19-
* @see https://jsonapi.org/format/#document-jsonapi-object
19+
* @see https://jsonapi.org/format/1.1/#document-jsonapi-object
2020
*/
21-
class JsonApiTest extends AbstractTestCase
21+
class JsonApiObjectTest extends AbstractTestCase
2222
{
2323
/**
2424
* @var JsonApi
2525
*/
2626
private $api;
2727

28-
/**
29-
* @var MockAdapter
30-
*/
31-
private $adapter;
32-
3328
public function setUp(): void
3429
{
3530
$this->api = new JsonApi('http://example.com');
3631

37-
$this->adapter = new MockAdapter();
32+
$this->api->resourceType('articles', new MockAdapter());
3833
}
3934

40-
public function test_document_includes_jsonapi_member_with_version_1_0()
35+
public function test_document_includes_jsonapi_member_with_version_1_1()
4136
{
42-
$this->markTestIncomplete();
37+
$response = $this->api->handle(
38+
$this->buildRequest('GET', '/articles')
39+
);
40+
41+
$this->assertJsonApiDocumentSubset([
42+
'jsonapi' => [
43+
'version' => '1.1',
44+
],
45+
], $response->getBody());
4346
}
4447
}

tests/specification/OffsetPaginationTest.php

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
namespace Tobyz\Tests\JsonApiServer\specification;
1313

1414
use Tobyz\JsonApiServer\JsonApi;
15+
use Tobyz\JsonApiServer\Schema\Type;
1516
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
1617
use Tobyz\Tests\JsonApiServer\MockAdapter;
1718

1819
/**
19-
* @see https://jsonapi.org/format/1.0/#fetching-pagination
20-
* @todo Create a profile for offset pagination strategy
20+
* @see https://jsonapi.org/format/1.1/#fetching-pagination
2121
*/
2222
class OffsetPaginationTest extends AbstractTestCase
2323
{
@@ -26,60 +26,84 @@ class OffsetPaginationTest extends AbstractTestCase
2626
*/
2727
private $api;
2828

29-
/**
30-
* @var MockAdapter
31-
*/
32-
private $adapter;
33-
3429
public function setUp(): void
3530
{
3631
$this->api = new JsonApi('http://example.com');
3732

38-
$this->adapter = new MockAdapter();
33+
$adapter = new MockAdapter(
34+
array_map(function ($i) {
35+
return (object) ['id' => (string) $i];
36+
}, range(1, 100))
37+
);
38+
39+
$this->api->resourceType('articles', $adapter, function (Type $type) {
40+
$type->paginate(20);
41+
});
3942
}
4043

4144
public function test_can_request_limit_on_resource_collection()
4245
{
43-
$this->markTestIncomplete();
46+
$response = $this->api->handle(
47+
$this->buildRequest('GET', '/articles')
48+
->withQueryParams(['page' => ['limit' => '10']])
49+
);
50+
51+
$data = json_decode($response->getBody(), true)['data'] ?? null;
52+
53+
$this->assertCount(10, $data);
4454
}
4555

4656
public function test_can_request_offset_on_resource_collection()
4757
{
48-
$this->markTestIncomplete();
49-
}
58+
$response = $this->api->handle(
59+
$this->buildRequest('GET', '/articles')
60+
->withQueryParams(['page' => ['offset' => '5']])
61+
);
5062

51-
public function test_first_pagination_link_is_correct()
52-
{
53-
$this->markTestIncomplete();
54-
}
63+
$data = json_decode($response->getBody(), true)['data'] ?? null;
5564

56-
public function test_last_pagination_link_is_correct()
57-
{
58-
$this->markTestIncomplete();
65+
$this->assertEquals('6', $data[0]['id'] ?? null);
5966
}
6067

61-
public function test_next_pagination_link_is_correct()
68+
public function test_pagination_links_are_correct_and_retain_query_parameters()
6269
{
63-
$this->markTestIncomplete();
70+
$response = $this->api->handle(
71+
$this->buildRequest('GET', '/articles')
72+
->withQueryParams([
73+
'page' => ['offset' => '40'],
74+
'otherParam' => 'value',
75+
])
76+
);
77+
78+
$links = json_decode($response->getBody(), true)['links'] ?? null;
79+
80+
$this->assertEquals('/articles?otherParam=value', $links['first'] ?? null);
81+
$this->assertEquals('/articles?otherParam=value&page%5Boffset%5D=80', $links['last'] ?? null);
82+
$this->assertEquals('/articles?otherParam=value&page%5Boffset%5D=60', $links['next'] ?? null);
83+
$this->assertEquals('/articles?otherParam=value&page%5Boffset%5D=20', $links['prev'] ?? null);
6484
}
6585

6686
public function test_next_pagination_link_is_not_included_on_last_page()
6787
{
68-
$this->markTestIncomplete();
69-
}
88+
$response = $this->api->handle(
89+
$this->buildRequest('GET', '/articles')
90+
->withQueryParams(['page' => ['offset' => '80']])
91+
);
7092

71-
public function test_prev_pagination_link_is_correct()
72-
{
73-
$this->markTestIncomplete();
74-
}
93+
$links = json_decode($response->getBody(), true)['links'] ?? null;
7594

76-
public function test_prev_pagination_link_is_not_included_on_last_page()
77-
{
78-
$this->markTestIncomplete();
95+
$this->assertNull($links['next'] ?? null);
7996
}
8097

81-
public function test_pagination_links_retain_other_query_parameters()
98+
public function test_prev_pagination_link_is_not_included_on_first_page()
8299
{
83-
$this->markTestIncomplete();
100+
$response = $this->api->handle(
101+
$this->buildRequest('GET', '/articles')
102+
->withQueryParams(['page' => ['offset' => '0']])
103+
);
104+
105+
$links = json_decode($response->getBody(), true)['links'] ?? null;
106+
107+
$this->assertNull($links['prev'] ?? null);
84108
}
85109
}

0 commit comments

Comments
 (0)