Skip to content

Add array field #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
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(),
);
}
}