From f9b2716d0b8bb9fd174bbd083efba6f2beda4896 Mon Sep 17 00:00:00 2001 From: Enno Woortmann Date: Thu, 19 Sep 2024 17:17:26 +0200 Subject: [PATCH] Add additional test cases for const usage with some patches. --- .../Validator/PropertyNamesValidator.php | 11 +- .../Property/AbstractPropertyProcessor.php | 4 +- .../Property/ArrayProcessor.php | 11 +- tests/Basic/PropertyNamesTest.php | 29 ++ tests/Objects/ArrayPropertyTest.php | 1 - tests/Objects/ConstPropertyTest.php | 282 ++++++++++++++---- .../AdditionalPropertiesConst.json | 6 + .../ConstPropertyTest/ArrayContainsConst.json | 11 + .../ArrayItemConstProperty.json | 8 +- .../ArrayTupleConstProperty.json | 13 + ...tProperty.json => OneOfConstProperty.json} | 0 .../PatternPropertiesConst.json | 8 + tests/manual/schema/person.json | 24 +- tests/manual/test.php | 8 +- 14 files changed, 338 insertions(+), 78 deletions(-) create mode 100644 tests/Schema/ConstPropertyTest/AdditionalPropertiesConst.json create mode 100644 tests/Schema/ConstPropertyTest/ArrayContainsConst.json create mode 100644 tests/Schema/ConstPropertyTest/ArrayTupleConstProperty.json rename tests/Schema/ConstPropertyTest/{AnyOfConstProperty.json => OneOfConstProperty.json} (100%) create mode 100644 tests/Schema/ConstPropertyTest/PatternPropertiesConst.json diff --git a/src/Model/Validator/PropertyNamesValidator.php b/src/Model/Validator/PropertyNamesValidator.php index 019655ff..e738e553 100644 --- a/src/Model/Validator/PropertyNamesValidator.php +++ b/src/Model/Validator/PropertyNamesValidator.php @@ -10,6 +10,7 @@ use PHPModelGenerator\Model\Schema; use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; use PHPModelGenerator\Model\Validator; +use PHPModelGenerator\PropertyProcessor\Property\ConstProcessor; use PHPModelGenerator\PropertyProcessor\Property\StringProcessor; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\SchemaProcessor\SchemaProcessor; @@ -38,7 +39,15 @@ public function __construct( ) { $this->isResolved = true; - $nameValidationProperty = (new StringProcessor(new PropertyMetaDataCollection(), $schemaProcessor, $schema)) + $processor = array_key_exists('const', $propertiesNames->getJson()) + ? ConstProcessor::class + : StringProcessor::class; + + if ($processor === ConstProcessor::class && gettype($propertiesNames->getJson()['const']) !== 'string') { + throw new SchemaException("Invalid const property name in file {$propertiesNames->getFile()}"); + } + + $nameValidationProperty = (new $processor(new PropertyMetaDataCollection(), $schemaProcessor, $schema)) ->process('property name', $propertiesNames) // the property name validator doesn't need type checks or required checks so simply filter them out ->filterValidators(static function (Validator $validator): bool { diff --git a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php index e041c810..31984003 100644 --- a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php +++ b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php @@ -213,7 +213,9 @@ protected function addComposedValueValidator(PropertyInterface $property, JsonSc $propertySchema->withJson([ 'type' => $composedValueKeyword, 'propertySchema' => $propertySchema, - 'onlyForDefinedValues' => !($this instanceof BaseProcessor) && !$property->isRequired(), + 'onlyForDefinedValues' => !($this instanceof BaseProcessor) && + (!$property->isRequired() + && $this->schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed()), ]), ); diff --git a/src/PropertyProcessor/Property/ArrayProcessor.php b/src/PropertyProcessor/Property/ArrayProcessor.php index 8937e2a0..0057fcbe 100644 --- a/src/PropertyProcessor/Property/ArrayProcessor.php +++ b/src/PropertyProcessor/Property/ArrayProcessor.php @@ -16,11 +16,13 @@ use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Property\PropertyType; use PHPModelGenerator\Model\SchemaDefinition\JsonSchema; +use PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Validator\AdditionalItemsValidator; use PHPModelGenerator\Model\Validator\ArrayItemValidator; use PHPModelGenerator\Model\Validator\ArrayTupleValidator; use PHPModelGenerator\Model\Validator\PropertyTemplateValidator; use PHPModelGenerator\Model\Validator\PropertyValidator; +use PHPModelGenerator\Model\Validator\RequiredPropertyValidator; use PHPModelGenerator\PropertyProcessor\Decorator\Property\DefaultArrayToEmptyArrayDecorator; use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection; use PHPModelGenerator\PropertyProcessor\PropertyFactory; @@ -253,16 +255,21 @@ private function addContainsValidation(PropertyInterface $property, JsonSchema $ return; } + $name = "item of array {$property->getName()}"; // an item of the array behaves like a nested property to add item-level validation $nestedProperty = (new PropertyFactory(new PropertyProcessorFactory())) ->create( - new PropertyMetaDataCollection(), + new PropertyMetaDataCollection([$name]), $this->schemaProcessor, $this->schema, - "item of array {$property->getName()}", + $name, $propertySchema->withJson($propertySchema->getJson()[self::JSON_FIELD_CONTAINS]), ); + $nestedProperty->filterValidators(static function (Validator $validator): bool { + return !is_a($validator->getValidator(), RequiredPropertyValidator::class); + }); + $property->addValidator( new PropertyTemplateValidator( $property, diff --git a/tests/Basic/PropertyNamesTest.php b/tests/Basic/PropertyNamesTest.php index 692c641c..5c715158 100644 --- a/tests/Basic/PropertyNamesTest.php +++ b/tests/Basic/PropertyNamesTest.php @@ -4,6 +4,7 @@ namespace PHPModelGenerator\Tests\Basic; +use PHPModelGenerator\Exception\SchemaException; use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTestCase; @@ -84,6 +85,12 @@ public function validPropertyNamesDataProvider(): array 'test1298398717931793179317937197931' => 2, ], ], + 'const' => [ + '{"const": "test"}', + [ + 'test' => 1, + ], + ], ], ); } @@ -166,6 +173,21 @@ public function invalidPropertyNamesDataProvider(): array * Value for property name doesn't match pattern ^test[0-9]+$ - invalid property 'test' * Value for property name doesn't match pattern ^test[0-9]+$ +ERROR + ], + 'const violation' => [ + '{"const": "test"}', + [ + 'test1' => 1, + 'test' => 2, + 'bla' => 3, + ], + <<expectException(SchemaException::class); + + $this->generateClassFromFileTemplate('PropertyNames.json', ['{"const": null}']); + } } diff --git a/tests/Objects/ArrayPropertyTest.php b/tests/Objects/ArrayPropertyTest.php index 4f855ccd..13c34c6e 100644 --- a/tests/Objects/ArrayPropertyTest.php +++ b/tests/Objects/ArrayPropertyTest.php @@ -683,7 +683,6 @@ public function validArrayContainsDataProvider(): array return $this->combineDataProvider( $this->validationMethodDataProvider(), [ - 'null' => [[3, null, true]], 'empty string' => [[3, '', true]], 'lowercase string' => [[3, 'abc', true]], 'uppercase string' => [[3, 'AB', true]], diff --git a/tests/Objects/ConstPropertyTest.php b/tests/Objects/ConstPropertyTest.php index e4a225ae..8fc15798 100644 --- a/tests/Objects/ConstPropertyTest.php +++ b/tests/Objects/ConstPropertyTest.php @@ -4,10 +4,14 @@ namespace PHPModelGenerator\Tests\Objects; +use PHPModelGenerator\Exception\Arrays\ContainsException; +use PHPModelGenerator\Exception\Arrays\InvalidItemException; use PHPModelGenerator\Exception\Arrays\InvalidTupleException; use PHPModelGenerator\Exception\ComposedValue\OneOfException; use PHPModelGenerator\Exception\ErrorRegistryException; use PHPModelGenerator\Exception\FileSystemException; +use PHPModelGenerator\Exception\Object\InvalidAdditionalPropertiesException; +use PHPModelGenerator\Exception\Object\InvalidPatternPropertiesException; use PHPModelGenerator\Exception\Object\RequiredValueException; use PHPModelGenerator\Exception\ValidationException; use PHPModelGenerator\Exception\RenderException; @@ -39,44 +43,126 @@ public function testProvidedConstPropertyIsValid(): void } /** - * @throws FileSystemException - * @throws RenderException - * @throws SchemaException + * @dataProvider nestedConstStructureDataProvider */ - public function testProvidedArrayItemConstPropertyIsValid(): void + public function testNotProvidedOptionalNestedConstPropertyIsValid(string $file): void { - $className = $this->generateClassFromFile('ArrayItemConstProperty.json'); + $className = $this->generateClassFromFile($file); - $object = new $className(['property' => ['red', 'red']]); + $object = new $className([]); + + $this->assertNull($object->getProperty()); + } - $this->assertIsArray($object->getProperty()); - $this->assertSame(['red', 'red'], $object->getProperty()); + public function nestedConstStructureDataProvider(): array + { + return [ + 'array tuple' => ['ArrayTupleConstProperty.json'], + 'array item' => ['ArrayItemConstProperty.json'], + 'oneOf' => ['OneOfConstProperty.json'], + ]; } /** - * @dataProvider stringIntDataProvider - * - * @throws FileSystemException - * @throws RenderException - * @throws SchemaException + * @dataProvider validArrayConstValues */ - public function testProvidedAnyOfConstPropertyIsValid(string|int $propertyValue): void + public function testProvidedArrayConstPropertyIsValid(string $file, ?array $value): void + { + $className = $this->generateClassFromFile($file); + + $object = new $className(['property' => $value]); + + $this->assertSame($value, $object->getProperty()); + } + + public function validArrayConstValues(): array { - $className = $this->generateClassFromFile('AnyOfConstProperty.json'); + return $this->combineDataProvider( + [ + 'array tuple' => ['ArrayTupleConstProperty.json'], + 'array item' => ['ArrayItemConstProperty.json'], + ], + [ + 'item provided' => [['red']], + 'multiple items provided' => [['red', 'red']], + 'null provided' => [null], + ], + ); + } + + /** + * @dataProvider invalidArrayConstDataProvider + */ + public function testNotMatchingArrayConstPropertyThrowsAnException( + string $file, + string $exception, + array $value, + ): void { + $this->expectException($exception); + + $className = $this->generateClassFromFile($file); + + new $className(['property' => $value]); + } + + public function invalidArrayConstDataProvider(): array + { + return $this->combineDataProvider( + [ + 'array tuple' => ['ArrayTupleConstProperty.json', InvalidTupleException::class], + 'array item' => ['ArrayItemConstProperty.json', InvalidItemException::class], + ], + [ + 'invalid item' => [['green']], + 'invalid item (multiple items)' => [['green', 'red']], + 'null' => [[null]], + ], + ); + } + + /** + * @dataProvider nestedConstStructureDataProvider + */ + public function testNullForNestedConstPropertyWithImplicitNullDisabledThrowsAnException(string $file): void + { + $this->expectException(ValidationException::class); + + $className = $this->generateClassFromFile($file, implicitNull: false); + + new $className(['property' => null]); + } + + /** + * @dataProvider validOneOfDataProvider + */ + public function testProvidedOneOfConstPropertyIsValid(mixed $propertyValue): void + { + $className = $this->generateClassFromFile('OneOfConstProperty.json'); $object = new $className(['property' => $propertyValue]); $this->assertSame($propertyValue, $object->getProperty()); } - public function stringIntDataProvider(): array + public function validOneOfDataProvider(): array { return [ - ['red'], - [1], + 'first branch' => ['red'], + 'second branch' => [1], + 'implicit null' => [null], ]; } + public function testNotMatchingOneOfPropertyThrowsAnException(): void + { + $this->expectException(OneOfException::class); + $this->expectExceptionMessage('Invalid value for property declined by composition constraint'); + + $className = $this->generateClassFromFile('OneOfConstProperty.json'); + + new $className(['property' => 'green']); + } + /** * @dataProvider invalidPropertyDataProvider * @@ -109,36 +195,6 @@ public function invalidPropertyDataProvider(): array ]; } - /** - * @throws FileSystemException - * @throws RenderException - * @throws SchemaException - */ - public function testNotMatchingArrayItemConstPropertyThrowsAnException(): void - { - $this->expectException(InvalidTupleException::class); - $this->expectExceptionMessage('Invalid tuple item in array property'); - - $className = $this->generateClassFromFile('ArrayItemConstProperty.json'); - - new $className(['property' => ['green']]); - } - - /** - * @throws FileSystemException - * @throws RenderException - * @throws SchemaException - */ - public function testNotMatchingArrayItemConstPropertyThrowsAnException1(): void - { - $this->expectException(OneOfException::class); - $this->expectExceptionMessage('Invalid value for property declined by composition constraint'); - - $className = $this->generateClassFromFile('AnyOfConstProperty.json'); - - new $className(['property' => 'green']); - } - /** * @throws FileSystemException * @throws RenderException @@ -262,16 +318,136 @@ public function invalidRequiredAndOptionalConstPropertiesDataProvider(): array } /** - * @throws FileSystemException - * @throws RenderException - * @throws SchemaException + * @dataProvider implicitNullDataProvider */ - public function testProvidedNullValueConstPropertyIsValid(): void + public function testProvidedNullValueConstPropertyIsValid(bool $implicitNull): void { - $className = $this->generateClassFromFile('NullValueConstProperty.json', null, false, false); + $className = $this->generateClassFromFile('NullValueConstProperty.json', implicitNull: $implicitNull); $object = new $className(['nullProperty' => null]); $this->assertNull($object->getNullProperty()); } + + /** + * @dataProvider validConstAdditionalPropertiesDataProvider + */ + public function testValidConstAdditionalProperties(array $value): void + { + $className = $this->generateClassFromFile('AdditionalPropertiesConst.json'); + + $object = new $className($value); + + $this->assertSame($value, $object->getRawModelDataInput()); + } + + public function validConstAdditionalPropertiesDataProvider(): array + { + return [ + 'no properties' => [[]], + 'one property' => [['property1' => 'red']], + 'multiple properties' => [['property1' => 'red', 'property2' => 'red']], + ]; + } + + /** + * @dataProvider invalidConstAdditionalPropertiesDataProvider + */ + public function testInvalidConstAdditionalPropertiesThrowsAnException(array $value): void + { + $this->expectException(InvalidAdditionalPropertiesException::class); + $this->expectExceptionMessageMatches('/Invalid value for additional property declined by const constraint/'); + + $className = $this->generateClassFromFile('AdditionalPropertiesConst.json'); + + new $className($value); + } + + public function invalidConstAdditionalPropertiesDataProvider(): array + { + return [ + 'null' => [['property1' => null]], + 'invalid value' => [['property1' => 'green']], + 'mixed valid and invalid values' => [['property1' => 'red', 'property2' => 'green']], + ]; + } + + /** + * @dataProvider validConstPatternPropertiesDataProvider + */ + public function testValidConstPatternProperties(array $value): void + { + $className = $this->generateClassFromFile('PatternPropertiesConst.json'); + + $object = new $className($value); + + $this->assertSame($value, $object->getRawModelDataInput()); + } + + public function validConstPatternPropertiesDataProvider(): array + { + return [ + 'no properties' => [[]], + 'one property' => [['property1' => 'red']], + 'multiple properties' => [['property1' => 'red', 'property2' => 'red']], + 'not matching property' => [['different' => 'green']], + ]; + } + + /** + * @dataProvider invalidConstAdditionalPropertiesDataProvider + */ + public function testInvalidConstPatternPropertiesThrowsAnException(array $value): void + { + $this->expectException(InvalidPatternPropertiesException::class); + $this->expectExceptionMessageMatches('/Invalid value for pattern property declined by const constraint/'); + + $className = $this->generateClassFromFile('PatternPropertiesConst.json'); + + new $className($value); + } + + + /** + * @dataProvider validConstArrayContainsDataProvider + */ + public function testValidConstArrayContains(array $value): void + { + $className = $this->generateClassFromFile('ArrayContainsConst.json'); + + $object = new $className(['property' => $value]); + + $this->assertSame($value, $object->getProperty()); + } + + public function validConstArrayContainsDataProvider(): array + { + return [ + 'one item' => [['red']], + 'multiple items all matching' => [['red', 'red']], + 'multiple items one matching' => [['green', 'red', 'yellow']], + ]; + } + + /** + * @dataProvider invalidConstArrayContainsDataProvider + */ + public function testInvalidConstArrayContainsThrowsAnException(array $value): void + { + $this->expectException(ContainsException::class); + $this->expectExceptionMessage('No item in array property matches contains constraint'); + + $className = $this->generateClassFromFile('ArrayContainsConst.json'); + + new $className(['property' => $value]); + } + + public function invalidConstArrayContainsDataProvider(): array + { + return [ + 'empty array' => [[]], + 'null' => [[null]], + 'value not in array' => [['green', 'yellow', 'blue']], + ]; + } } diff --git a/tests/Schema/ConstPropertyTest/AdditionalPropertiesConst.json b/tests/Schema/ConstPropertyTest/AdditionalPropertiesConst.json new file mode 100644 index 00000000..4513ce30 --- /dev/null +++ b/tests/Schema/ConstPropertyTest/AdditionalPropertiesConst.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "additionalProperties": { + "const": "red" + } +} \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/ArrayContainsConst.json b/tests/Schema/ConstPropertyTest/ArrayContainsConst.json new file mode 100644 index 00000000..fbca23b1 --- /dev/null +++ b/tests/Schema/ConstPropertyTest/ArrayContainsConst.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "property": { + "type": "array", + "contains": { + "const": "red" + } + } + } +} \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json b/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json index c111e74b..7620d710 100644 --- a/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json +++ b/tests/Schema/ConstPropertyTest/ArrayItemConstProperty.json @@ -3,11 +3,9 @@ "properties": { "property": { "type": "array", - "items": [ - { - "const": "red" - } - ] + "items": { + "const": "red" + } } } } \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/ArrayTupleConstProperty.json b/tests/Schema/ConstPropertyTest/ArrayTupleConstProperty.json new file mode 100644 index 00000000..c111e74b --- /dev/null +++ b/tests/Schema/ConstPropertyTest/ArrayTupleConstProperty.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "property": { + "type": "array", + "items": [ + { + "const": "red" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Schema/ConstPropertyTest/AnyOfConstProperty.json b/tests/Schema/ConstPropertyTest/OneOfConstProperty.json similarity index 100% rename from tests/Schema/ConstPropertyTest/AnyOfConstProperty.json rename to tests/Schema/ConstPropertyTest/OneOfConstProperty.json diff --git a/tests/Schema/ConstPropertyTest/PatternPropertiesConst.json b/tests/Schema/ConstPropertyTest/PatternPropertiesConst.json new file mode 100644 index 00000000..885e885d --- /dev/null +++ b/tests/Schema/ConstPropertyTest/PatternPropertiesConst.json @@ -0,0 +1,8 @@ +{ + "type": "object", + "patternProperties": { + "property\\d+": { + "const": "red" + } + } +} \ No newline at end of file diff --git a/tests/manual/schema/person.json b/tests/manual/schema/person.json index 0296a97a..1a946931 100644 --- a/tests/manual/schema/person.json +++ b/tests/manual/schema/person.json @@ -1,19 +1,15 @@ { - "$id": "Person", "type": "object", "properties": { - "name": { - "type": "string", - "description": "The name of the person", - "example": "Lawrence" - }, - "age": { - "type": "integer", - "description": "The age of the person", - "example": 42 + "property": { + "oneOf": [ + { + "const": "1" + }, + { + "const": "2" + } + ] } - }, - "required": [ - "name" - ] + } } \ No newline at end of file diff --git a/tests/manual/test.php b/tests/manual/test.php index 60456579..87d6c557 100644 --- a/tests/manual/test.php +++ b/tests/manual/test.php @@ -8,9 +8,15 @@ $generator = new ModelGenerator((new GeneratorConfiguration()) ->setNamespacePrefix('\\ManualSchema') - ->setImmutable(false), + ->setImmutable(false) + ->setCollectErrors(false) + ->setImplicitNull(false) ); $generator ->generateModelDirectory(__DIR__ . '/result') ->generateModels(new RecursiveDirectoryProvider(__DIR__ . '/schema'), __DIR__ . '/result'); + +$p = new \ManualSchema\Person(['property' => 1]); + +var_dump($p->getProperty());