Skip to content

Commit e1c6901

Browse files
bertramakerstobyzerner
authored andcommitted
Add array type
1 parent 9d69780 commit e1c6901

File tree

2 files changed

+330
-0
lines changed

2 files changed

+330
-0
lines changed

Diff for: src/Schema/Type/Arr.php

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Tobyz\JsonApiServer\Schema\Type;
4+
5+
class Arr implements TypeInterface
6+
{
7+
private int $minItems = 0;
8+
private ?int $maxItems = null;
9+
private bool $uniqueItems = false;
10+
private ?TypeInterface $items = null;
11+
12+
public static function make(): static
13+
{
14+
return new static();
15+
}
16+
17+
public function serialize(mixed $value): mixed
18+
{
19+
return $value;
20+
}
21+
22+
public function deserialize(mixed $value): mixed
23+
{
24+
return $value;
25+
}
26+
27+
public function validate(mixed $value, callable $fail): void
28+
{
29+
if (!is_array($value)) {
30+
$fail('must be an array');
31+
return;
32+
}
33+
34+
if (count($value) < $this->minItems) {
35+
$fail(sprintf('must contain at least %d values', $this->minItems));
36+
}
37+
38+
if ($this->maxItems !== null && count($value) > $this->maxItems) {
39+
$fail(sprintf('must contain no more than %d values', $this->maxItems));
40+
}
41+
42+
if ($this->uniqueItems && count($value) !== count(array_unique($value))) {
43+
$fail('must contain unique values');
44+
}
45+
46+
if ($this->items) {
47+
foreach ($value as $item) {
48+
$this->items->validate($item, $fail);
49+
}
50+
}
51+
}
52+
53+
public function schema(): array
54+
{
55+
return [
56+
'type' => 'array',
57+
'minItems' => $this->minItems,
58+
'maxItems' => $this->maxItems,
59+
'uniqueItems' => $this->uniqueItems,
60+
'items' => $this->items?->schema(),
61+
];
62+
}
63+
64+
public function minItems(int $minItems): static
65+
{
66+
$this->minItems = $minItems;
67+
68+
return $this;
69+
}
70+
71+
public function maxItems(?int $maxItems): static
72+
{
73+
$this->maxItems = $maxItems;
74+
75+
return $this;
76+
}
77+
78+
public function uniqueItems(bool $uniqueItems = true): static
79+
{
80+
$this->uniqueItems = $uniqueItems;
81+
82+
return $this;
83+
}
84+
85+
public function items(?TypeInterface $type): static
86+
{
87+
$this->items = $type;
88+
89+
return $this;
90+
}
91+
}

Diff for: tests/feature/ArrTest.php

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
<?php
2+
3+
namespace Tobyz\Tests\JsonApiServer\feature;
4+
5+
use Tobyz\JsonApiServer\Endpoint\Create;
6+
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
7+
use Tobyz\JsonApiServer\JsonApi;
8+
use Tobyz\JsonApiServer\Schema\Field\Attribute;
9+
use Tobyz\JsonApiServer\Schema\Type\Arr;
10+
use Tobyz\JsonApiServer\Schema\Type\Str;
11+
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
12+
use Tobyz\Tests\JsonApiServer\MockResource;
13+
14+
class ArrTest extends AbstractTestCase
15+
{
16+
private JsonApi $api;
17+
18+
public function setUp(): void
19+
{
20+
$this->api = new JsonApi();
21+
}
22+
23+
public function test_validates_array()
24+
{
25+
$this->api->resource(
26+
new MockResource(
27+
'customers',
28+
endpoints: [Create::make()],
29+
fields: [
30+
Attribute::make('featureToggles')
31+
->type(Arr::make())
32+
->writable(),
33+
],
34+
),
35+
);
36+
37+
$this->expectException(UnprocessableEntityException::class);
38+
39+
$this->api->handle(
40+
$this->buildRequest('POST', '/customers')->withParsedBody([
41+
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => 1]],
42+
]),
43+
);
44+
}
45+
46+
public function test_invalid_min_length()
47+
{
48+
$this->api->resource(
49+
new MockResource(
50+
'customers',
51+
endpoints: [Create::make()],
52+
fields: [
53+
Attribute::make('featureToggles')
54+
->type(Arr::make()->minItems(1))
55+
->writable(),
56+
],
57+
),
58+
);
59+
60+
$this->expectException(UnprocessableEntityException::class);
61+
62+
$this->api->handle(
63+
$this->buildRequest('POST', '/customers')->withParsedBody([
64+
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => []]],
65+
]),
66+
);
67+
}
68+
69+
public function test_invalid_max_length()
70+
{
71+
$this->api->resource(
72+
new MockResource(
73+
'customers',
74+
endpoints: [Create::make()],
75+
fields: [
76+
Attribute::make('featureToggles')
77+
->type(Arr::make()->maxItems(1))
78+
->writable(),
79+
],
80+
),
81+
);
82+
83+
$this->expectException(UnprocessableEntityException::class);
84+
85+
$this->api->handle(
86+
$this->buildRequest('POST', '/customers')->withParsedBody([
87+
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => [1, 2]]],
88+
]),
89+
);
90+
}
91+
92+
public function test_invalid_uniqueness()
93+
{
94+
$this->api->resource(
95+
new MockResource(
96+
'customers',
97+
endpoints: [Create::make()],
98+
fields: [
99+
Attribute::make('featureToggles')
100+
->type(Arr::make()->uniqueItems())
101+
->writable(),
102+
],
103+
),
104+
);
105+
106+
$this->expectException(UnprocessableEntityException::class);
107+
108+
$this->api->handle(
109+
$this->buildRequest('POST', '/customers')->withParsedBody([
110+
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => [1, 1]]],
111+
]),
112+
);
113+
}
114+
115+
public function test_valid_items_constraints()
116+
{
117+
$this->api->resource(
118+
new MockResource(
119+
'customers',
120+
endpoints: [Create::make()],
121+
fields: [
122+
Attribute::make('featureToggles')
123+
->type(
124+
Arr::make()
125+
->minItems(2)
126+
->maxItems(4)
127+
->uniqueItems(),
128+
)
129+
->writable(),
130+
],
131+
),
132+
);
133+
134+
$response = $this->api->handle(
135+
$this->buildRequest('POST', '/customers')->withParsedBody([
136+
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => [1, 2, 3]]],
137+
]),
138+
);
139+
140+
$this->assertJsonApiDocumentSubset(
141+
['data' => ['attributes' => ['featureToggles' => [1, 2, 3]]]],
142+
$response->getBody(),
143+
true,
144+
);
145+
}
146+
147+
public function test_invalid_items()
148+
{
149+
$this->api->resource(
150+
new MockResource(
151+
'customers',
152+
endpoints: [Create::make()],
153+
fields: [
154+
Attribute::make('featureToggles')
155+
->type(Arr::make()->items(Str::make()->enum(['valid'])))
156+
->writable(),
157+
],
158+
),
159+
);
160+
161+
$this->expectException(UnprocessableEntityException::class);
162+
163+
$this->api->handle(
164+
$this->buildRequest('POST', '/customers')->withParsedBody([
165+
'data' => [
166+
'type' => 'customers',
167+
'attributes' => ['featureToggles' => ['valid', 'invalid']],
168+
],
169+
]),
170+
);
171+
}
172+
173+
public function test_valid_items()
174+
{
175+
$this->api->resource(
176+
new MockResource(
177+
'customers',
178+
endpoints: [Create::make()],
179+
fields: [
180+
Attribute::make('featureToggles')
181+
->type(Arr::make()->items(Str::make()->enum(['valid1', 'valid2'])))
182+
->writable(),
183+
],
184+
),
185+
);
186+
187+
$response = $this->api->handle(
188+
$this->buildRequest('POST', '/customers')->withParsedBody([
189+
'data' => [
190+
'type' => 'customers',
191+
'attributes' => ['featureToggles' => ['valid1', 'valid2']],
192+
],
193+
]),
194+
);
195+
196+
$this->assertJsonApiDocumentSubset(
197+
['data' => ['attributes' => ['featureToggles' => ['valid1', 'valid2']]]],
198+
$response->getBody(),
199+
true,
200+
);
201+
}
202+
203+
public function test_schema()
204+
{
205+
$this->assertEquals(
206+
[
207+
'type' => 'array',
208+
'minItems' => 0,
209+
'maxItems' => null,
210+
'uniqueItems' => false,
211+
'items' => null,
212+
],
213+
Arr::make()->schema(),
214+
);
215+
216+
$this->assertEquals(
217+
[
218+
'type' => 'array',
219+
'minItems' => 1,
220+
'maxItems' => 10,
221+
'uniqueItems' => true,
222+
'items' => [
223+
'type' => 'string',
224+
'enum' => ['valid1', 'valid2'],
225+
'minLength' => 0,
226+
'maxLength' => null,
227+
'pattern' => null,
228+
'format' => null,
229+
],
230+
],
231+
Arr::make()
232+
->minItems(1)
233+
->maxItems(10)
234+
->uniqueItems()
235+
->items(Str::make()->enum(['valid1', 'valid2']))
236+
->schema(),
237+
);
238+
}
239+
}

0 commit comments

Comments
 (0)