diff --git a/src/Schema/Field/ArrayField.php b/src/Schema/Field/ArrayField.php new file mode 100644 index 0000000..ab9c610 --- /dev/null +++ b/src/Schema/Field/ArrayField.php @@ -0,0 +1,86 @@ +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(), + ]; + } +} diff --git a/tests/feature/ArrayFieldTest.php b/tests/feature/ArrayFieldTest.php new file mode 100644 index 0000000..19cd33a --- /dev/null +++ b/tests/feature/ArrayFieldTest.php @@ -0,0 +1,237 @@ +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(), + ); + } +}