8000 [Feature] Improve spec validation of resource object field names · AxonDivisionDev/laravel-json-api@f30e4a9 · GitHub
[go: up one dir, main page]

Skip to content

Commit f30e4a9

Browse files
committed
[Feature] Improve spec validation of resource object field names
Will now reject a resource that has `type` or `id` members in its attributes or relationship members. Will now reject a resource that has a field that is common to both its attributes and relationships members.
1 parent ece8274 commit f30e4a9

File tree

8 files changed

+247
-27
lines changed

8 files changed

+247
-27
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ request.
1313
- Added an `existingRelationships` method to the abstract validators class. Child classes can overload
1414
this method if they need the validator to have access to any existing relationship values for an
1515
update request.
16+
- JSON API specification validation will now fail if the `attributes` or `relationships` members have
17+
`type` or `id` fields.
18+
- JSON API specification validation will now fail if the `attributes` and `relationships` members have
19+
common field names, as field names share a common namespace.
1620

1721
### Changed
1822
- [#248](https://github.com/cloudcreativity/laravel-json-api/pull/248)

resources/lang/en/errors.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@
5858
'code' => '',
5959
],
6060

61+
'member_field_not_allowed' => [
62+
'title' => 'Non-Compliant JSON API Document',
63+
'detail' => "The member :member cannot have a :field field.",
64+
'code' => '',
65+
],
66+
6167
'resource_type_not_supported' => [
6268
'title' => 'Not Supported',
6369
'detail' => "Resource type :type is not supported by this endpoint.",
@@ -88,6 +94,12 @@
8894
'code' => '',
8995
],
9096

97+
'resource_field_exists_in_attributes_and_relationships' => [
98+
'title' => 'Non-Compliant JSON API Document',
99+
'detail' => 'The :field field cannot exist as an attribute and a relationship.',
100+
'code' => '',
101+
],
102+
91103
'resource_invalid' => [
92104
'title' => 'Unprocessable Entity',
93105
'detail' => 'The document was well-formed but contains semantic errors.',

src/Document/ResourceObject.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
use Illuminate\Contracts\Support\Arrayable;
2323
use Illuminate\Support\Arr;
2424
use Illuminate\Support\Collection;
25-
use Illuminate\Support\Str;
2625

2726
class ResourceObject implements Arrayable, \IteratorAggregate, \JsonSerializable, \ArrayAccess
2827
{

src/Validation/ErrorTranslator.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,27 @@ public function memberNotObject(string $path, string $member): ErrorInterface
8585
);
8686
}
8787

88+
/**
89+
* Create an error for when a member has a field that is not allowed.
90+
*
91+
* @param string $path
92+
* @param string $member
93+
* @param string $field
94+
* @return ErrorInterface
95+
*/
96+
public function memberFieldNotAllowed(string $path, string $member, string $field): ErrorInterface
97+
{
98+
return new Error(
99+
null,
100+
null,
101+
Response::HTTP_BAD_REQUEST,
102+
$this->trans('member_field_not_allowed', 'code'),
103+
$this->trans('member_field_not_allowed', 'title'),
104+
$this->trans('member_field_not_allowed', 'detail', compact('member', 'field')),
105+
$this->pointer($path, $member)
106+
);
107+
}
108+
88109
/**
89110
* Create an error for a member that must be a string.
90111
*
@@ -230,6 +251,29 @@ public function resourceDoesNotExist(string $path): ErrorInterface
230251
);
231252
}
232253

254+
/**
255+
* Create an error for when a resource field exists in both the attributes and relationships members.
256+
*
257+
* @param string $field
258+
* @param string $path
259+
* @return ErrorInterface
260+
*/
261+
public function resourceFieldExistsInAttributesAndRelationships(
262+
string $field,
263+
string $path = '/data'
264+
): ErrorInterface
265+
{
266+
return new Error(
267+
null,
268+
null,
269+
Response::HTTP_BAD_REQUEST,
270+
$this->trans('resource_field_exists_in_attributes_and_relationships', 'code'),
271+
$this->trans('resource_field_exists_in_attributes_and_relationships', 'title'),
272+
$this->trans('resource_field_exists_in_attributes_and_relationships', 'detail', compact('field')),
273+
$this->pointer($path)
274+
);
275+
}
276+
233277
/**
234278
* Create an error for an invalid resource.
235279
*

src/Validation/Spec/AbstractValidator.php

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,16 @@ protected function dataHas($key): bool
315315
* Get a value from the document data object use dot notation.
316316
*
317317
* @param string|array $key
318+
* @param mixed $default
318319
* @return mixed|null
319320
*/
320-
protected function dataGet($key)
321+
protected function dataGet($key, $default = null)
321322
{
322323
if (!isset($this->document->data)) {
323-
return null;
324+
return $default;
324325
}
325326

326-
return data_get($this->document->data, $key);
327+
return data_get($this->document->data, $key, $default);
327328
}
328329

329330
/**
@@ -362,6 +363,20 @@ protected function memberNotObject(string $path, string $member): void
362363
$this->errors->add($this->translator->memberNotObject($path, $member));
363364
}
364365

366+
/**
367+
* Add errors for when a member has a field that is not allowed.
368+
*
369+
* @param string $path
370+
* @param string $member
371+
* @param iterable $fields
372+
*/
373+
protected function memberFieldsNotAllowed(string $path, string $member, iterable $fields): void
374+
{
375+
foreach ($fields as $field) {
376+
$this->errors->add($this->translator->memberFieldNotAllowed($path, $member, $field));
377+
}
378+
}
379+
365380
/**
366381
* Add an error for a member that must be a string.
367382
*
@@ -446,4 +461,19 @@ protected function resourceDoesNotExist(string $path): void
446461
$this->errors->add($this->translator->resourceDoesNotExist($path));
447462
}
448463

464+
/**
465+
* Add errors for when a resource field exists in both the attributes and relationships members.
466+
*
467+
* @param iterable $fields
468+
* @return void
469+
*/
470+
protected function resourceFieldsExistInAttributesAndRelationships(iterable $fields): void
471+
{
472+
foreach ($fields as $field) {
473+
$this->errors->add(
474+
$this->translator->resourceFieldExistsInAttributesAndRelationships($field)
475+
);
476+
}
477+
}
478+
449479
}

src/Validation/Spec/CreateResourceValidator.php

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,22 +68,7 @@ protected function validate(): bool
6868
return false;
6969
}
7070

71-
/** Validate the resource... */
72-
$valid = true;
73-
74-
if (!$this->validateTypeAndId()) {
75-
$valid = false;
76-
}
77-
78-
if ($this->dataHas('attributes') && !$this->validateAttributes()) {
79-
$valid = false;
80-
}
81-
82-
if ($this->dataHas('relationships') && !$this->validateRelationships()) {
83-
$valid = false;
84-
}
85-
86-
return $valid;
71+
return $this->validateResource();
8772
}
8873

8974
/**
@@ -108,6 +93,24 @@ protected function validateData(): bool
10893
return true;
10994
}
11095

96+
/**
97+
* Validate the resource object.
98+
*
99+
* @return bool
100+
*/
101+
protected function validateResource(): bool
102+
{
103+
$identifier = $this->validateTypeAndId();
104+
$attributes = $this->validateAttributes();
105+
$relationships = $this->validateRelationships();
106+
107+
if ($attributes && $relationships) {
108+
return $this->validateAllFields() && $identifier;
109+
}
110+
111+
return $identifier && $attributes && $relationships;
112+
}
113+
111114
/**
112115
* Validate the resource type and id.
113116
*
@@ -177,14 +180,24 @@ protected function validateId(): bool
177180
*/
178181
protected function validateAttributes(): bool
179182
{
183+
if (!$this->dataHas('attributes')) {
184+
return true;
185+
}
186+
180187
$attrs = $this->dataGet('attributes');
181188

182189
if (!is_object($attrs)) {
183190
$this->memberNotObject('/data', 'attributes');
184191
return false;
185192
}
186193

187-
return true;
194+
$disallowed = collect(['type', 'id'])->filter(function ($field) use ($attrs) {
195+
return property_exists($attrs, $field);
196+
});
197+
198+
$this->memberFieldsNotAllowed('/data', 'attributes', $disallowed);
199+
200+
return $disallowed->isEmpty();
188201
}
189202

190203
/**
@@ -194,14 +207,23 @@ protected function validateAttributes(): bool
194207
*/
195208
protected function validateRelationships(): bool
196209
{
210+
if (!$this->dataHas('relationships')) {
211+
return true;
212+
}
213+
197214
$relationships = $this->dataGet('relationships');
198215

199216
if (!is_object($relationships)) {
200217
$this->memberNotObject('/data', 'relationships');
201218
return false;
202219
}
203220

204-
$valid = true;
221+
$disallowed = collect(['type', 'id'])->filter(function ($field) use ($relationships) {
222+
return property_exists($relationships, $field);
223+
});
224+
225+
$valid = $disallowed->isEmpty();
226+
$this->memberFieldsNotAllowed('/data', 'relationships', $disallowed);
205227

206228
foreach ($relationships as $field => $relation) {
207229
if (!$this->validateRelationship($relation, $field)) {
@@ -212,4 +234,22 @@ protected function validateRelationships(): bool
212234
return $valid;
213235
}
214236

237+
/**
238+
* Validate the resource's attributes and relationships collectively.
239+
*
240+
* @return bool
241+
*/
242+
protected function validateAllFields(): bool
243+
{
244+
$duplicates = collect(
245+
(array) $this->dataGet('attributes', [])
246+
)->intersectByKeys(
247+
(array) $this->dataGet('relationships', [])
248+
)->keys();
249+
250+
$this->resourceFieldsExistInAttributesAndRelationships($duplicates);
251+
252+
return $duplicates->isEmpty();
253+
}
254+
215255
}

0 commit comments

Comments
 (0)
0