Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function testCreate(): void
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create(Argument::cetera())->willReturn(new PropertyNameCollection());
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Argument::cetera())->willReturn(new ApiProperty());
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$linkFactory = new LinkFactory($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal());
$pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class);
Expand Down
47 changes: 46 additions & 1 deletion src/OpenApi/Factory/OpenApiFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use ApiPlatform\Metadata\FilterInterface;
use ApiPlatform\Metadata\HeaderParameterInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link as MetadataLink;
use ApiPlatform\Metadata\Parameter as MetadataParameter;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
Expand Down Expand Up @@ -331,7 +332,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
$uriVariable->getRequired() ?? true,
false,
null,
$uriVariable->getSchema() ?? ['type' => 'string'],
$this->getIdentifierSchema($uriVariable),
);

if ($linkParameter = $uriVariable->getOpenApi()) {
Expand Down Expand Up @@ -1258,4 +1259,48 @@ private function getErrorResource(string $error, ?int $status = null, ?string $d

return $this->localErrorResourceCache[$error] = $errorResource;
}

private function getIdentifierSchema(MetadataLink $link): array
{
$defaultSchema = ['type' => 'string'];
if (null !== ($schema = $link->getSchema())) {
return $schema;
}

$identifiers = $link->getIdentifiers();
if (true === $link->getCompositeIdentifier() || (null !== $identifiers && 1 !== \count($identifiers))) {
return $defaultSchema;
}

$class = $link->getFromClass() ?? $link->getToClass();
$property = $link->getFromProperty() ?? $link->getToProperty() ?? $link->getParameterName();

try {
$schema = $this->propertyMetadataFactory->create($class, $property)->getSchema();
} catch (\Throwable) {
return $defaultSchema;
}

if (null === $schema || !isset($schema['type'])) {
return $defaultSchema;
}

if (!\is_array($schema['type'])) {
$schema['type'] = [$schema['type']];
}

// Remove possible null type, as this is not possible in the uriVariable context
$schema['type'] = array_diff($schema['type'], ['null']);

// Link types are limited to primitive types
$schema['type'] = array_values(array_filter($schema['type'], static fn ($type) => \in_array($type, ['string', 'number', 'integer', 'boolean'], true)));
if (!$schema['type']) {
return $defaultSchema;
}

$schema['type'] = 1 === \count($schema['type']) ? $schema['type'][0] : $schema['type'];

// only allow the type/format fields
return array_intersect_key($schema, array_flip(['type', 'format'])) ?: $defaultSchema;
}
}
103 changes: 103 additions & 0 deletions src/OpenApi/Tests/Factory/OpenApiFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1979,4 +1979,107 @@ private function getGeneratedQueryParameters(array $parameters, ?ContainerInterf

return $openApi->getPaths()->getPath('/dummies')->getGet()->getParameters() ?? [];
}

public function testInferUriVariableSchema(): void
{
$resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class);
$resourceCollectionMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class);
$propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class);
$propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class);
$definitionNameFactory = new DefinitionNameFactory();

$resourceCollectionMetadata = new ResourceMetadataCollection(Dummy::class, [
(new ApiResource(operations: [
(new Get())
->withClass(Dummy::class)
->withShortName('Dummy')
->withUriTemplate('/dummies/{id}')
->withUriVariables([
'id' => (new Link())->withFromClass(Dummy::class)->withFromProperty('id')->withIdentifiers(['id']),
]),
(new Get())
->withClass(Dummy::class)
->withShortName('Dummy')
->withUriTemplate('/dummies/uuid/{uuid}')
->withUriVariables([
'uuid' => (new Link())->withFromClass(Dummy::class)->withFromProperty('uuid')->withIdentifiers(['uuid']),
]),
(new Get())
->withClass(Dummy::class)
->withShortName('Dummy')
->withUriTemplate('/dummies/composite/{id}')
->withUriVariables([
'id' => (new Link())->withFromClass(Dummy::class)->withFromProperty('id')->withIdentifiers(['id', 'other'])->withCompositeIdentifier(true),
]),
(new Get())
->withClass(Dummy::class)
->withShortName('Dummy')
->withUriTemplate('/dummies/custom/{id}')
->withUriVariables([
'id' => (new Link(schema: ['type' => 'number']))->withFromClass(Dummy::class)->withFromProperty('id')->withIdentifiers(['id']),
]),
]))->withClass(Dummy::class),
]);

$resourceCollectionMetadataFactory
->method('create')
->willReturnCallback(static fn (string $resourceClass): ResourceMetadataCollection => match ($resourceClass) {
default => new ResourceMetadataCollection($resourceClass, []),
Dummy::class => $resourceCollectionMetadata,
});

$resourceNameCollectionFactory
->method('create')
->willReturn(new ResourceNameCollection([Dummy::class]));

$propertyNameCollectionFactory->method('create')->willReturn(new PropertyNameCollection(['id', 'uuid']));

$propertyMetadataFactory
->method('create')
->willReturnCallback(static fn (string $resourceClass, string $property): ApiProperty => match ($property) {
'id' => (new ApiProperty())->withIdentifier(true)->withSchema(['type' => 'integer', 'description' => 'Dummy ID', 'readOnly' => true]),
'uuid' => (new ApiProperty())->withIdentifier(true)->withSchema(['type' => 'string', 'format' => 'uuid', 'description' => 'UUID']),
default => new ApiProperty(),
});

$schemaFactory = new SchemaFactory(
resourceMetadataFactory: $resourceCollectionMetadataFactory,
propertyNameCollectionFactory: $propertyNameCollectionFactory,
propertyMetadataFactory: $propertyMetadataFactory,
nameConverter: new CamelCaseToSnakeCaseNameConverter(),
definitionNameFactory: $definitionNameFactory,
);

$factory = new OpenApiFactory(
$resourceNameCollectionFactory,
$resourceCollectionMetadataFactory,
$propertyNameCollectionFactory,
$propertyMetadataFactory,
$schemaFactory,
null,
[],
new Options('Test API', 'This is a test API.', '1.2.3'),
new PaginationOptions(),
null,
['json' => ['application/problem+json']]
);

$openApi = $factory->__invoke();

// Single integer identifier -> schema type: integer (type/format only)
$idParam = $openApi->getPaths()->getPath('/dummies/{id}')->getGet()->getParameters()[0];
$this->assertSame(['type' => 'integer'], $idParam->getSchema());

// Single uuid identifier -> schema type: string, format: uuid
$uuidParam = $openApi->getPaths()->getPath('/dummies/uuid/{uuid}')->getGet()->getParameters()[0];
$this->assertSame(['type' => 'string', 'format' => 'uuid'], $uuidParam->getSchema());

// Composite identifier -> schema type: string
$compositeParam = $openApi->getPaths()->getPath('/dummies/composite/{id}')->getGet()->getParameters()[0];
$this->assertSame(['type' => 'string'], $compositeParam->getSchema());

// Custom schema on Link -> schema preserved
$customParam = $openApi->getPaths()->getPath('/dummies/custom/{id}')->getGet()->getParameters()[0];
$this->assertSame(['type' => 'number'], $customParam->getSchema());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\ApiResource\LinkSchema;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Link;

#[ApiResource(
operations: [
new Get(
uriTemplate: '/link_schema_array_identifiers/{id}',
uriVariables: [
'id' => new Link(
fromProperty: 'id',
fromClass: LinkSchemaArrayIdentifier::class,
identifiers: ['id']
),
],
provider: [LinkSchemaArrayIdentifier::class, 'provide']
),
]
)]
class LinkSchemaArrayIdentifier
{
/**
* @var string[]
*/
#[ApiProperty(identifier: true, schema: ['type' => 'array', 'items' => ['type' => 'string']])]
public array $id = [];

public static function provide(): self
{
return new self();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\ApiResource\LinkSchema;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Link;

#[ApiResource(
operations: [
new Get(
uriTemplate: '/link_schema_parents/{parentId}/children/{id}',
uriVariables: [
'parentId' => new Link(
fromProperty: 'id',
fromClass: LinkSchemaParent::class
),
'id' => new Link(
fromClass: LinkSchemaChild::class,
identifiers: ['id']
),
],
provider: [LinkSchemaChild::class, 'provide']
),
new Get(
uriTemplate: '/link_schema_parents_by_code/{parentCode}/children/{id}',
uriVariables: [
'parentCode' => new Link(
fromProperty: 'code',
fromClass: LinkSchemaParent::class
),
'id' => new Link(
fromClass: LinkSchemaChild::class,
identifiers: ['id']
),
],
provider: [LinkSchemaChild::class, 'provide']
),
]
)]
class LinkSchemaChild
{
#[ApiProperty(identifier: true)]
public ?int $id = null;

public ?string $title = null;

public static function provide(): self
{
return new self();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\ApiResource\LinkSchema;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use Symfony\Component\Uid\Uuid;

#[ApiResource(
operations: [
new Get(
uriTemplate: '/link_schema_parents/{id}',
provider: [LinkSchemaParent::class, 'provide']
),
]
)]
class LinkSchemaParent
{
#[ApiProperty(identifier: true)]
public ?Uuid $id = null;

public ?int $code = null;

public ?string $name = null;

public static function provide(): self
{
return new self();
}
}
Loading
Loading