From e39f8df3ce98cd4b7a71d885343265194b002230 Mon Sep 17 00:00:00 2001 From: Gwendolen Lynch Date: Mon, 1 Apr 2024 14:35:32 +0200 Subject: [PATCH 1/6] fix(jsonapi): add missing "included" schema parts --- src/JsonApi/JsonSchema/SchemaFactory.php | 44 ++++++++++++++++--- .../Command/JsonSchemaGenerateCommandTest.php | 12 +++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index b48b91799cf..2e3bca6db40 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -128,7 +128,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin return $schema; } - $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $schema, $serializerContext); + $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $schema, $serializerContext); if ($schema->getRootDefinitionKey()) { return $schema; @@ -166,17 +166,24 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void } } - private function buildDefinitionPropertiesSchema(string $key, string $className, Schema $schema, ?array $serializerContext): array + private function buildDefinitionPropertiesSchema(string $key, string $className, string $format, Schema $schema, ?array $serializerContext): array { $definitions = $schema->getDefinitions(); $properties = $definitions[$key]['properties'] ?? []; $attributes = []; $relationships = []; + $relatedDefinitions = []; foreach ($properties as $propertyName => $property) { if ($relation = $this->getRelationship($className, $propertyName, $serializerContext)) { - [$isOne, $isMany] = $relation; + [$isOne, $hasOperations, $relatedClassName] = $relation; + if (false === $hasOperations) { + continue; + } + $relatedDefName = $this->getShortClassName($relatedClassName).('json' === $format ? '' : ".$format"); + $ref = Schema::VERSION_OPENAPI === $schema->getVersion() ? '#/components/schemas/'.$relatedDefName : '#/definitions/'.$relatedDefName; + $relatedDefinitions[$propertyName] = ['$ref' => $ref]; if ($isOne) { $relationships[$propertyName]['properties']['data'] = self::RELATION_PROPS; continue; @@ -197,11 +204,25 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, $replacement = self::PROPERTY_PROPS; $replacement['attributes']['properties'] = $attributes; + $included = []; if (\count($relationships) > 0) { $replacement['relationships'] = [ 'type' => 'object', 'properties' => $relationships, ]; + $included = [ + 'included' => [ + 'description' => 'Related resources requested via the "include" query parameter.', + 'type' => 'array', + 'items' => [ + 'anyOf' => array_values($relatedDefinitions), + ], + 'readOnly' => true, + 'externalDocs' => [ + 'url' => 'https://jsonapi.org/format/#fetching-includes', + ], + ], + ]; } if ($required = $definitions[$key]['required'] ?? null) { @@ -223,7 +244,7 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, 'properties' => $replacement, 'required' => ['type', 'id'], ], - ]; + ] + $included; } private function getRelationship(string $resourceClass, string $property, ?array $serializerContext): ?array @@ -232,6 +253,7 @@ private function getRelationship(string $resourceClass, string $property, ?array $types = $propertyMetadata->getBuiltinTypes() ?? []; $isRelationship = false; $isOne = $isMany = false; + $className = $hasOperations = null; foreach ($types as $type) { if ($type->isCollection()) { @@ -244,8 +266,20 @@ private function getRelationship(string $resourceClass, string $property, ?array continue; } $isRelationship = true; + $resourceMetadata = $this->resourceMetadataFactory->create($className); + $operation = $resourceMetadata->getOperation(); + // @see https://github.com/api-platform/core/issues/5501 + // @see https://github.com/api-platform/core/pull/5722 + $hasOperations ??= $operation->canRead(); } - return $isRelationship ? [$isOne, $isMany] : null; + return $isRelationship ? [$isOne, $hasOperations, $className] : null; + } + + private function getShortClassName(string $fullyQualifiedName): string + { + $parts = explode('\\', $fullyQualifiedName); + + return end($parts); } } diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index 3e4ed9d4843..11a38ed1b63 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -177,4 +177,16 @@ public function testSubSchemaJsonLd(): void $this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends']['properties']); } + + public function testJsonApiIncludesSchema(): void + { + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question', '--type' => 'output', '--format' => 'jsonapi']); + $result = $this->tester->getDisplay(); + $json = json_decode($result, associative: true); + + $this->assertArrayHasKey('answer', $json['definitions']['Question.jsonapi']['properties']['data']['properties']['relationships']['properties']); + $this->assertArrayHasKey('anyOf', $json['definitions']['Question.jsonapi']['properties']['included']['items']); + $this->assertArrayHasKey('$ref', $json['definitions']['Question.jsonapi']['properties']['included']['items']['anyOf'][0]); + $this->assertSame('#/definitions/Answer.jsonapi', $json['definitions']['Question.jsonapi']['properties']['included']['items']['anyOf'][0]['$ref']); + } } From 16150eaea1889e5d3df5527f728880c633e4d039 Mon Sep 17 00:00:00 2001 From: Gwendolen Lynch Date: Tue, 2 Apr 2024 11:56:18 +0200 Subject: [PATCH 2/6] fix(test): test correct format --- tests/JsonApi/JsonSchema/SchemaFactoryTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/JsonApi/JsonSchema/SchemaFactoryTest.php b/tests/JsonApi/JsonSchema/SchemaFactoryTest.php index a61649fed73..677c4c79e51 100644 --- a/tests/JsonApi/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonApi/JsonSchema/SchemaFactoryTest.php @@ -81,10 +81,10 @@ public function testBuildSchema(): void public function testCustomFormatBuildSchema(): void { - $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json'); + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonapi'); $this->assertTrue($resultSchema->isDefined()); - $this->assertSame('Dummy', $resultSchema->getRootDefinitionKey()); + $this->assertSame('Dummy.jsonapi', $resultSchema->getRootDefinitionKey()); } public function testHasRootDefinitionKeyBuildSchema(): void From 6eacdcc396f1264f56c0a01a52e3e15a7fa5887e Mon Sep 17 00:00:00 2001 From: Gwendolen Lynch Date: Tue, 2 Apr 2024 17:44:12 +0200 Subject: [PATCH 3/6] chore(jsonschema): refactor definition name logic --- src/JsonApi/JsonSchema/SchemaFactory.php | 32 ++-- src/JsonSchema/DefinitionNameFactory.php | 67 ++++++++ .../DefinitionNameFactoryInterface.php | 33 ++++ src/JsonSchema/ResourceMetadataTrait.php | 105 ++++++++++++ src/JsonSchema/SchemaFactory.php | 152 +++--------------- src/JsonSchema/Tests/SchemaFactoryTest.php | 45 +++++- .../Tests/Factory/OpenApiFactoryTest.php | 12 +- .../Serializer/OpenApiNormalizerTest.php | 12 +- .../Bundle/Resources/config/json_schema.xml | 5 + .../Bundle/Resources/config/jsonapi.xml | 2 + tests/Hal/JsonSchema/SchemaFactoryTest.php | 15 +- tests/Hydra/JsonSchema/SchemaFactoryTest.php | 15 +- .../JsonApi/JsonSchema/SchemaFactoryTest.php | 7 +- .../JsonSchema/DefinitionNameFactoryTest.php | 72 +++++++++ 14 files changed, 406 insertions(+), 168 deletions(-) create mode 100644 src/JsonSchema/DefinitionNameFactory.php create mode 100644 src/JsonSchema/DefinitionNameFactoryInterface.php create mode 100644 src/JsonSchema/ResourceMetadataTrait.php create mode 100644 tests/JsonSchema/DefinitionNameFactoryTest.php diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index 2e3bca6db40..8d68a2f154a 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -14,11 +14,14 @@ namespace ApiPlatform\JsonApi\JsonSchema; use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; +use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; +use ApiPlatform\JsonSchema\ResourceMetadataTrait; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface; use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; /** @@ -28,6 +31,7 @@ */ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { + use ResourceMetadataTrait; private const LINKS_PROPS = [ 'type' => 'object', 'properties' => [ @@ -102,11 +106,13 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI ], ]; - public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver) + public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly ?DefinitionNameFactoryInterface $definitionNameFactory = null) { if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) { $this->schemaFactory->setSchemaFactory($this); } + $this->resourceClassResolver = $resourceClassResolver; + $this->resourceMetadataFactory = $resourceMetadataFactory; } /** @@ -114,10 +120,12 @@ public function __construct(private readonly SchemaFactoryInterface $schemaFacto */ public function buildSchema(string $className, string $format = 'jsonapi', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { - $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); if ('jsonapi' !== $format) { - return $schema; + return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); } + // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources. + // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes + $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, [], $forceCollection); if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) { $definitions = $schema->getDefinitions(); @@ -128,7 +136,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin return $schema; } - $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $schema, $serializerContext); + $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []); if ($schema->getRootDefinitionKey()) { return $schema; @@ -166,7 +174,7 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void } } - private function buildDefinitionPropertiesSchema(string $key, string $className, string $format, Schema $schema, ?array $serializerContext): array + private function buildDefinitionPropertiesSchema(string $key, string $className, string $format, string $type, ?Operation $operation, Schema $schema, ?array $serializerContext): array { $definitions = $schema->getDefinitions(); $properties = $definitions[$key]['properties'] ?? []; @@ -181,8 +189,11 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, continue; } - $relatedDefName = $this->getShortClassName($relatedClassName).('json' === $format ? '' : ".$format"); - $ref = Schema::VERSION_OPENAPI === $schema->getVersion() ? '#/components/schemas/'.$relatedDefName : '#/definitions/'.$relatedDefName; + $operation = $this->findOperation($relatedClassName, $type, $operation, $serializerContext); + $inputOrOutputClass = $this->findOutputClass($relatedClassName, $type, $operation, $serializerContext); + $serializerContext ??= $this->getSerializerContext($operation, $type); + $definitionName = $this->definitionNameFactory->create($relatedClassName, $format, $inputOrOutputClass, $operation, $serializerContext); + $ref = Schema::VERSION_OPENAPI === $schema->getVersion() ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; $relatedDefinitions[$propertyName] = ['$ref' => $ref]; if ($isOne) { $relationships[$propertyName]['properties']['data'] = self::RELATION_PROPS; @@ -275,11 +286,4 @@ private function getRelationship(string $resourceClass, string $property, ?array return $isRelationship ? [$isOne, $hasOperations, $className] : null; } - - private function getShortClassName(string $fullyQualifiedName): string - { - $parts = explode('\\', $fullyQualifiedName); - - return end($parts); - } } diff --git a/src/JsonSchema/DefinitionNameFactory.php b/src/JsonSchema/DefinitionNameFactory.php new file mode 100644 index 00000000000..1def499f946 --- /dev/null +++ b/src/JsonSchema/DefinitionNameFactory.php @@ -0,0 +1,67 @@ + + * + * 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\JsonSchema; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + +final class DefinitionNameFactory implements DefinitionNameFactoryInterface +{ + use ResourceClassInfoTrait; + + public function __construct(private ?array $distinctFormats) + { + } + + /** + * @param class-string $className + */ + public function create(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): string + { + if ($operation) { + $prefix = $operation->getShortName(); + } + + if (!isset($prefix)) { + $prefix = (new \ReflectionClass($className))->getShortName(); + } + + if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) { + $parts = explode('\\', $inputOrOutputClass); + $shortName = end($parts); + $prefix .= '.'.$shortName; + } + + if ('json' !== $format && ($this->distinctFormats[$format] ?? false)) { + // JSON is the default, and so isn't included in the definition name + $prefix .= '.'.$format; + } + + $definitionName = $serializerContext[SchemaFactory::OPENAPI_DEFINITION_NAME] ?? null; + if ($definitionName) { + $name = sprintf('%s-%s', $prefix, $definitionName); + } else { + $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []); + $name = $groups ? sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix; + } + + return $this->encodeDefinitionName($name); + } + + private function encodeDefinitionName(string $name): string + { + return preg_replace('/[^a-zA-Z0-9.\-_]/', '.', $name); + } +} diff --git a/src/JsonSchema/DefinitionNameFactoryInterface.php b/src/JsonSchema/DefinitionNameFactoryInterface.php new file mode 100644 index 00000000000..26de6f1d3f1 --- /dev/null +++ b/src/JsonSchema/DefinitionNameFactoryInterface.php @@ -0,0 +1,33 @@ + + * + * 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\JsonSchema; + +use ApiPlatform\Metadata\Operation; + +/** + * Factory for creating definition names for resources in a JSON Schema document. + * + * @author Gwendolen Lynch + */ +interface DefinitionNameFactoryInterface +{ + /** + * Creates a resource definition name. + * + * @param class-string $className + * + * @return string the definition name + */ + public function create(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): string; +} diff --git a/src/JsonSchema/ResourceMetadataTrait.php b/src/JsonSchema/ResourceMetadataTrait.php new file mode 100644 index 00000000000..51232c33a34 --- /dev/null +++ b/src/JsonSchema/ResourceMetadataTrait.php @@ -0,0 +1,105 @@ + + * + * 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\JsonSchema; + +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; + +/** + * @internal + */ +trait ResourceMetadataTrait +{ + use ResourceClassInfoTrait; + + private function findOutputClass(string $className, string $type, Operation $operation, ?array $serializerContext): ?string + { + $inputOrOutput = ['class' => $className]; + $inputOrOutput = Schema::TYPE_OUTPUT === $type ? ($operation->getOutput() ?? $inputOrOutput) : ($operation->getInput() ?? $inputOrOutput); + $forceSubschema = $serializerContext[SchemaFactory::FORCE_SUBSCHEMA] ?? false; + + return $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null); + } + + private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext): Operation + { + if (null === $operation) { + if (null === $this->resourceMetadataFactory) { + return new HttpOperation(); + } + $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); + + try { + $operation = $resourceMetadataCollection->getOperation(); + } catch (OperationNotFoundException $e) { + $operation = new HttpOperation(); + } + $forceSubschema = $serializerContext[SchemaFactory::FORCE_SUBSCHEMA] ?? false; + if ($operation->getShortName() === $this->getShortClassName($className) && $forceSubschema) { + $operation = new HttpOperation(); + } + + return $this->findOperationForType($resourceMetadataCollection, $type, $operation); + } + + // The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise + if ($this->resourceMetadataFactory && !$operation->getClass()) { + $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); + + if ($operation->getName()) { + return $resourceMetadataCollection->getOperation($operation->getName()); + } + + return $this->findOperationForType($resourceMetadataCollection, $type, $operation); + } + + return $operation; + } + + private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation + { + // Find the operation and use the first one that matches criterias + foreach ($resourceMetadataCollection as $resourceMetadata) { + foreach ($resourceMetadata->getOperations() ?? [] as $op) { + if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { + $operation = $op; + break 2; + } + + if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { + $operation = $op; + break 2; + } + } + } + + return $operation; + } + + private function getSerializerContext(Operation $operation, string $type = Schema::TYPE_OUTPUT): array + { + return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); + } + + private function getShortClassName(string $fullyQualifiedName): string + { + $parts = explode('\\', $fullyQualifiedName); + + return end($parts); + } +} diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 0b40d42036c..d6253f28e8d 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -16,15 +16,12 @@ use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -35,18 +32,21 @@ */ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { - use ResourceClassInfoTrait; + use ResourceMetadataTrait; private ?TypeFactoryInterface $typeFactory = null; private ?SchemaFactoryInterface $schemaFactory = null; // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link'; public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; - public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null) + public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { if ($typeFactory) { $this->typeFactory = $typeFactory; } + if (!$definitionNameFactory) { + $this->definitionNameFactory = new DefinitionNameFactory($this->distinctFormats); + } $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceClassResolver = $resourceClassResolver; @@ -59,21 +59,31 @@ public function buildSchema(string $className, string $format = 'json', string $ { $schema = $schema ? clone $schema : new Schema(); - if (null === $metadata = $this->getMetadata($className, $type, $operation, $serializerContext)) { - return $schema; + if (!$this->isResourceClass($className)) { + $operation = null; + $inputOrOutputClass = $className; + $serializerContext ??= []; + } else { + $operation = $this->findOperation($className, $type, $operation, $serializerContext); + $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext); + $serializerContext ??= $this->getSerializerContext($operation, $type); } - [$operation, $serializerContext, $validationGroups, $inputOrOutputClass] = $metadata; + if (null === $inputOrOutputClass) { + // input or output disabled + return $schema; + } + $validationGroups = $operation ? $this->getValidationGroups($operation) : []; $version = $schema->getVersion(); - $definitionName = $this->buildDefinitionName($className, $format, $inputOrOutputClass, $operation, $serializerContext); + $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext); $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET'; if (!$operation) { $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET'; } - // In case of FORCE_SUBSCHEMA an object can be writable through another class eventhough it has no POST operation + // In case of FORCE_SUBSCHEMA an object can be writable through another class even though it has no POST operation if (!($serializerContext[self::FORCE_SUBSCHEMA] ?? false) && Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) { return $schema; } @@ -241,121 +251,6 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); } - private function buildDefinitionName(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, ?array $serializerContext = null): string - { - if ($operation) { - $prefix = $operation->getShortName(); - } - - if (!isset($prefix)) { - $prefix = (new \ReflectionClass($className))->getShortName(); - } - - if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) { - $shortName = $this->getShortClassName($inputOrOutputClass); - $prefix .= '.'.$shortName; - } - - if ('json' !== $format && ($this->distinctFormats[$format] ?? false)) { - // JSON is the default, and so isn't included in the definition name - $prefix .= '.'.$format; - } - - $definitionName = $serializerContext[self::OPENAPI_DEFINITION_NAME] ?? null; - if ($definitionName) { - $name = sprintf('%s-%s', $prefix, $definitionName); - } else { - $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []); - $name = $groups ? sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix; - } - - return $this->encodeDefinitionName($name); - } - - private function encodeDefinitionName(string $name): string - { - return preg_replace('/[^a-zA-Z0-9.\-_]/', '.', $name); - } - - private function getMetadata(string $className, string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?array $serializerContext = null): ?array - { - if (!$this->isResourceClass($className)) { - return [ - null, - $serializerContext ?? [], - [], - $className, - ]; - } - - $forceSubschema = $serializerContext[self::FORCE_SUBSCHEMA] ?? false; - if (null === $operation) { - $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); - try { - $operation = $resourceMetadataCollection->getOperation(); - } catch (OperationNotFoundException $e) { - $operation = new HttpOperation(); - } - if ($operation->getShortName() === $this->getShortClassName($className) && $forceSubschema) { - $operation = new HttpOperation(); - } - - $operation = $this->findOperationForType($resourceMetadataCollection, $type, $operation); - } else { - // The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise - if (!$operation->getClass()) { - $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); - - if ($operation->getName()) { - $operation = $resourceMetadataCollection->getOperation($operation->getName()); - } else { - $operation = $this->findOperationForType($resourceMetadataCollection, $type, $operation); - } - } - } - - $inputOrOutput = ['class' => $className]; - $inputOrOutput = Schema::TYPE_OUTPUT === $type ? ($operation->getOutput() ?? $inputOrOutput) : ($operation->getInput() ?? $inputOrOutput); - $outputClass = $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null); - - if (null === $outputClass) { - // input or output disabled - return null; - } - - return [ - $operation, - $serializerContext ?? $this->getSerializerContext($operation, $type), - $this->getValidationGroups($operation), - $outputClass, - ]; - } - - private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation - { - // Find the operation and use the first one that matches criterias - foreach ($resourceMetadataCollection as $resourceMetadata) { - foreach ($resourceMetadata->getOperations() ?? [] as $op) { - if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { - $operation = $op; - break 2; - } - - if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { - $operation = $op; - break 2; - } - } - } - - return $operation; - } - - private function getSerializerContext(Operation $operation, string $type = Schema::TYPE_OUTPUT): array - { - return Schema::TYPE_OUTPUT === $type ? ($operation->getNormalizationContext() ?? []) : ($operation->getDenormalizationContext() ?? []); - } - private function getValidationGroups(Operation $operation): array { $groups = $operation->getValidationContext()['groups'] ?? []; @@ -393,13 +288,6 @@ private function getFactoryOptions(array $serializerContext, array $validationGr return $options; } - private function getShortClassName(string $fullyQualifiedName): string - { - $parts = explode('\\', $fullyQualifiedName); - - return end($parts); - } - public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void { $this->schemaFactory = $schemaFactory; diff --git a/src/JsonSchema/Tests/SchemaFactoryTest.php b/src/JsonSchema/Tests/SchemaFactoryTest.php index 4d6dbf412e9..0f66e915acc 100644 --- a/src/JsonSchema/Tests/SchemaFactoryTest.php +++ b/src/JsonSchema/Tests/SchemaFactoryTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\JsonSchema\Tests; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\OverriddenOperationDummy; @@ -74,7 +75,16 @@ public function testBuildSchemaForNonResourceClass(): void $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); - $schemaFactory = new SchemaFactory(null, $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + + $schemaFactory = new SchemaFactory( + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); $resultSchema = $schemaFactory->buildSchema(NotAResource::class); $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); @@ -142,7 +152,16 @@ public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): voi $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResourceWithUnionIntersectTypes::class)->willReturn(false); - $schemaFactory = new SchemaFactory(null, $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + + $schemaFactory = new SchemaFactory( + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); $resultSchema = $schemaFactory->buildSchema(NotAResourceWithUnionIntersectTypes::class); $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); @@ -217,7 +236,16 @@ public function testBuildSchemaWithSerializerGroups(): void $resourceClassResolverProphecy->isResourceClass(OverriddenOperationDummy::class)->willReturn(true); $resourceClassResolverProphecy->isResourceClass(GenderTypeEnum::class)->willReturn(true); - $schemaFactory = new SchemaFactory(null, $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + + $schemaFactory = new SchemaFactory( + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); $resultSchema = $schemaFactory->buildSchema(OverriddenOperationDummy::class, 'json', Schema::TYPE_OUTPUT, null, null, ['groups' => $serializerGroup, AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false]); $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); @@ -267,7 +295,16 @@ public function testBuildSchemaForAssociativeArray(): void $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); - $schemaFactory = new SchemaFactory(null, $resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), null, $resourceClassResolverProphecy->reveal()); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + + $schemaFactory = new SchemaFactory( + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); $resultSchema = $schemaFactory->buildSchema(NotAResource::class); $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 4780a582e25..7be076f3043 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\OpenApi\Tests\Factory; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\TypeFactory; @@ -432,7 +433,16 @@ public function testInvoke(): void $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $schemaFactory = new SchemaFactory(null, $resourceCollectionMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); + $definitionNameFactory = new DefinitionNameFactory([]); + + $schemaFactory = new SchemaFactory( + typeFactory: null, + resourceMetadataFactory: $resourceCollectionMetadataFactory, + propertyNameCollectionFactory: $propertyNameCollectionFactory, + propertyMetadataFactory: $propertyMetadataFactory, + nameConverter: new CamelCaseToSnakeCaseNameConverter(), + definitionNameFactory: $definitionNameFactory, + ); $typeFactory = new TypeFactory(); $typeFactory->setSchemaFactory($schemaFactory); diff --git a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php index 632ce4d3837..a0fcf600ff6 100644 --- a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php +++ b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\OpenApi\Tests\Serializer; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\SchemaFactory; use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Metadata\ApiProperty; @@ -50,7 +51,6 @@ use Psr\Container\ContainerInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -205,7 +205,15 @@ public function testNormalize(): void $propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal(); $propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal(); - $schemaFactory = new SchemaFactory(null, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, new CamelCaseToSnakeCaseNameConverter()); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + + $schemaFactory = new SchemaFactory( + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactory, + propertyNameCollectionFactory: $propertyNameCollectionFactory, + propertyMetadataFactory: $propertyMetadataFactory, + definitionNameFactory: $definitionNameFactory, + ); $typeFactory = new TypeFactory(); $typeFactory->setSchemaFactory($schemaFactory); diff --git a/src/Symfony/Bundle/Resources/config/json_schema.xml b/src/Symfony/Bundle/Resources/config/json_schema.xml index 1e58ff0a7e2..c49ffe710e9 100644 --- a/src/Symfony/Bundle/Resources/config/json_schema.xml +++ b/src/Symfony/Bundle/Resources/config/json_schema.xml @@ -23,6 +23,7 @@ %api_platform.jsonschema_formats% + @@ -41,6 +42,10 @@ + + %api_platform.jsonschema_formats% + + diff --git a/src/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Symfony/Bundle/Resources/config/jsonapi.xml index bd9904f4de0..5937cfbf83c 100644 --- a/src/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Symfony/Bundle/Resources/config/jsonapi.xml @@ -9,6 +9,8 @@ + + diff --git a/tests/Hal/JsonSchema/SchemaFactoryTest.php b/tests/Hal/JsonSchema/SchemaFactoryTest.php index dd56b49691a..99ad84e113a 100644 --- a/tests/Hal/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hal/JsonSchema/SchemaFactoryTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Hal\JsonSchema\SchemaFactory; use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; use ApiPlatform\Metadata\ApiResource; @@ -49,14 +50,14 @@ protected function setUp(): void $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT])->willReturn(new PropertyNameCollection()); $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $baseSchemaFactory = new BaseSchemaFactory( - null, - $resourceMetadataFactory->reveal(), - $propertyNameCollectionFactory->reveal(), - $propertyMetadataFactory->reveal(), - null, - null, - ['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true], + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactory->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), + propertyMetadataFactory: $propertyMetadataFactory->reveal(), + definitionNameFactory: $definitionNameFactory, ); $hydraSchemaFactory = new HydraSchemaFactory($baseSchemaFactory); diff --git a/tests/Hydra/JsonSchema/SchemaFactoryTest.php b/tests/Hydra/JsonSchema/SchemaFactoryTest.php index ce9d03e1376..b1a6029503d 100644 --- a/tests/Hydra/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hydra/JsonSchema/SchemaFactoryTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Hydra\JsonSchema\SchemaFactory; use ApiPlatform\JsonLd\ContextBuilder; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; use ApiPlatform\Metadata\ApiResource; @@ -50,14 +51,14 @@ protected function setUp(): void $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT])->willReturn(new PropertyNameCollection()); $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $baseSchemaFactory = new BaseSchemaFactory( - null, - $resourceMetadataFactoryCollection->reveal(), - $propertyNameCollectionFactory->reveal(), - $propertyMetadataFactory->reveal(), - null, - null, - ['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true], + typeFactory: null, + resourceMetadataFactory: $resourceMetadataFactoryCollection->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), + propertyMetadataFactory: $propertyMetadataFactory->reveal(), + definitionNameFactory: $definitionNameFactory, ); $this->schemaFactory = new SchemaFactory($baseSchemaFactory); diff --git a/tests/JsonApi/JsonSchema/SchemaFactoryTest.php b/tests/JsonApi/JsonSchema/SchemaFactoryTest.php index 677c4c79e51..7c0d335e78a 100644 --- a/tests/JsonApi/JsonSchema/SchemaFactoryTest.php +++ b/tests/JsonApi/JsonSchema/SchemaFactoryTest.php @@ -16,6 +16,7 @@ use ApiPlatform\Hal\JsonSchema\SchemaFactory as HalSchemaFactory; use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; use ApiPlatform\JsonApi\JsonSchema\SchemaFactory; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; use ApiPlatform\Metadata\ApiResource; @@ -51,12 +52,14 @@ protected function setUp(): void $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT])->willReturn(new PropertyNameCollection()); $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true]); + $baseSchemaFactory = new BaseSchemaFactory( typeFactory: null, resourceMetadataFactory: $resourceMetadataFactory->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), propertyMetadataFactory: $propertyMetadataFactory->reveal(), - distinctFormats: ['jsonhal' => true, 'jsonapi' => true, 'jsonld' => true], + definitionNameFactory: $definitionNameFactory, ); $halSchemaFactory = new HalSchemaFactory($baseSchemaFactory); @@ -68,6 +71,8 @@ protected function setUp(): void schemaFactory: $hydraSchemaFactory, propertyMetadataFactory: $propertyMetadataFactory->reveal(), resourceClassResolver: $resourceClassResolver->reveal(), + resourceMetadataFactory: $resourceMetadataFactory->reveal(), + definitionNameFactory: $definitionNameFactory, ); } diff --git a/tests/JsonSchema/DefinitionNameFactoryTest.php b/tests/JsonSchema/DefinitionNameFactoryTest.php new file mode 100644 index 00000000000..10444160fe7 --- /dev/null +++ b/tests/JsonSchema/DefinitionNameFactoryTest.php @@ -0,0 +1,72 @@ + + * + * 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\JsonSchema; + +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\DtoOutput; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + +final class DefinitionNameFactoryTest extends TestCase +{ + public static function providerDefinitions(): iterable + { + yield ['Dummy', Dummy::class, 'json']; + yield ['Dummy.jsonapi', Dummy::class, 'jsonapi']; + yield ['Dummy.jsonhal', Dummy::class, 'jsonhal']; + yield ['Dummy.jsonld', Dummy::class, 'jsonld']; + + yield ['Dummy.DtoOutput', Dummy::class, 'json', DtoOutput::class]; + yield ['Dummy.DtoOutput.jsonapi', Dummy::class, 'jsonapi', DtoOutput::class]; + yield ['Dummy.DtoOutput.jsonhal', Dummy::class, 'jsonhal', DtoOutput::class]; + yield ['Dummy.DtoOutput.jsonld', Dummy::class, 'jsonld', DtoOutput::class]; + + yield ['Bar', Dummy::class, 'json', null, new Get(shortName: 'Bar')]; + yield ['Bar.jsonapi', Dummy::class, 'jsonapi', null, new Get(shortName: 'Bar')]; + yield ['Bar.jsonhal', Dummy::class, 'jsonhal', null, new Get(shortName: 'Bar')]; + yield ['Bar.jsonld', Dummy::class, 'jsonld', null, new Get(shortName: 'Bar')]; + + yield ['Dummy-Baz', Dummy::class, 'json', null, null, [SchemaFactory::OPENAPI_DEFINITION_NAME => 'Baz']]; + yield ['Dummy.jsonapi-Baz', Dummy::class, 'jsonapi', null, null, [SchemaFactory::OPENAPI_DEFINITION_NAME => 'Baz']]; + yield ['Dummy.jsonhal-Baz', Dummy::class, 'jsonhal', null, null, [SchemaFactory::OPENAPI_DEFINITION_NAME => 'Baz']]; + yield ['Dummy.jsonld-Baz', Dummy::class, 'jsonld', null, null, [SchemaFactory::OPENAPI_DEFINITION_NAME => 'Baz']]; + + yield ['Dummy-read', Dummy::class, 'json', null, null, [AbstractNormalizer::GROUPS => ['read']]]; + yield ['Dummy.jsonapi-read', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::GROUPS => ['read']]]; + yield ['Dummy.jsonhal-read', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::GROUPS => ['read']]]; + yield ['Dummy.jsonld-read', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::GROUPS => ['read']]]; + + yield ['Dummy-read_write', Dummy::class, 'json', null, null, [AbstractNormalizer::GROUPS => ['read', 'write']]]; + yield ['Dummy.jsonapi-read_write', Dummy::class, 'jsonapi', null, null, [AbstractNormalizer::GROUPS => ['read', 'write']]]; + yield ['Dummy.jsonhal-read_write', Dummy::class, 'jsonhal', null, null, [AbstractNormalizer::GROUPS => ['read', 'write']]]; + yield ['Dummy.jsonld-read_write', Dummy::class, 'jsonld', null, null, [AbstractNormalizer::GROUPS => ['read', 'write']]]; + + yield ['Bar.DtoOutput-read_write', Dummy::class, 'json', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]]; + yield ['Bar.DtoOutput.jsonapi-read_write', Dummy::class, 'jsonapi', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]]; + yield ['Bar.DtoOutput.jsonhal-read_write', Dummy::class, 'jsonhal', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]]; + yield ['Bar.DtoOutput.jsonld-read_write', Dummy::class, 'jsonld', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]]; + } + + /** @dataProvider providerDefinitions */ + public function testCreate(string $expected, string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): void + { + $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + + static::assertSame($expected, $definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext)); + } +} From d2bbc2666bfaa1444b480ceabc9ce72684109286 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 5 Apr 2024 11:17:38 +0200 Subject: [PATCH 4/6] remove useless comment --- src/JsonSchema/DefinitionNameFactory.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/JsonSchema/DefinitionNameFactory.php b/src/JsonSchema/DefinitionNameFactory.php index 1def499f946..4007e837fa5 100644 --- a/src/JsonSchema/DefinitionNameFactory.php +++ b/src/JsonSchema/DefinitionNameFactory.php @@ -25,9 +25,6 @@ public function __construct(private ?array $distinctFormats) { } - /** - * @param class-string $className - */ public function create(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): string { if ($operation) { From c7e89d60564fa25a44fe146200f2dee3c82f01f6 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 5 Apr 2024 11:19:58 +0200 Subject: [PATCH 5/6] remove empty line --- src/JsonSchema/ResourceMetadataTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonSchema/ResourceMetadataTrait.php b/src/JsonSchema/ResourceMetadataTrait.php index 51232c33a34..fc36e71bd03 100644 --- a/src/JsonSchema/ResourceMetadataTrait.php +++ b/src/JsonSchema/ResourceMetadataTrait.php @@ -99,7 +99,6 @@ private function getSerializerContext(Operation $operation, string $type = Schem private function getShortClassName(string $fullyQualifiedName): string { $parts = explode('\\', $fullyQualifiedName); - return end($parts); } } From c7f7de10e45a8654c272364ef2267b6a74786da3 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 5 Apr 2024 11:20:57 +0200 Subject: [PATCH 6/6] add on invalid --- src/Symfony/Bundle/Resources/config/jsonapi.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Symfony/Bundle/Resources/config/jsonapi.xml index 5937cfbf83c..106ff577d01 100644 --- a/src/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Symfony/Bundle/Resources/config/jsonapi.xml @@ -9,8 +9,8 @@ - - + +