From 5593a6b26287ca1c339968f726efc50984c9b3ec Mon Sep 17 00:00:00 2001 From: Sean Tymon Date: Mon, 13 Jan 2025 20:31:48 +0000 Subject: [PATCH] add enum and const --- src/Exceptions/SchemaException.php | 3 + src/Types/AbstractSchema.php | 6 ++ src/Types/Concerns/HasConst.php | 44 ++++++++++++ src/Types/Concerns/HasEnum.php | 41 +++++++++++ src/Types/Concerns/HasValidation.php | 1 - tests/Unit/Targets/NumberSchemaTest.php | 91 +++++++++++++++++++++++++ tests/Unit/Targets/ObjectSchemaTest.php | 3 +- tests/Unit/Targets/StringSchemaTest.php | 48 +++++++++++++ 8 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 src/Types/Concerns/HasConst.php create mode 100644 src/Types/Concerns/HasEnum.php diff --git a/src/Exceptions/SchemaException.php b/src/Exceptions/SchemaException.php index abd1534..69f2c62 100644 --- a/src/Exceptions/SchemaException.php +++ b/src/Exceptions/SchemaException.php @@ -33,6 +33,9 @@ public function getError(): ValidationError return $this->error; } + /** + * @return array + */ public function getErrors(): array { return (new ErrorFormatter())->format($this->error); diff --git a/src/Types/AbstractSchema.php b/src/Types/AbstractSchema.php index b4ae0fc..87e23ae 100644 --- a/src/Types/AbstractSchema.php +++ b/src/Types/AbstractSchema.php @@ -6,6 +6,8 @@ use Cortex\JsonSchema\Contracts\Schema; use Cortex\JsonSchema\Enums\SchemaType; +use Cortex\JsonSchema\Types\Concerns\HasEnum; +use Cortex\JsonSchema\Types\Concerns\HasConst; use Cortex\JsonSchema\Types\Concerns\HasTitle; use Cortex\JsonSchema\Types\Concerns\HasFormat; use Cortex\JsonSchema\Types\Concerns\HasRequired; @@ -21,6 +23,8 @@ abstract class AbstractSchema implements Schema use HasReadWrite; use HasValidation; use HasDescription; + use HasEnum; + use HasConst; protected string $schemaVersion = 'http://json-schema.org/draft-07/schema#'; @@ -93,6 +97,8 @@ public function toArray(bool $includeSchemaRef = true, bool $includeTitle = true $schema = $this->addTitleToSchema($schema, $includeTitle); $schema = $this->addFormatToSchema($schema); $schema = $this->addDescriptionToSchema($schema); + $schema = $this->addEnumToSchema($schema); + $schema = $this->addConstToSchema($schema); return $this->addReadWriteToSchema($schema); } diff --git a/src/Types/Concerns/HasConst.php b/src/Types/Concerns/HasConst.php new file mode 100644 index 0000000..6c20787 --- /dev/null +++ b/src/Types/Concerns/HasConst.php @@ -0,0 +1,44 @@ +const = $value; + $this->hasConst = true; + + return $this; + } + + /** + * Add const value to schema array. + * + * @param array $schema + * + * @return array + */ + protected function addConstToSchema(array $schema): array + { + if ($this->hasConst) { + $schema['const'] = $this->const; + } + + return $schema; + } +} diff --git a/src/Types/Concerns/HasEnum.php b/src/Types/Concerns/HasEnum.php new file mode 100644 index 0000000..65fcb06 --- /dev/null +++ b/src/Types/Concerns/HasEnum.php @@ -0,0 +1,41 @@ +|null + */ + protected ?array $enum = null; + + /** + * Set the allowed enum values. + * + * @param non-empty-array $values + */ + public function enum(array $values): static + { + $this->enum = array_values(array_unique($values, SORT_REGULAR)); + + return $this; + } + + /** + * Add enum values to schema array. + * + * @param array $schema + * + * @return array + */ + protected function addEnumToSchema(array $schema): array + { + if ($this->enum !== null) { + $schema['enum'] = $this->enum; + } + + return $schema; + } +} diff --git a/src/Types/Concerns/HasValidation.php b/src/Types/Concerns/HasValidation.php index 59ef1b2..ae3e2b4 100644 --- a/src/Types/Concerns/HasValidation.php +++ b/src/Types/Concerns/HasValidation.php @@ -7,7 +7,6 @@ use Opis\JsonSchema\Helper; use InvalidArgumentException; use Opis\JsonSchema\Validator; -use Opis\JsonSchema\Errors\ErrorFormatter; use Cortex\JsonSchema\Exceptions\SchemaException; use Opis\JsonSchema\Exceptions\SchemaException as OpisSchemaException; diff --git a/tests/Unit/Targets/NumberSchemaTest.php b/tests/Unit/Targets/NumberSchemaTest.php index 7cbb755..b240bc7 100644 --- a/tests/Unit/Targets/NumberSchemaTest.php +++ b/tests/Unit/Targets/NumberSchemaTest.php @@ -140,3 +140,94 @@ 'The data (string) must match the type: number, null', ); }); + +it('can create a number schema with enum values', function (): void { + $schema = Schema::number('rating') + ->description('Product rating') + ->enum([1.0, 2.5, 3.0, 4.5, 5.0]); + + $schemaArray = $schema->toArray(); + + expect($schemaArray)->toHaveKey('type', 'number'); + expect($schemaArray)->toHaveKey('title', 'rating'); + expect($schemaArray)->toHaveKey('description', 'Product rating'); + expect($schemaArray)->toHaveKey('enum', [1.0, 2.5, 3.0, 4.5, 5.0]); + + // Validation tests + expect(fn() => $schema->validate(3.5))->toThrow( + SchemaException::class, + 'The data should match one item from enum', + ); + + expect(fn() => $schema->validate(1.0))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate(2.5))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate(5.0))->not->toThrow(SchemaException::class); +}); + +it('can create a nullable number schema with enum values', function (): void { + $schema = Schema::number('discount') + ->description('Product discount percentage') + ->enum([0.1, 0.25, 0.5, null]) + ->nullable(); + + $schemaArray = $schema->toArray(); + + expect($schemaArray)->toHaveKey('type', ['number', 'null']); + expect($schemaArray)->toHaveKey('title', 'discount'); + expect($schemaArray)->toHaveKey('description', 'Product discount percentage'); + expect($schemaArray)->toHaveKey('enum', [0.1, 0.25, 0.5, null]); + + // Validation tests + expect(fn() => $schema->validate(0.75))->toThrow( + SchemaException::class, + 'The data should match one item from enum', + ); + + expect(fn() => $schema->validate(0.1))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate(0.25))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate(0.5))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate(null))->not->toThrow(SchemaException::class); +}); + +it('can create a number schema with const value', function (): void { + $schema = Schema::number('tax_rate') + ->description('Fixed tax rate percentage') + ->const(0.21); // 21% VAT + + $schemaArray = $schema->toArray(); + + expect($schemaArray)->toHaveKey('type', 'number'); + expect($schemaArray)->toHaveKey('title', 'tax_rate'); + expect($schemaArray)->toHaveKey('description', 'Fixed tax rate percentage'); + expect($schemaArray)->toHaveKey('const', 0.21); + + // Validation tests + expect(fn() => $schema->validate(0.20))->toThrow( + SchemaException::class, + 'The data must match the const value', + ); + + expect(fn() => $schema->validate(0.21))->not->toThrow(SchemaException::class); +}); + +it('can create a nullable number schema with const value', function (): void { + $schema = Schema::number('standard_fee') + ->description('Standard processing fee') + ->nullable() + ->const(null); + + $schemaArray = $schema->toArray(); + + expect($schemaArray)->toHaveKey('type', ['number', 'null']); + expect($schemaArray)->toHaveKey('title', 'standard_fee'); + expect($schemaArray)->toHaveKey('description', 'Standard processing fee'); + expect($schemaArray)->toHaveKey('const', null); + + // Validation tests + expect(fn() => $schema->validate(0.0))->toThrow( + SchemaException::class, + 'The data must match the const value', + ); + + expect(fn() => $schema->validate(null))->not->toThrow(SchemaException::class); +}); diff --git a/tests/Unit/Targets/ObjectSchemaTest.php b/tests/Unit/Targets/ObjectSchemaTest.php index 29f36ac..cd820ea 100644 --- a/tests/Unit/Targets/ObjectSchemaTest.php +++ b/tests/Unit/Targets/ObjectSchemaTest.php @@ -5,7 +5,6 @@ namespace Cortex\JsonSchema\Tests\Unit; use Cortex\JsonSchema\Enums\SchemaFormat; -use Opis\JsonSchema\Errors\ErrorFormatter; use Cortex\JsonSchema\SchemaFactory as Schema; use Cortex\JsonSchema\Exceptions\SchemaException; @@ -95,7 +94,7 @@ expect($e->getMessage())->toBe('The properties must match schema: email'); expect($e->getErrors())->toBe([ '/email' => [ - 'The data must match the \'email\' format' + "The data must match the 'email' format", ], ]); diff --git a/tests/Unit/Targets/StringSchemaTest.php b/tests/Unit/Targets/StringSchemaTest.php index 6cc9bc1..39d6487 100644 --- a/tests/Unit/Targets/StringSchemaTest.php +++ b/tests/Unit/Targets/StringSchemaTest.php @@ -134,3 +134,51 @@ expect(fn() => $schema->validate('2024-03-14T12:00:00Z'))->not->toThrow(SchemaException::class); }); + +it('can create a string schema with enum values', function (): void { + $schema = Schema::string('status') + ->description('Current status of the record') + ->enum(['draft', 'published', 'archived']); + + $schemaArray = $schema->toArray(); + + expect($schemaArray)->toHaveKey('type', 'string'); + expect($schemaArray)->toHaveKey('title', 'status'); + expect($schemaArray)->toHaveKey('description', 'Current status of the record'); + expect($schemaArray)->toHaveKey('enum', ['draft', 'published', 'archived']); + + // Validation tests + expect(fn() => $schema->validate('pending'))->toThrow( + SchemaException::class, + 'The data should match one item from enum', + ); + + expect(fn() => $schema->validate('draft'))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate('published'))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate('archived'))->not->toThrow(SchemaException::class); +}); + +it('can create a nullable string schema with enum values', function (): void { + $schema = Schema::string('priority') + ->description('Task priority level') + ->enum(['low', 'medium', 'high', null]) + ->nullable(); + + $schemaArray = $schema->toArray(); + + expect($schemaArray)->toHaveKey('type', ['string', 'null']); + expect($schemaArray)->toHaveKey('title', 'priority'); + expect($schemaArray)->toHaveKey('description', 'Task priority level'); + expect($schemaArray)->toHaveKey('enum', ['low', 'medium', 'high', null]); + + // Validation tests + expect(fn() => $schema->validate('critical'))->toThrow( + SchemaException::class, + 'The data should match one item from enum', + ); + + expect(fn() => $schema->validate('low'))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate('medium'))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate('high'))->not->toThrow(SchemaException::class); + expect(fn() => $schema->validate(null))->not->toThrow(SchemaException::class); +});