Skip to content

Commit c0d9c89

Browse files
committed
Wire specification validation for create and update requests
1 parent 659deec commit c0d9c89

File tree

18 files changed

+684
-26
lines changed

18 files changed

+684
-26
lines changed

src/Contracts/Routing/Route.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?php
2-
/*
2+
/**
33
* Copyright 2020 Cloud Creativity Limited
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,6 +37,13 @@ public function resourceType(): string;
3737
*/
3838
public function modelOrResourceId();
3939

40+
/**
41+
* Get the resource id.
42+
*
43+
* @return string
44+
*/
45+
public function resourceId(): string;
46+
4047
/**
4148
* Get the schema for the current route.
4249
*

src/Contracts/Schema/Container.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ interface Container
3030
*/
3131
public function schemaFor(string $resourceType): Schema;
3232

33+
/**
34+
* Get a list of all the supported resource types.
35+
*
36+
* @return array
37+
*/
38+
public function types(): array;
39+
3340
/**
3441
* Get a list of model classes mapped to their resource classes.
3542
*

src/Core/Document/Error.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,16 @@ public function code(): ?string
222222
/**
223223
* Add an application-specific error code.
224224
*
225-
* @param string|null $code
225+
* @param string|int|null $code
226226
* @return $this
227227
*/
228-
public function setCode(?string $code): self
228+
public function setCode($code): self
229229
{
230-
$this->code = $code ?: null;
230+
if (!is_int($code) && !is_string($code) && !is_null($code)) {
231+
throw new InvalidArgumentException('Expecting an integer, string or null.');
232+
}
233+
234+
$this->code = !is_null($code) ? strval($code) : null;
231235

232236
return $this;
233237
}

src/Core/Document/ErrorList.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
namespace LaravelJsonApi\Core\Document;
2121

2222
use Countable;
23+
use Illuminate\Contracts\Support\Responsable;
24+
use Illuminate\Http\Response;
2325
use IteratorAggregate;
2426
use LaravelJsonApi\Contracts\Serializable;
2527
use LogicException;
2628
use function array_merge;
2729
use function collect;
2830

29-
class ErrorList implements Serializable, Countable, IteratorAggregate
31+
class ErrorList implements Serializable, Countable, IteratorAggregate, Responsable
3032
{
3133

3234
use Concerns\Serializable;
@@ -91,6 +93,33 @@ public function __clone()
9193
$this->stack = array_map(fn($error) => clone $error, $this->stack);
9294
}
9395

96+
/**
97+
* Get the most applicable HTTP status code.
98+
*
99+
* When a server encounters multiple problems for a single request, the most generally applicable HTTP error
100+
* code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate for multiple
101+
* 4xx errors or 500 Internal Server Error might be appropriate for multiple 5xx errors.
102+
*
103+
* @param int the default status to return, if there are no statuses.
104+
* @return int
105+
* @see https://jsonapi.org/format/#errors
106+
*/
107+
public function status(int $default = Response::HTTP_INTERNAL_SERVER_ERROR): int
108+
{
109+
$statuses = collect($this->stack)
110+
->map(fn(Error $error) => intval($error->status()))
111+
->filter()
112+
->unique();
113+
114+
if (2 > count($statuses)) {
115+
return $statuses->first() ?: $default;
116+
}
117+
118+
$only4xx = $statuses->every(fn(int $status) => 400 <= $status && 499 >= $status);
119+
120+
return $only4xx ? Response::HTTP_BAD_REQUEST : Response::HTTP_INTERNAL_SERVER_ERROR;
121+
}
122+
94123
/**
95124
* Add errors.
96125
*
@@ -170,4 +199,24 @@ public function jsonSerialize()
170199
return $this->stack;
171200
}
172201

202+
/**
203+
* @param $request
204+
* @return ErrorResponse
205+
*/
206+
public function prepareResponse($request): ErrorResponse
207+
{
208+
return new ErrorResponse($this);
209+
}
210+
211+
/**
212+
* @inheritDoc
213+
*/
214+
public function toResponse($request)
215+
{
216+
return $this
217+
->prepareResponse($request)
218+
->toResponse($request);
219+
}
220+
221+
173222
}

src/Core/Document/ErrorResponse.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
/**
3+
* Copyright 2020 Cloud Creativity Limited
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace LaravelJsonApi\Core\Document;
21+
22+
use Illuminate\Contracts\Support\Responsable;
23+
use LaravelJsonApi\Contracts\Serializable;
24+
25+
class ErrorResponse implements Serializable, Responsable
26+
{
27+
28+
use Concerns\Serializable;
29+
30+
/**
31+
* @var ErrorList
32+
*/
33+
private ErrorList $errors;
34+
35+
/**
36+
* @var int|null
37+
*/
38+
private ?int $status = null;
39+
40+
/**
41+
* @var array
42+
*/
43+
private array $headers = [];
44+
45+
/**
46+
* @var int
47+
*/
48+
private int $encodeOptions = 0;
49+
50+
/**
51+
* ErrorResponse constructor.
52+
*
53+
* @param ErrorList|Error|Error[] $errors
54+
*/
55+
public function __construct($errors)
56+
{
57+
$this->errors = ErrorList::cast($errors);
58+
}
59+
60+
/**
61+
* Set JSON encode options.
62+
*
63+
* @param int $options
64+
* @return $this
65+
*/
66+
public function withEncodeOptions(int $options): self
67+
{
68+
$this->encodeOptions = $options;
69+
70+
return $this;
71+
}
72+
73+
/**
74+
* Set response headers.
75+
*
76+
* @param array $headers
77+
* @return $this
78+
*/
79+
public function withHeaders(array $headers): self
80+
{
81+
$this->headers = $headers;
82+
83+
return $this;
84+
}
85+
86+
/**
87+
* Set the response status.
88+
*
89+
* This overrides the default status, which is derived from
90+
* the error list.
91+
*
92+
* @param int $status
93+
* @return $this
94+
*/
95+
public function withStatus(int $status): self
96+
{
97+
$this->status = $status;
98+
99+
return $this;
100+
}
101+
102+
/**
103+
* @inheritDoc
104+
*/
105+
public function toResponse($request)
106+
{
107+
return response(
108+
$this->toJson($this->encodeOptions),
109+
$this->status ?: $this->errors->status(),
110+
$this->headers
111+
);
112+
}
113+
114+
/**
115+
* @inheritDoc
116+
*/
117+
public function toArray()
118+
{
119+
return [
120+
'errors' => $this->errors->toArray(),
121+
];
122+
}
123+
124+
/**
125+
* @inheritDoc
126+
*/
127+
public function jsonSerialize()
128+
{
129+
return [
130+
'errors' => $this->errors,
131+
];
132+
}
133+
134+
}

src/Core/Schema/Container.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ public function schemaFor(string $resourceType): Schema
8484
throw new LogicException("No schema for JSON API resource type {$resourceType}.");
8585
}
8686

87+
/**
88+
* @inheritDoc
89+
*/
90+
public function types(): array
91+
{
92+
return array_keys($this->types);
93+
}
94+
8795
/**
8896
* @inheritDoc
8997
*/

src/Eloquent/Repository.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public function find(string $resourceId)
8585
*/
8686
public function exists(string $resourceId): bool
8787
{
88+
// @TODO check the resource id against a regex querying the database.
89+
8890
return $this
8991
->query()
9092
->whereResourceId($resourceId)

0 commit comments

Comments
 (0)