Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/Schema/Field/ArrayField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Tobyz\JsonApiServer\Schema\Field;

use Tobyz\JsonApiServer\Context;

class ArrayField extends Field
{
private int $minItems = 0;
private ?int $maxItems = null;
private bool $uniqueItems = false;
private ?Attribute $items = null;

public function __construct(string $name)
{
parent::__construct($name);

$this->validate(function (mixed $value, callable $fail, Context $context): void {
if ($value === null) {
return;
}

if (!is_array($value)) {
$fail('must be an array');
return;
}

if (count($value) < $this->minItems) {
$fail(sprintf('must contain at least %d values', $this->minItems));
}

if ($this->maxItems !== null && count($value) > $this->maxItems) {
$fail(sprintf('must contain no more than %d values', $this->maxItems));
}

if ($this->uniqueItems && $value !== array_unique($value)) {
$fail('must contain unique values');
}

if ($this->items) {
foreach ($value as $item) {
$this->items->validateValue($item, $fail, $context);
}
}
});
}

public function minItems(int $minItems): static
{
$this->minItems = $minItems;

return $this;
}

public function maxItems(?int $maxItems): static
{
$this->maxItems = $maxItems;

return $this;
}

public function uniqueItems(bool $uniqueItems = true): static
{
$this->uniqueItems = $uniqueItems;

return $this;
}

public function items(Attribute $schema): static
{
$this->items = $schema;

return $this;
}

public function getSchema(): array
{
return parent::getSchema() + [
'type' => 'array',
'minItems' => $this->minItems,
'maxItems' => $this->maxItems,
'uniqueItems' => $this->uniqueItems,
'items' => $this->items?->getSchema(),
];
}
}
237 changes: 237 additions & 0 deletions tests/feature/ArrayFieldTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<?php

namespace Tobyz\Tests\JsonApiServer\feature;

use Tobyz\JsonApiServer\Endpoint\Create;
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
use Tobyz\JsonApiServer\JsonApi;
use Tobyz\JsonApiServer\Schema\Field\ArrayField;
use Tobyz\JsonApiServer\Schema\Field\Str;
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
use Tobyz\Tests\JsonApiServer\MockResource;

class ArrayFieldTest extends AbstractTestCase
{
private JsonApi $api;

public function setUp(): void
{
$this->api = new JsonApi();
}

public function test_validates_array()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [ArrayField::make('featureToggles')->writable()],
),
);

$this->expectException(UnprocessableEntityException::class);

$this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => 1]],
]),
);
}

public function test_invalid_min_length()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [
ArrayField::make('featureToggles')
->minItems(1)
->writable(),
],
),
);

$this->expectException(UnprocessableEntityException::class);

$this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => []]],
]),
);
}

public function test_invalid_max_length()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [
ArrayField::make('featureToggles')
->maxItems(1)
->writable(),
],
),
);

$this->expectException(UnprocessableEntityException::class);

$this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => [1, 2]]],
]),
);
}

public function test_invalid_uniqueness()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [
ArrayField::make('featureToggles')
->uniqueItems()
->writable(),
],
),
);

$this->expectException(UnprocessableEntityException::class);

$this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => [1, 1]]],
]),
);
}

public function test_valid_items_constraints()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [
ArrayField::make('featureToggles')
->minItems(2)
->maxItems(4)
->uniqueItems()
->writable(),
],
),
);

$response = $this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => ['type' => 'customers', 'attributes' => ['featureToggles' => [1, 2, 3]]],
]),
);

$this->assertJsonApiDocumentSubset(
['data' => ['attributes' => ['featureToggles' => [1, 2, 3]]]],
$response->getBody(),
true,
);
}

public function test_invalid_items()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [
ArrayField::make('featureToggles')
->items(Str::make('')->enum(['valid']))
->writable(),
],
),
);

$this->expectException(UnprocessableEntityException::class);

$this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => [
'type' => 'customers',
'attributes' => ['featureToggles' => ['valid', 'invalid']],
],
]),
);
}

public function test_valid_items()
{
$this->api->resource(
new MockResource(
'customers',
endpoints: [Create::make()],
fields: [
ArrayField::make('featureToggles')
->items(Str::make('')->enum(['valid1', 'valid2']))
->writable(),
],
),
);

$response = $this->api->handle(
$this->buildRequest('POST', '/customers')->withParsedBody([
'data' => [
'type' => 'customers',
'attributes' => ['featureToggles' => ['valid1', 'valid2']],
],
]),
);

$this->assertJsonApiDocumentSubset(
['data' => ['attributes' => ['featureToggles' => ['valid1', 'valid2']]]],
$response->getBody(),
true,
);
}

public function test_schema()
{
$this->assertEquals(
[
'type' => 'array',
'minItems' => 0,
'maxItems' => null,
'uniqueItems' => false,
'items' => null,
'description' => null,
'nullable' => false,
],
ArrayField::make('featureToggles')->getSchema(),
);

$this->assertEquals(
[
'type' => 'array',
'minItems' => 1,
'maxItems' => 10,
'uniqueItems' => true,
'items' => [
'type' => 'string',
'enum' => ['valid1', 'valid2'],
'description' => null,
'nullable' => false,
'minLength' => 0,
'maxLength' => null,
'pattern' => null,
'format' => null,
],
'description' => null,
'nullable' => false,
],
ArrayField::make('featureToggles')
->minItems(1)
->maxItems(10)
->uniqueItems()
->items(Str::make('')->enum(['valid1', 'valid2']))
->getSchema(),
);
}
}