diff --git a/README.md b/README.md index 4a682ed..41bd47d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ As an optional parameter you can set up a *GeneratorConfiguration* object to con $generator = new Generator( (new GeneratorConfiguration()) ->setNamespacePrefix('\MyApp\Model') - ->setImmutable(true) + ->setImmutable(false) ); $generator diff --git a/docs/source/combinedSchemas/mergedProperty.rst b/docs/source/combinedSchemas/mergedProperty.rst index 86734ec..201adbb 100644 --- a/docs/source/combinedSchemas/mergedProperty.rst +++ b/docs/source/combinedSchemas/mergedProperty.rst @@ -1,8 +1,11 @@ Merged Property =============== -If multiple subschemas are combined with `oneOf`, `anyOf` or `allOf` and the subschemas contain multiple nested objects all properties of the nested objects will be merged together in a single object. -For example we combine two objects with `allOf`: +If multiple subschemas are combined with `oneOf`, `anyOf` or `allOf` and the subschemas contain multiple nested objects all properties of the nested objects will be merged together in a single object representing all composition elements. + +If the composition is used on object level no merged property will be generated as the object itself works as a merged property holding all properties of the nested objects from the composition subschemas. + +For example we combine two objects with `allOf` for an object property: .. code-block:: json @@ -58,3 +61,44 @@ Generated interface: public function setName(?string $name): self public function getAge(): ?int public function setAge(?int $name): self + +If your composition is defined on object level the object will gain access to all properties of the combined schemas: + +.. code-block:: json + + { + "$id": "CEO", + "type": "object", + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "age": { + "type": "integer" + } + } + } + ] + } + +This schema will generate three classes as no merged property is created. The main class will be `CEO` and two classes will be generated to validate the subschemas combined with the `allOf` independent: + +* Ceo.php +* Ceo_Ceo5e4a82e39edc3.php +* Ceo_Ceo5e4a82e39fe37.php + +.. code-block:: php + + # class CEO + public function getName(): ?string + public function setName(?string $name): self + public function getAge(): ?int + public function setAge(?int $name): self diff --git a/docs/source/gettingStarted.rst b/docs/source/gettingStarted.rst index 54266c6..12868b1 100644 --- a/docs/source/gettingStarted.rst +++ b/docs/source/gettingStarted.rst @@ -41,7 +41,7 @@ As an optional parameter you can set up a *GeneratorConfiguration* object to con $generator = new Generator( (new GeneratorConfiguration()) ->setNamespacePrefix('\MyApp\Model') - ->setImmutable(true) + ->setImmutable(false) ); $generator diff --git a/src/Model/Property/BaseProperty.php b/src/Model/Property/BaseProperty.php new file mode 100644 index 0000000..a87d310 --- /dev/null +++ b/src/Model/Property/BaseProperty.php @@ -0,0 +1,14 @@ +rootLevelComposition = $rootLevelComposition; + } /** * @inheritdoc @@ -44,6 +67,9 @@ protected function generateValidators(PropertyInterface $property, array $proper } $compositionProperties = $this->getCompositionProperties($property, $propertyData); + + $this->transferPropertyType($property, $compositionProperties); + $availableAmount = count($compositionProperties); $property->addValidator( @@ -62,7 +88,7 @@ protected function generateValidators(PropertyInterface $property, array $proper // Otherwise (eg. a NotProcessor) the value must be proposed before the validation 'postPropose' => $this instanceof ComposedPropertiesInterface, 'mergedProperty' => - $this instanceof MergedComposedPropertiesInterface + !$this->rootLevelComposition && $this instanceof MergedComposedPropertiesInterface ? $this->createMergedProperty($property, $compositionProperties, $propertyData) : null, 'onlyForDefinedValues' => @@ -120,12 +146,32 @@ protected function getCompositionProperties(PropertyInterface $property, array $ } /** - * TODO: no nested properties --> cancel, only one --> use original model + * Check if the provided property can inherit a single type from the composition properties. * + * @param PropertyInterface $property + * @param CompositionPropertyDecorator[] $compositionProperties + */ + private function transferPropertyType(PropertyInterface $property, array $compositionProperties) + { + $compositionPropertyTypes = array_unique( + array_map( + function (CompositionPropertyDecorator $property): string { + return $property->getType(); + }, + $compositionProperties + ) + ); + + if (count($compositionPropertyTypes) === 1 && !($this instanceof NotProcessor)) { + $property->setType($compositionPropertyTypes[0]); + } + } + + /** * Gather all nested object properties and merge them together into a single merged property * * @param PropertyInterface $property - * @param CompositionPropertyDecorator[] $properties + * @param CompositionPropertyDecorator[] $compositionProperties * @param array $propertyData * * @return PropertyInterface|null @@ -134,9 +180,18 @@ protected function getCompositionProperties(PropertyInterface $property, array $ */ private function createMergedProperty( PropertyInterface $property, - array $properties, + array $compositionProperties, array $propertyData ): ?PropertyInterface { + $redirectToProperty = $this->redirectMergedProperty($compositionProperties); + if ($redirectToProperty === null || $redirectToProperty instanceof PropertyInterface) { + if ($redirectToProperty) { + $property->addTypeHintDecorator(new CompositionTypeHintDecorator($redirectToProperty)); + } + + return $redirectToProperty; + } + $mergedClassName = $this->schemaProcessor ->getGeneratorConfiguration() ->getClassNameGenerator() @@ -156,7 +211,7 @@ private function createMergedProperty( $mergedProperty = new Property('MergedProperty', $mergedClassName); self::$generatedMergedProperties[$mergedClassName] = $mergedProperty; - $this->transferPropertiesToMergedSchema($mergedPropertySchema, $properties); + $this->transferPropertiesToMergedSchema($mergedPropertySchema, $compositionProperties); $this->schemaProcessor->generateClassFile( $this->schemaProcessor->getCurrentClassPath(), @@ -173,13 +228,40 @@ private function createMergedProperty( ->setNestedSchema($mergedPropertySchema); } + /** + * Check if multiple $compositionProperties contain nested schemas. Only in this case a merged property must be + * created. If no nested schemas are detected null will be returned. If only one $compositionProperty contains a + * nested schema the $compositionProperty will be used as a replacement for the merged property. + * + * Returns false if a merged property must be created. + * + * @param CompositionPropertyDecorator[] $compositionProperties + * + * @return PropertyInterface|null|false + */ + private function redirectMergedProperty(array $compositionProperties) + { + $redirectToProperty = null; + foreach ($compositionProperties as $property) { + if ($property->getNestedSchema()) { + if ($redirectToProperty !== null) { + return false; + } + + $redirectToProperty = $property; + } + } + + return $redirectToProperty; + } + /** * @param Schema $mergedPropertySchema - * @param PropertyInterface[] $properties + * @param PropertyInterface[] $compositionProperties */ - protected function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, array $properties): void + private function transferPropertiesToMergedSchema(Schema $mergedPropertySchema, array $compositionProperties): void { - foreach ($properties as $property) { + foreach ($compositionProperties as $property) { if (!$property->getNestedSchema()) { continue; } diff --git a/src/PropertyProcessor/ComposedValueProcessorFactory.php b/src/PropertyProcessor/ComposedValueProcessorFactory.php index 88f810b..7f367c1 100644 --- a/src/PropertyProcessor/ComposedValueProcessorFactory.php +++ b/src/PropertyProcessor/ComposedValueProcessorFactory.php @@ -13,6 +13,19 @@ */ class ComposedValueProcessorFactory implements ProcessorFactoryInterface { + /** @var bool */ + private $rootLevelComposition; + + /** + * ComposedValueProcessorFactory constructor. + * + * @param bool $rootLevelComposition is the composed value on object root level (true) or on property level (false)? + */ + public function __construct(bool $rootLevelComposition) + { + $this->rootLevelComposition = $rootLevelComposition; + } + /** * @inheritdoc * @@ -29,6 +42,6 @@ public function getProcessor( throw new SchemaException("Unsupported composed value type $type"); } - return new $processor($propertyMetaDataCollection, $schemaProcessor, $schema); + return new $processor($propertyMetaDataCollection, $schemaProcessor, $schema, $this->rootLevelComposition); } } diff --git a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php index bdfb1ef..89e4417 100644 --- a/src/PropertyProcessor/Property/AbstractPropertyProcessor.php +++ b/src/PropertyProcessor/Property/AbstractPropertyProcessor.php @@ -5,6 +5,7 @@ namespace PHPModelGenerator\PropertyProcessor\Property; use PHPModelGenerator\Exception\SchemaException; +use PHPModelGenerator\Model\Property\BaseProperty; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Schema; use PHPModelGenerator\Model\Validator\EnumValidator; @@ -161,7 +162,7 @@ private function transferDependendPropertiesToBaseSchema(Schema $dependencySchem protected function addComposedValueValidator(PropertyInterface $property, array $propertyData): void { $composedValueKeywords = ['allOf', 'anyOf', 'oneOf', 'not', 'if']; - $propertyFactory = new PropertyFactory(new ComposedValueProcessorFactory()); + $propertyFactory = new PropertyFactory(new ComposedValueProcessorFactory($property instanceof BaseProperty)); foreach ($composedValueKeywords as $composedValueKeyword) { if (!isset($propertyData[$composedValueKeyword])) { @@ -188,10 +189,17 @@ protected function addComposedValueValidator(PropertyInterface $property, array } $property->addTypeHintDecorator(new TypeHintTransferDecorator($composedProperty)); + + if (!$property->getType() && $composedProperty->getType()) { + $property->setType($composedProperty->getType(), $composedProperty->getType(true)); + } } } /** + * If the type of a property containing a composition is defined outside of the composition make sure each + * composition which doesn't define a type inherits the type + * * @param array $propertyData * @param string $composedValueKeyword * diff --git a/src/PropertyProcessor/Property/BaseProcessor.php b/src/PropertyProcessor/Property/BaseProcessor.php index 3d2a3f4..0f6b54e 100644 --- a/src/PropertyProcessor/Property/BaseProcessor.php +++ b/src/PropertyProcessor/Property/BaseProcessor.php @@ -11,7 +11,7 @@ use PHPModelGenerator\Exception\Object\MaxPropertiesException; use PHPModelGenerator\Exception\Object\MinPropertiesException; use PHPModelGenerator\Exception\SchemaException; -use PHPModelGenerator\Model\Property\Property; +use PHPModelGenerator\Model\Property\BaseProperty; use PHPModelGenerator\Model\Property\PropertyInterface; use PHPModelGenerator\Model\Validator; use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator; @@ -53,7 +53,7 @@ public function process(string $propertyName, array $propertyData): PropertyInte ->setUpDefinitionDictionary($propertyData, $this->schemaProcessor, $this->schema); // create a property which is used to gather composed properties validators. - $property = new Property($propertyName, static::TYPE); + $property = new BaseProperty($propertyName, static::TYPE); $this->generateValidators($property, $propertyData); $this->addPropertyNamesValidator($propertyData); diff --git a/src/Templates/Model.phptpl b/src/Templates/Model.phptpl index bb20d9a..9d5de6d 100644 --- a/src/Templates/Model.phptpl +++ b/src/Templates/Model.phptpl @@ -102,7 +102,7 @@ class {{ class }} implements \PHPModelGenerator\Interfaces\JSONModelInterface * @return {{ property.getTypeHint(true) }}{% if viewHelper.implicitNull(property) %}|null{% endif %} */ public function get{{ viewHelper.ucfirst(property.getAttribute()) }}() - {% if property.getType(true) %}: {% if not property.isRequired() %}?{% endif %}{{ property.getType(true) }}{% endif %} + {% if property.getType(true) %}: {% if viewHelper.implicitNull(property) %}?{% endif %}{{ property.getType(true) }}{% endif %} { return $this->{{ property.getAttribute() }}; } diff --git a/tests/AbstractPHPModelGeneratorTest.php b/tests/AbstractPHPModelGeneratorTest.php index 0c33936..85267d1 100644 --- a/tests/AbstractPHPModelGeneratorTest.php +++ b/tests/AbstractPHPModelGeneratorTest.php @@ -403,12 +403,12 @@ public function namespaceDataProvider(): array /** * Get the annotated type for an object property * - * @param object $object + * @param string|object $object * @param string $property * * @return string */ - protected function getPropertyType(object $object, string $property): string + protected function getPropertyType($object, string $property): string { $matches = []; preg_match( @@ -423,12 +423,12 @@ protected function getPropertyType(object $object, string $property): string /** * Get the annotated return type for an object method * - * @param object $object + * @param string|object $object * @param string $method * * @return string */ - protected function getMethodReturnType(object $object, string $method): string + protected function getMethodReturnType($object, string $method): string { $matches = []; preg_match( @@ -440,6 +440,11 @@ protected function getMethodReturnType(object $object, string $method): string return $matches[1]; } + protected function getGeneratedFiles(): array + { + return $this->generatedFiles; + } + /** * Generate a unique name for a class * diff --git a/tests/Basic/BasicSchemaGenerationTest.php b/tests/Basic/BasicSchemaGenerationTest.php index c54581f..361ce60 100644 --- a/tests/Basic/BasicSchemaGenerationTest.php +++ b/tests/Basic/BasicSchemaGenerationTest.php @@ -9,6 +9,7 @@ use PHPModelGenerator\Interfaces\SerializationInterface; use PHPModelGenerator\Model\GeneratorConfiguration; use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest; +use ReflectionMethod; /** * Class BasicSchemaGenerationTest @@ -17,11 +18,19 @@ */ class BasicSchemaGenerationTest extends AbstractPHPModelGeneratorTest { - public function testGetterAndSetterAreGeneratedForMutableObjects(): void + /** + * @dataProvider implicitNullDataProvider + * + * @param bool $implicitNull + * @param bool $nullable + */ + public function testGetterAndSetterAreGeneratedForMutableObjects(bool $implicitNull): void { $className = $this->generateClassFromFile( 'BasicSchema.json', - (new GeneratorConfiguration())->setImmutable(false) + (new GeneratorConfiguration())->setImmutable(false), + false, + $implicitNull ); $object = new $className(['property' => 'Hello']); @@ -29,6 +38,18 @@ public function testGetterAndSetterAreGeneratedForMutableObjects(): void $this->assertTrue(is_callable([$object, 'getProperty'])); $this->assertTrue(is_callable([$object, 'setProperty'])); $this->assertSame('Hello', $object->getProperty()); + + $this->assertSame($object, $object->setProperty('Bye')); + $this->assertSame('Bye', $object->getProperty()); + + // test if the property is typed correctly + $returnType = (new ReflectionMethod($object, 'getProperty'))->getReturnType(); + $this->assertSame('string', $returnType->getName()); + $this->assertSame($implicitNull, $returnType->allowsNull()); + + $setType = (new ReflectionMethod($object, 'setProperty'))->getParameters()[0]->getType(); + $this->assertSame('string', $setType->getName()); + $this->assertSame($implicitNull, $setType->allowsNull()); } public function testGetterAndSetterAreNotGeneratedByDefault(): void diff --git a/tests/ComposedValue/ComposedAllOfTest.php b/tests/ComposedValue/ComposedAllOfTest.php index 7a8543b..90c7fde 100644 --- a/tests/ComposedValue/ComposedAllOfTest.php +++ b/tests/ComposedValue/ComposedAllOfTest.php @@ -4,6 +4,7 @@ use PHPModelGenerator\Exception\ValidationException; use PHPModelGenerator\Tests\AbstractPHPModelGeneratorTest; +use ReflectionMethod; use stdClass; /** @@ -55,6 +56,7 @@ public function propertyLevelAllOfSchemaFileDataProvider(): array { return [ 'Property level composition' => ['ExtendedPropertyDefinition.json'], + 'Property level composition 2' => ['ComposedPropertyDefinition.json'], 'Multiple objects' => ['ReferencedObjectSchema.json'], 'Empty all of' => ['EmptyAllOf.json'], ]; @@ -97,6 +99,67 @@ public function testAllOfTypePropertyHasTypeAnnotation(): void $this->assertRegExp($regexp, $this->getPropertyType($object, 'property')); $this->assertRegExp($regexp, $this->getMethodReturnType($object, 'getProperty')); + + // base class, merged property class and two classes for validating the composition components + $this->assertCount(4, $this->getGeneratedFiles()); + } + + /** + * @dataProvider validComposedPropertyDataProvider + * + * @param int|null $propertyValue + */ + public function testComposedPropertyDefinitionWithValidValues(?int $propertyValue): void + { + $className = $this->generateClassFromFile('ComposedPropertyDefinition.json'); + + $object = new $className(['property' => $propertyValue]); + $this->assertSame($propertyValue, $object->getProperty()); + + // check if no merged property is created + $this->assertCount(1, $this->getGeneratedFiles()); + + // test if the property is typed correctly + $returnType = (new ReflectionMethod($object, 'getProperty'))->getReturnType(); + $this->assertSame('int', $returnType->getName()); + $this->assertTrue($returnType->allowsNull()); + } + + public function validComposedPropertyDataProvider(): array + { + return [ + 'null' => [null], + 'int 5' => [5], + 'int 10' => [10], + ]; + } + + /** + * @dataProvider invalidComposedPropertyDataProvider + * + * @param $propertyValue + * @param string $exceptionMessage + */ + public function testComposedPropertyDefinitionWithInvalidValuesThrowsAnException( + $propertyValue, + string $exceptionMessage + ): void { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage($exceptionMessage); + + $className = $this->generateClassFromFile('ComposedPropertyDefinition.json'); + + new $className(['property' => $propertyValue]); + } + + public function invalidComposedPropertyDataProvider(): array + { + return [ + 'one match - int 4' => [4, 'Invalid value for property declined by composition constraint'], + 'one match - int 11' => [11, 'Invalid value for property declined by composition constraint'], + 'int -1' => [-1, 'Invalid value for property declined by composition constraint'], + 'int 20' => [20, 'Invalid value for property declined by composition constraint'], + ]; } /** @@ -111,6 +174,14 @@ public function testExtendedPropertyDefinitionWithValidValues($propertyValue): v $object = new $className(['property' => $propertyValue]); // cast expected to float as an int is casted to an float internally for a number property $this->assertSame(is_int($propertyValue) ? (float) $propertyValue : $propertyValue, $object->getProperty()); + + // check if no merged property is created + $this->assertCount(1, $this->getGeneratedFiles()); + + // test if the property is typed correctly + $returnType = (new ReflectionMethod($object, 'getProperty'))->getReturnType(); + $this->assertSame('float', $returnType->getName()); + $this->assertTrue($returnType->allowsNull()); } public function validExtendedPropertyDataProvider(): array @@ -262,6 +333,9 @@ public function testMatchingPropertyForComposedAllOfObjectIsValid( $object = new $className($input); $this->assertSame($stringPropertyValue, $object->getStringProperty()); $this->assertSame($intPropertyValue, $object->getIntegerProperty()); + + // base class and two classes for validating the composition components + $this->assertCount(3, $this->getGeneratedFiles()); } public function validComposedObjectDataProvider(): array diff --git a/tests/ComposedValue/ComposedAnyOfTest.php b/tests/ComposedValue/ComposedAnyOfTest.php index 3d46f15..cf47c5b 100644 --- a/tests/ComposedValue/ComposedAnyOfTest.php +++ b/tests/ComposedValue/ComposedAnyOfTest.php @@ -69,8 +69,9 @@ public function propertyLevelAnyOfSchemaFileDataProvider(): array return [ 'Scalar types' => ['AnyOfType.json'], 'Property level composition' => ['ExtendedPropertyDefinition.json'], - 'Object with scalar type' => ['ReferencedObjectSchema.json'], + 'Object with scalar type and one object' => ['ReferencedObjectSchema.json'], 'Multiple objects' => ['ReferencedObjectSchema2.json'], + 'Object with scalar type and multiple objects' => ['ReferencedObjectSchema3.json'], 'Empty any of' => ['EmptyAnyOf.json'], ]; } @@ -122,22 +123,49 @@ public function testValidProvidedAnyOfTypePropertyIsValid($propertyValue): void * * @param string $schema * @param string $annotationPattern + * @param int $generatedClasses */ - public function testAnyOfTypePropertyHasTypeAnnotation(string $schema, string $annotationPattern): void - { + public function testAnyOfTypePropertyHasTypeAnnotation( + string $schema, + string $annotationPattern, + int $generatedClasses + ): void { $className = $this->generateClassFromFile($schema); - $object = new $className([]); - $this->assertRegExp($annotationPattern, $this->getPropertyType($object, 'property')); - $this->assertRegExp($annotationPattern, $this->getMethodReturnType($object, 'getProperty')); + $this->assertRegExp($annotationPattern, $this->getPropertyType($className, 'property')); + $this->assertRegExp($annotationPattern, $this->getMethodReturnType($className, 'getProperty')); + + $this->assertCount($generatedClasses, $this->getGeneratedFiles()); } public function annotationDataProvider(): array { return [ - 'Multiple scalar types' => ['AnyOfType.json', '/string\|int\|bool/'], - 'Object with scalar type' => ['ReferencedObjectSchema.json', '/string\|Composed[\w]*_Merged_[\w]*/'], - 'Multiple objects' => ['ReferencedObjectSchema2.json', '/ComposedAnyOfTest[\w]*_Merged_[\w]*/'] + 'Multiple scalar types (no merged property)' => [ + 'AnyOfType.json', + '/^string\|int\|bool\|null$/', + 1, + ], + 'Multiple scalar types required (no merged property)' => [ + 'AnyOfTypeRequired.json', + '/^string\|int\|bool$/', + 1, + ], + 'Object with scalar type (no merged property - redirect to generated object)' => [ + 'ReferencedObjectSchema.json', + '/^string\|ComposedAnyOfTest[\w]*Property[\w]*\|null$/', + 2, + ], + 'Multiple objects (merged property created)' => [ + 'ReferencedObjectSchema2.json', + '/^ComposedAnyOfTest[\w]*_Merged_[\w]*\|null$/', + 4, + ], + 'Scalar type and multiple objects (merged property created)' => [ + 'ReferencedObjectSchema3.json', + '/^string\|ComposedAnyOfTest[\w]*_Merged_[\w]*\|null$/', + 4, + ], ]; } @@ -274,11 +302,12 @@ public function invalidExtendedPropertyDataProvider(): array /** * @dataProvider composedPropertyWithReferencedSchemaDataProvider * + * @param string $schema * @param $propertyValue */ - public function testMatchingComposedPropertyWithReferencedSchemaIsValid($propertyValue): void + public function testMatchingComposedPropertyWithReferencedSchemaIsValid(string $schema, $propertyValue): void { - $className = $this->generateClassFromFile('ReferencedObjectSchema.json'); + $className = $this->generateClassFromFile($schema); $object = new $className(['property' => $propertyValue]); $this->assertSame($propertyValue, $object->getProperty()); @@ -286,10 +315,16 @@ public function testMatchingComposedPropertyWithReferencedSchemaIsValid($propert public function composedPropertyWithReferencedSchemaDataProvider(): array { - return [ - 'null' => [null], - 'string matching required length' => ['Hanne'], - ]; + return $this->combineDataProvider( + [ + 'ReferencedObjectSchema.json' => ['ReferencedObjectSchema.json'], + 'ReferencedObjectSchema3.json' => ['ReferencedObjectSchema3.json'], + ], + [ + 'null' => [null], + 'string matching required length' => ['Hanne'], + ] + ); } /** @@ -313,12 +348,26 @@ public function referencedPersonDataProvider(): array return [ 'ReferencedObjectSchema.json' => ['ReferencedObjectSchema.json'], 'ReferencedObjectSchema2.json' => ['ReferencedObjectSchema2.json'], + 'ReferencedObjectSchema3.json' => ['ReferencedObjectSchema3.json'], ]; } - public function testMatchingObjectPropertyWithReferencedPetSchemaIsValid(): void + public function referencedPetDataProvider(): array { - $className = $this->generateClassFromFile('ReferencedObjectSchema2.json'); + return [ + 'ReferencedObjectSchema2.json' => ['ReferencedObjectSchema2.json'], + 'ReferencedObjectSchema3.json' => ['ReferencedObjectSchema3.json'], + ]; + } + + /** + * @dataProvider referencedPetDataProvider + * + * @param string $schema + */ + public function testMatchingObjectPropertyWithReferencedPetSchemaIsValid(string $schema): void + { + $className = $this->generateClassFromFile($schema); $object = new $className(['property' => ['race' => 'Horse']]); @@ -367,14 +416,17 @@ public function invalidObjectPropertyWithReferencedPersonSchemaDataProvider(): a /** * @dataProvider invalidObjectPropertyWithReferencedPetSchemaDataProvider * + * @param string $schema * @param $propertyValue */ - public function testNotMatchingObjectPropertyWithReferencedPetSchemaThrowsAnException($propertyValue): void - { + public function testNotMatchingObjectPropertyWithReferencedPetSchemaThrowsAnException( + string $schema, + $propertyValue + ): void { $this->expectException(ValidationException::class); $this->expectExceptionMessage('Invalid value for property declined by composition constraint'); - $className = $this->generateClassFromFile('ReferencedObjectSchema2.json'); + $className = $this->generateClassFromFile($schema); new $className(['property' => $propertyValue]); } @@ -382,13 +434,14 @@ public function testNotMatchingObjectPropertyWithReferencedPetSchemaThrowsAnExce public function invalidObjectPropertyWithReferencedPetSchemaDataProvider(): array { return $this->combineDataProvider( - $this->referencedPersonDataProvider(), + $this->referencedPetDataProvider(), [ 'int' => [0], 'float' => [0.92], 'bool' => [true], 'object' => [new stdClass()], - 'string' => ['Horse'], + // a string is allowed by ReferencedObjectSchema3 but must be declined due to length violation + 'string' => ['Cat'], 'empty array' => [[]], 'Too many properties' => [['race' => 'Horse', 'alive' => true]], 'Matching object with invalid type' => [['race' => 123]], diff --git a/tests/Schema/ComposedAllOfTest/ComposedPropertyDefinition.json b/tests/Schema/ComposedAllOfTest/ComposedPropertyDefinition.json new file mode 100644 index 0000000..1982cca --- /dev/null +++ b/tests/Schema/ComposedAllOfTest/ComposedPropertyDefinition.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "property": { + "allOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 10 + }, + { + "type": "integer", + "minimum": 5, + "maximum": 15 + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Schema/ComposedAnyOfTest/ReferencedObjectSchema3.json b/tests/Schema/ComposedAnyOfTest/ReferencedObjectSchema3.json new file mode 100644 index 0000000..13db964 --- /dev/null +++ b/tests/Schema/ComposedAnyOfTest/ReferencedObjectSchema3.json @@ -0,0 +1,51 @@ +{ + "definitions": { + "person": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2 + }, + "age": { + "type": "integer" + } + }, + "required": [ + "name", + "age" + ], + "additionalProperties": false + }, + "pet": { + "type": "object", + "properties": { + "race": { + "type": "string", + "minLength": 2 + } + }, + "required": [ + "race" + ], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "property": { + "anyOf": [ + { + "$ref": "#/definitions/person" + }, + { + "$ref": "#/definitions/pet" + }, + { + "type": "string", + "minLength": 5 + } + ] + } + } +} \ No newline at end of file