Skip to content

Commit e851778

Browse files
committed
Resource spec validation done
1 parent c3b505c commit e851778

File tree

13 files changed

+501
-160
lines changed

13 files changed

+501
-160
lines changed

resources/lang/en/errors.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@
9494
'code' => '',
9595
],
9696

97+
'member_field_not_supported' => [
98+
'title' => 'Non-Compliant JSON API Document',
99+
'detail' => 'The field :field is not a supported :type.',
100+
'code' => '',
101+
],
102+
97103
'resource_type_not_supported' => [
98104
'title' => 'Not Supported',
99105
'detail' => 'Resource type :type is not supported by this endpoint.',

resources/lang/nl/errors.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@
9494
'code' => '',
9595
],
9696

97+
'member_field_not_supported' => [
98+
'title' => 'Non-Compliant JSON API Document',
99+
'detail' => 'Het veld :field is geen ondersteund :type.',
100+
'code' => '',
101+
],
102+
97103
'resource_type_not_supported' => [
98104
'title' => 'Niet Ondersteund',
99105
'detail' => 'Resource type :type wordt niet ondersteund door dit endpoint.',

src/Core/Document/ErrorList.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ public function __construct(Error ...$errors)
8383
$this->stack = $errors;
8484
}
8585

86+
/**
87+
* @return void
88+
*/
89+
public function __clone()
90+
{
91+
$this->stack = array_map(fn($error) => clone $error, $this->stack);
92+
}
93+
8694
/**
8795
* Add errors.
8896
*

src/Spec/Builder.php

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
namespace LaravelJsonApi\Spec;
2121

2222
use Illuminate\Pipeline\Pipeline;
23+
use LogicException;
2324
use function json_decode;
2425

2526
class Builder
@@ -33,7 +34,12 @@ class Builder
3334
/**
3435
* @var string|null
3536
*/
36-
private ?string $expects = null;
37+
private ?string $expectedType = null;
38+
39+
/**
40+
* @var string|null
41+
*/
42+
private ?string $expectedId = null;
3743

3844
/**
3945
* Builder constructor.
@@ -46,14 +52,16 @@ public function __construct(Pipeline $pipeline)
4652
}
4753

4854
/**
49-
* Expect the supplied resource type.
55+
* Expect the supplied resource type and id.
5056
*
5157
* @param string $resourceType
58+
* @param string|null $resourceId
5259
* @return $this
5360
*/
54-
public function expects(string $resourceType): self
61+
public function expects(string $resourceType, ?string $resourceId): self
5562
{
56-
$this->expects = $resourceType;
63+
$this->expectedType = $resourceType;
64+
$this->expectedId = $resourceId;
5765

5866
return $this;
5967
}
@@ -72,20 +80,30 @@ public function build($json): Document
7280
throw new \InvalidArgumentException('Expecting a string or object.');
7381
}
7482

75-
$pipes = [
76-
Validators\DataValidator::class,
77-
Validators\TypeValidator::class,
78-
Validators\ClientIdValidator::class,
79-
Validators\FieldsValidator::class,
80-
Validators\AttributesValidator::class,
81-
Validators\RelationshipsValidator::class,
82-
Validators\RelationshipValidator::class,
83-
];
84-
8583
return $this->pipeline
86-
->send(new Document($json, $this->expects))
87-
->through($pipes)
84+
->send(new Document($json, $this->expectedType, $this->expectedId))
85+
->through($this->pipes())
8886
->via('validate')
8987
->thenReturn();
9088
}
89+
90+
/**
91+
* @return string[]
92+
*/
93+
private function pipes(): array
94+
{
95+
if ($this->expectedType) {
96+
return [
97+
Validators\DataValidator::class,
98+
Validators\TypeValidator::class,
99+
Validators\ClientIdValidator::class,
100+
Validators\IdValidator::class,
101+
Validators\FieldsValidator::class,
102+
Validators\AttributesValidator::class,
103+
Validators\RelationshipsValidator::class,
104+
];
105+
}
106+
107+
throw new LogicException('Cannot determine validation pipes.');
108+
}
91109
}

src/Spec/Document.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ class Document
3434
*/
3535
private string $resourceType;
3636

37+
/**
38+
* @var string|null
39+
*/
40+
private ?string $resourceId;
41+
3742
/**
3843
* @var string|null
3944
*/
@@ -49,12 +54,18 @@ class Document
4954
*
5055
* @param object $document
5156
* @param string $resourceType
57+
* @param string|null $resourceId
5258
* @param string|null $relation
5359
*/
54-
public function __construct(object $document, string $resourceType, string $relation = null)
55-
{
60+
public function __construct(
61+
object $document,
62+
string $resourceType,
63+
?string $resourceId,
64+
string $relation = null
65+
) {
5666
$this->document = $document;
5767
$this->resourceType = $resourceType;
68+
$this->resourceId = $resourceId;
5869
$this->relation = $relation;
5970
$this->errors = new ErrorList();
6071
}
@@ -87,6 +98,14 @@ public function type(): string
8798
return $this->resourceType;
8899
}
89100

101+
/**
102+
* @return string|null
103+
*/
104+
public function id(): ?string
105+
{
106+
return $this->resourceId;
107+
}
108+
90109
/**
91110
* Get the relation that the document represents.
92111
*
@@ -154,7 +173,7 @@ public function invalid(): bool
154173
*/
155174
public function toBase(): object
156175
{
157-
return $this->document;
176+
return clone $this->document;
158177
}
159178

160179
}

src/Spec/Translator.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Illuminate\Contracts\Translation\Translator as IlluminateTranslator;
2323
use Illuminate\Http\Response;
2424
use LaravelJsonApi\Core\Document\Error;
25+
use LaravelJsonApi\Core\Support\Str;
2526
use Neomerx\JsonApi\Contracts\Document\ErrorInterface;
2627
use Neomerx\JsonApi\Document\Error as NeomerxError;
2728

@@ -112,7 +113,6 @@ public function memberNotIdentifier(string $path, string $member): Error
112113
->setSourcePointer($this->pointer($path, $member));
113114
}
114115

115-
116116
/**
117117
* Create an error for when a member has a field that is not allowed.
118118
*
@@ -131,6 +131,26 @@ public function memberFieldNotAllowed(string $path, string $member, string $fiel
131131
->setSourcePointer($this->pointer($path, $member));
132132
}
133133

134+
/**
135+
* Create an error for when a member has a field that is not supported.
136+
*
137+
* @param string $path
138+
* @param string $member
139+
* @param string $field
140+
* @return Error
141+
*/
142+
public function memberFieldNotSupported(string $path, string $member, string $field): Error
143+
{
144+
$type = $this->translator->get(Str::singular($member));
145+
146+
return Error::make()
147+
->setStatus(Response::HTTP_BAD_REQUEST)
148+
->setCode($this->trans('member_field_not_supported', 'code'))
149+
->setTitle($this->trans('member_field_not_supported', 'title'))
150+
->setDetail($this->trans('member_field_not_supported', 'detail', compact('type', 'field')))
151+
->setSourcePointer($this->pointer($path, $member));
152+
}
153+
134154
/**
135155
* Create an error for a member that must be a string.
136156
*

src/Spec/Validators/AttributesValidator.php

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,20 @@
1919

2020
namespace LaravelJsonApi\Spec\Validators;
2121

22+
use LaravelJsonApi\Contracts\Schema\Attribute;
23+
use LaravelJsonApi\Core\Document\ErrorList;
2224
use LaravelJsonApi\Spec\Document;
25+
use LaravelJsonApi\Spec\Specification;
2326
use LaravelJsonApi\Spec\Translator;
2427

2528
class AttributesValidator
2629
{
2730

31+
/**
32+
* @var Specification
33+
*/
34+
private Specification $spec;
35+
2836
/**
2937
* @var Translator
3038
*/
@@ -33,10 +41,12 @@ class AttributesValidator
3341
/**
3442
* AttributesValidator constructor.
3543
*
44+
* @param Specification $spec
3645
* @param Translator $translator
3746
*/
38-
public function __construct(Translator $translator)
47+
public function __construct(Specification $spec, Translator $translator)
3948
{
49+
$this->spec = $spec;
4050
$this->translator = $translator;
4151
}
4252

@@ -49,28 +59,48 @@ public function __construct(Translator $translator)
4959
*/
5060
public function validate(Document $document, \Closure $next): Document
5161
{
52-
$data = $document->data;
62+
$data = $document->data ?? null;
5363

54-
if (property_exists($data, 'attributes') && $errors = $this->accept($data->attributes)) {
55-
$document->errors()->push(...$errors);
64+
if ($data && property_exists($data, 'attributes')) {
65+
$document->errors()->merge(
66+
$this->accept($document->type(), $data->attributes)
67+
);
5668
}
5769

5870
return $next($document);
5971
}
6072

6173
/**
62-
* @param $value
63-
* @return array|null
74+
* @param string $resourceType
75+
* @param $attributes
76+
* @return ErrorList
6477
*/
65-
private function accept($value): ?array
78+
private function accept(string $resourceType, $attributes): ErrorList
6679
{
67-
if (!is_object($value)) {
68-
return [$this->translator->memberNotObject('/data', 'attributes')];
80+
$errors = new ErrorList();
81+
82+
if (!is_object($attributes)) {
83+
return $errors->push(
84+
$this->translator->memberNotObject('/data', 'attributes')
85+
);
6986
}
7087

71-
return collect(['type', 'id'])
72-
->filter(fn($field) => property_exists($value, $field))
73-
->map(fn($field) => $this->translator->memberFieldNotAllowed('/data', 'attributes', $field))
74-
->all();
88+
/** Type and id are not allowed in attributes */
89+
$errors->push(...collect(['type', 'id'])->filter(fn($name) => property_exists($attributes, $name))->map(
90+
fn($name) => $this->translator->memberFieldNotAllowed('/data', 'attributes', $name)
91+
));
92+
93+
$fields = collect($this->spec->fields($resourceType))
94+
->whereInstanceOf(Attribute::class)
95+
->map(fn($field) => $field->name())
96+
->values();
97+
98+
$actual = collect(get_object_vars($attributes))
99+
->forget(['type', 'id'])
100+
->keys();
101+
102+
return $errors->push(...$actual->diff($fields)->map(
103+
fn($name) => $this->translator->memberFieldNotSupported('/data', 'attributes', $name)
104+
));
75105
}
76106
}

src/Spec/Validators/ClientIdValidator.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public function __construct(Specification $spec, Translator $translator)
5858
*/
5959
public function validate(Document $document, \Closure $next): Document
6060
{
61+
if ($document->id()) {
62+
return $next($document);
63+
}
64+
6165
$data = $document->data;
6266

6367
if (!property_exists($data, 'id')) {

0 commit comments

Comments
 (0)