diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 2878240e3b5..2cbf3b33c45 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -36,6 +36,7 @@ Feature: Documentation support And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists + And the OpenAPI class "Person" exists And the OpenAPI class "RelatedDummy" exists And the OpenAPI class "NoCollectionDummy" exists And the OpenAPI class "RelatedToDummyFriend" exists @@ -57,6 +58,21 @@ Feature: Documentation support # Properties And the "id" property exists for the OpenAPI class "Dummy" And the "name" property is required for the OpenAPI class "Dummy" + And the "genderType" property exists for the OpenAPI class "Person" + And the "genderType" property for the OpenAPI class "Person" should be equal to: + """ + { + "default": "male", + "example": "male", + "type": "string", + "enum": [ + "male", + "female", + null + ], + "nullable": true + } + """ # Enable these tests when SF 4.4 / PHP 7.1 support is dropped #And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean" #And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 12a2bdc85c9..b671cb89893 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -83,3 +83,5 @@ parameters: - message: '#^Property .+ is unused.$#' path: tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineDummy.php + # Waiting for https://github.com/laminas/laminas-code/pull/150 + - '#Call to an undefined method ReflectionEnum::.+#' diff --git a/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php b/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php index 9969c8606fd..93ce4a4d5d0 100644 --- a/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php +++ b/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php @@ -92,6 +92,10 @@ public function getTypes($class, $property, array $context = []): ?array if ($metadata->hasField($property)) { $typeOfField = $metadata->getTypeOfField($property); $nullable = $metadata instanceof MongoDbClassMetadata && $metadata->isNullable($property); + $enumType = null; + if (null !== $enumClass = $metadata instanceof MongoDbClassMetadata ? $metadata->getFieldMapping($property)['enumType'] ?? null : null) { + $enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); + } switch ($typeOfField) { case MongoDbType::DATE: @@ -102,11 +106,16 @@ public function getTypes($class, $property, array $context = []): ?array return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; case MongoDbType::COLLECTION: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT))]; - default: - $builtinType = $this->getPhpType($typeOfField); - - return $builtinType ? [new Type($builtinType, $nullable)] : null; + case MongoDbType::INT: + case MongoDbType::STRING: + if ($enumType) { + return [$enumType]; + } } + + $builtinType = $this->getPhpType($typeOfField); + + return $builtinType ? [new Type($builtinType, $nullable)] : null; } return null; diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 1cdd167e739..aee436fb591 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -175,6 +175,9 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } if (!isset($propertySchema['default']) && !empty($default = $propertyMetadata->getDefault())) { + if ($default instanceof \BackedEnum) { + $default = $default->value; + } $propertySchema['default'] = $default; } diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php index a96c5549d8e..e67d510b3ce 100644 --- a/src/JsonSchema/TypeFactory.php +++ b/src/JsonSchema/TypeFactory.php @@ -72,7 +72,7 @@ private function makeBasicType(Type $type, string $format = 'json', ?bool $reada Type::BUILTIN_TYPE_INT => ['type' => 'integer'], Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'], Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], - Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $format, $readableLink, $serializerContext, $schema), + Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $format, $readableLink, $serializerContext, $schema), default => ['type' => 'string'], }; } @@ -80,7 +80,7 @@ private function makeBasicType(Type $type, string $format = 'json', ?bool $reada /** * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided. */ - private function getClassType(?string $className, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array + private function getClassType(?string $className, bool $nullable, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array { if (null === $className) { return ['type' => 'string']; @@ -116,6 +116,18 @@ private function getClassType(?string $className, string $format, ?bool $readabl 'format' => 'binary', ]; } + if (is_a($className, \BackedEnum::class, true)) { + $rEnum = new \ReflectionEnum($className); + $enumCases = array_map(static fn (\ReflectionEnumBackedCase $rCase) => $rCase->getBackingValue(), $rEnum->getCases()); + if ($nullable) { + $enumCases[] = null; + } + + return [ + 'type' => (string) $rEnum->getBackingType(), + 'enum' => $enumCases, + ]; + } // Skip if $schema is null (filters only support basic types) if (null === $schema) { diff --git a/tests/Behat/OpenApiContext.php b/tests/Behat/OpenApiContext.php index 6865763e9be..c651149e3ff 100644 --- a/tests/Behat/OpenApiContext.php +++ b/tests/Behat/OpenApiContext.php @@ -16,7 +16,9 @@ use Behat\Behat\Context\Context; use Behat\Behat\Context\Environment\InitializedContextEnvironment; use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\PyStringNode; use Behatch\Context\RestContext; +use Behatch\Json\Json; use PHPUnit\Framework\Assert; use PHPUnit\Framework\ExpectationFailedException; @@ -42,51 +44,25 @@ public function gatherContexts(BeforeScenarioScope $scope): void $this->restContext = $restContext; } - /** - * @Then the Swagger class :class exists - */ - public function assertTheSwaggerClassExist(string $className): void - { - try { - $this->getClassInfo($className); - } catch (\InvalidArgumentException $e) { - throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e); - } - } - /** * @Then the OpenAPI class :class exists */ public function assertTheOpenApiClassExist(string $className): void { try { - $this->getClassInfo($className, 3); + $this->getClassInfo($className); } catch (\InvalidArgumentException $e) { throw new ExpectationFailedException(sprintf('The class "%s" doesn\'t exist.', $className), null, $e); } } - /** - * @Then the Swagger class :class doesn't exist - */ - public function assertTheSwaggerClassNotExist(string $className): void - { - try { - $this->getClassInfo($className); - } catch (\InvalidArgumentException) { - return; - } - - throw new ExpectationFailedException(sprintf('The class "%s" exists.', $className)); - } - /** * @Then the OpenAPI class :class doesn't exist */ public function assertTheOpenAPIClassNotExist(string $className): void { try { - $this->getClassInfo($className, 3); + $this->getClassInfo($className); } catch (\InvalidArgumentException) { return; } @@ -95,7 +71,6 @@ public function assertTheOpenAPIClassNotExist(string $className): void } /** - * @Then the Swagger path :arg1 exists * @Then the OpenAPI path :arg1 exists */ public function assertThePathExist(string $path): void @@ -105,54 +80,32 @@ public function assertThePathExist(string $path): void Assert::assertTrue(isset($json->paths) && isset($json->paths->{$path})); } - /** - * @Then the :prop property exists for the Swagger class :class - */ - public function assertThePropertyExistForTheSwaggerClass(string $propertyName, string $className): void - { - try { - $this->getPropertyInfo($propertyName, $className); - } catch (\InvalidArgumentException $e) { - throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e); - } - } - /** * @Then the :prop property exists for the OpenAPI class :class */ public function assertThePropertyExistForTheOpenApiClass(string $propertyName, string $className): void { try { - $this->getPropertyInfo($propertyName, $className, 3); + $this->getPropertyInfo($propertyName, $className); } catch (\InvalidArgumentException $e) { throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" doesn\'t exist.', $propertyName, $className), null, $e); } } - /** - * @Then the :prop property is required for the Swagger class :class - */ - public function assertThePropertyIsRequiredForTheSwaggerClass(string $propertyName, string $className): void - { - if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) { - throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className)); - } - } - /** * @Then the :prop property is required for the OpenAPI class :class */ public function assertThePropertyIsRequiredForTheOpenAPIClass(string $propertyName, string $className): void { - if (!\in_array($propertyName, $this->getClassInfo($className, 3)->required, true)) { + if (!\in_array($propertyName, $this->getClassInfo($className)->required, true)) { throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should be required', $propertyName, $className)); } } /** - * @Then the :prop property is not read only for the Swagger class :class + * @Then the :prop property is not read only for the OpenAPI class :class */ - public function assertThePropertyIsNotReadOnlyForTheSwaggerClass(string $propertyName, string $className): void + public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void { $propertyInfo = $this->getPropertyInfo($propertyName, $className); if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) { @@ -161,13 +114,15 @@ public function assertThePropertyIsNotReadOnlyForTheSwaggerClass(string $propert } /** - * @Then the :prop property is not read only for the OpenAPI class :class + * @Then the :prop property for the OpenAPI class :class should be equal to: */ - public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propertyName, string $className): void + public function assertThePropertyForTheOpenAPIClassShouldBeEqualTo(string $propertyName, string $className, PyStringNode $propertyContent): void { - $propertyInfo = $this->getPropertyInfo($propertyName, $className, 3); - if (property_exists($propertyInfo, 'readOnly') && $propertyInfo->readOnly) { - throw new ExpectationFailedException(sprintf('Property "%s" of class "%s" should not be read only', $propertyName, $className)); + $propertyInfo = $this->getPropertyInfo($propertyName, $className); + $propertyInfoJson = new Json(json_encode($propertyInfo)); + + if (new Json($propertyContent) != $propertyInfoJson) { + throw new ExpectationFailedException(sprintf("Property \"%s\" of class \"%s\" is '%s'", $propertyName, $className, $propertyInfoJson)); } } @@ -176,12 +131,10 @@ public function assertThePropertyIsNotReadOnlyForTheOpenAPIClass(string $propert * * @throws \InvalidArgumentException */ - private function getPropertyInfo(string $propertyName, string $className, int $specVersion = 2): \stdClass + private function getPropertyInfo(string $propertyName, string $className): \stdClass { - /** - * @var iterable $properties - */ - $properties = $this->getProperties($className, $specVersion); + /** @var iterable $properties */ + $properties = $this->getProperties($className); foreach ($properties as $classPropertyName => $property) { if ($classPropertyName === $propertyName) { return $property; @@ -194,9 +147,9 @@ private function getPropertyInfo(string $propertyName, string $className, int $s /** * Gets all operations of a given class. */ - private function getProperties(string $className, int $specVersion = 2): \stdClass + private function getProperties(string $className): \stdClass { - return $this->getClassInfo($className, $specVersion)->{'properties'} ?? new \stdClass(); + return $this->getClassInfo($className)->{'properties'} ?? new \stdClass(); } /** @@ -204,9 +157,9 @@ private function getProperties(string $className, int $specVersion = 2): \stdCla * * @throws \InvalidArgumentException */ - private function getClassInfo(string $className, int $specVersion = 2): \stdClass + private function getClassInfo(string $className): \stdClass { - $nodes = 2 === $specVersion ? $this->getLastJsonResponse()->{'definitions'} : $this->getLastJsonResponse()->{'components'}->{'schemas'}; + $nodes = $this->getLastJsonResponse()->{'components'}->{'schemas'}; foreach ($nodes as $classTitle => $classData) { if ($classTitle === $className) { return $classData; diff --git a/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php b/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php index 50c697632e2..84b10c33b8e 100644 --- a/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php +++ b/tests/Doctrine/Odm/PropertyInfo/DoctrineExtractorTest.php @@ -17,10 +17,13 @@ use ApiPlatform\Test\DoctrineMongoDbOdmSetup; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineDummy; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineEmbeddable; +use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineEnum; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineFooType; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineGeneratedValue; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineRelation; use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\DoctrineWithEmbedded; +use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\EnumInt; +use ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures\EnumString; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -128,6 +131,13 @@ public function testExtractWithEmbedMany(): void $this->assertEquals($expectedTypes, $actualTypes); } + public function testExtractEnum(): void + { + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString')); + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt')); + $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom')); + } + public function typesProvider(): array { return [ diff --git a/tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php b/tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php new file mode 100644 index 00000000000..57efa0ec47e --- /dev/null +++ b/tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineEnum.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures; + +use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Field; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Id; + +/** + * @author Alan Poulain + */ +#[Document] +class DoctrineEnum +{ + #[Id] + public int $id; + + #[Field(enumType: EnumString::class)] + protected EnumString $enumString; + + #[Field(type: 'int', enumType: EnumInt::class)] + protected EnumInt $enumInt; + + #[Field(type: 'custom_foo', enumType: EnumInt::class)] + protected EnumInt $enumCustom; +} diff --git a/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php new file mode 100644 index 00000000000..0fc31cff2a5 --- /dev/null +++ b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumInt.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures; + +enum EnumInt: int +{ + case Foo = 0; + case Bar = 1; +} diff --git a/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php new file mode 100644 index 00000000000..f96c6e29bd3 --- /dev/null +++ b/tests/Doctrine/Odm/PropertyInfo/Fixtures/EnumString.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Doctrine\Odm\PropertyInfo\Fixtures; + +enum EnumString: string +{ + case Foo = 'f'; + case Bar = 'b'; +} diff --git a/tests/Fixtures/TestBundle/Document/Person.php b/tests/Fixtures/TestBundle/Document/Person.php index 0e4f098366a..f7d11a309aa 100644 --- a/tests/Fixtures/TestBundle/Document/Person.php +++ b/tests/Fixtures/TestBundle/Document/Person.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; @@ -29,13 +30,19 @@ class Person { #[ODM\Id(strategy: 'INCREMENT', type: 'int')] - private $id; + private ?int $id = null; + #[Groups(['people.pets'])] #[ODM\Field(type: 'string')] - public $name; + public string $name; + + #[ODM\Field(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; + #[Groups(['people.pets'])] #[ODM\ReferenceMany(targetDocument: PersonToPet::class, mappedBy: 'person')] public Collection|iterable $pets; + #[ODM\ReferenceMany(targetDocument: Greeting::class, mappedBy: 'sender')] public Collection|iterable|null $sentGreetings = null; @@ -44,7 +51,7 @@ public function __construct() $this->pets = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/tests/Fixtures/TestBundle/Entity/Person.php b/tests/Fixtures/TestBundle/Entity/Person.php index 59b452b8103..775969b2786 100644 --- a/tests/Fixtures/TestBundle/Entity/Person.php +++ b/tests/Fixtures/TestBundle/Entity/Person.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -31,13 +32,19 @@ class Person #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue(strategy: 'AUTO')] - private $id; + private ?int $id = null; + + #[ORM\Column(type: 'string', enumType: GenderTypeEnum::class, nullable: true)] + public ?GenderTypeEnum $genderType = GenderTypeEnum::MALE; + #[ORM\Column(type: 'string')] #[Groups(['people.pets'])] - public $name; + public string $name; + #[ORM\OneToMany(targetEntity: PersonToPet::class, mappedBy: 'person')] #[Groups(['people.pets'])] public Collection|iterable $pets; + #[ORM\OneToMany(targetEntity: Greeting::class, mappedBy: 'sender')] public Collection|iterable|null $sentGreetings = null; @@ -46,7 +53,7 @@ public function __construct() $this->pets = new ArrayCollection(); } - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php new file mode 100644 index 00000000000..12327a2e6df --- /dev/null +++ b/tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Enum; + +enum GenderTypeEnum: string +{ + case MALE = 'male'; + case FEMALE = 'female'; +} diff --git a/tests/JsonSchema/SchemaFactoryTest.php b/tests/JsonSchema/SchemaFactoryTest.php index a7f75ff6ece..062a16a8566 100644 --- a/tests/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonSchema/SchemaFactoryTest.php @@ -28,6 +28,7 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\NotAResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OverriddenOperationDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -53,15 +54,22 @@ public function testBuildSchemaForNonResourceClass(): void ), Argument::cetera())->willReturn([ 'type' => 'integer', ]); + $typeFactoryProphecy->getType(Argument::allOf( + Argument::type(Type::class), + Argument::which('getBuiltinType', Type::BUILTIN_TYPE_OBJECT) + ), Argument::cetera())->willReturn([ + 'type' => 'object', + ]); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar'])); + $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar', 'genderType'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'foo', Argument::cetera())->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'bar', Argument::cetera())->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(true)->withDefault('default_bar')->withExample('example_bar')); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'genderType', Argument::cetera())->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT)])->withReadable(true)->withDefault(GenderTypeEnum::MALE)); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); @@ -91,6 +99,14 @@ public function testBuildSchemaForNonResourceClass(): void $this->assertSame('integer', $definitions[$rootDefinitionKey]['properties']['bar']['type']); $this->assertSame('default_bar', $definitions[$rootDefinitionKey]['properties']['bar']['default']); $this->assertSame('example_bar', $definitions[$rootDefinitionKey]['properties']['bar']['example']); + + $this->assertArrayHasKey('genderType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayHasKey('default', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayHasKey('example', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); + $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['default']); + $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['example']); } public function testBuildSchemaWithSerializerGroups(): void diff --git a/tests/JsonSchema/TypeFactoryTest.php b/tests/JsonSchema/TypeFactoryTest.php index 0b29bce5e10..418139459ef 100644 --- a/tests/JsonSchema/TypeFactoryTest.php +++ b/tests/JsonSchema/TypeFactoryTest.php @@ -17,6 +17,7 @@ use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -53,6 +54,8 @@ public function typeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['nullable' => true, 'type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['nullable' => true, 'type' => 'array', 'items' => ['type' => 'string']], @@ -174,6 +177,8 @@ public function jsonSchemaTypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => ['string', 'null'], 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield [['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable' => [ ['type' => ['array', 'null'], 'items' => ['type' => 'string']], @@ -288,6 +293,8 @@ public function openAPIV2TypeProvider(): iterable yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; yield [['type' => 'string', 'format' => 'iri-reference'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; + yield [['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; yield 'array can be itself nullable, but ignored in OpenAPI V2' => [ ['type' => 'array', 'items' => ['type' => 'string']],