Skip to content

feat(openapi): add error resources schemes #6332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 9, 2024
Merged
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
2 changes: 2 additions & 0 deletions src/Metadata/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -127,6 +128,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -125,6 +126,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -126,6 +127,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/GetCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -127,6 +128,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
29 changes: 24 additions & 5 deletions src/Metadata/HttpOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Metadata;

use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use ApiPlatform\OpenApi\Attributes\Webhook;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use ApiPlatform\State\OptionsInterface;
Expand Down Expand Up @@ -73,11 +74,12 @@ class HttpOperation extends Operation
* class?: string|null,
* name?: string,
* }|string|false|null $output {@see https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation}
* @param string|array|bool|null $mercure {@see https://api-platform.com/docs/core/mercure}
* @param string|bool|null $messenger {@see https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus}
* @param string|callable|null $provider {@see https://api-platform.com/docs/core/state-providers/#state-providers}
* @param string|callable|null $processor {@see https://api-platform.com/docs/core/state-processors/#state-processors}
* @param WebLink[]|null $links
* @param string|array|bool|null $mercure {@see https://api-platform.com/docs/core/mercure}
* @param string|bool|null $messenger {@see https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus}
* @param string|callable|null $provider {@see https://api-platform.com/docs/core/state-providers/#state-providers}
* @param string|callable|null $processor {@see https://api-platform.com/docs/core/state-processors/#state-processors}
* @param WebLink[]|null $links
* @param array<class-string<ProblemExceptionInterface>>|null $errors
*/
public function __construct(
protected string $method = 'GET',
Expand Down Expand Up @@ -154,6 +156,7 @@ public function __construct(
protected bool|OpenApiOperation|Webhook|null $openapi = null,
protected ?array $exceptionToStatus = null,
protected ?array $links = null,
protected ?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -623,4 +626,20 @@ public function withLinks(array $links): self

return $self;
}

public function getErrors(): ?array
{
return $this->errors;
}

/**
* @param class-string<ProblemExceptionInterface>[] $errors
*/
public function withErrors(array $errors): self
{
$self = clone $this;
$self->errors = $errors;

return $self;
}
}
2 changes: 2 additions & 0 deletions src/Metadata/NotExposed.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public function __construct(

?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -139,6 +140,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Patch.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -127,6 +128,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -128,6 +129,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/Put.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null,
?array $links = null,
?array $errors = null,

?string $shortName = null,
?string $class = null,
Expand Down Expand Up @@ -128,6 +129,7 @@ public function __construct(
exceptionToStatus: $exceptionToStatus,
queryParameterValidationEnabled: $queryParameterValidationEnabled,
links: $links,
errors: $errors,
shortName: $shortName,
class: $class,
paginationEnabled: $paginationEnabled,
Expand Down
96 changes: 88 additions & 8 deletions src/OpenApi/Factory/OpenApiFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
use ApiPlatform\JsonSchema\TypeFactoryInterface;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Error;
use ApiPlatform\Metadata\Exception\OperationNotFoundException;
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
use ApiPlatform\Metadata\Exception\RuntimeException;
use ApiPlatform\Metadata\HeaderParameterInterface;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
Expand All @@ -38,6 +43,7 @@
use ApiPlatform\OpenApi\Model\MediaType;
use ApiPlatform\OpenApi\Model\OAuthFlow;
use ApiPlatform\OpenApi\Model\OAuthFlows;
use ApiPlatform\OpenApi\Model\Operation;
use ApiPlatform\OpenApi\Model\Parameter;
use ApiPlatform\OpenApi\Model\PathItem;
use ApiPlatform\OpenApi\Model\Paths;
Expand Down Expand Up @@ -75,8 +81,19 @@ final class OpenApiFactory implements OpenApiFactoryInterface
*/
public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name';

public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly SchemaFactoryInterface $jsonSchemaFactory, ?TypeFactoryInterface $jsonSchemaTypeFactory, ContainerInterface $filterLocator, private readonly array $formats = [], ?Options $openApiOptions = null, ?PaginationOptions $paginationOptions = null, private readonly ?RouterInterface $router = null)
{
public function __construct(
private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory,
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory,
private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
private readonly SchemaFactoryInterface $jsonSchemaFactory,
?TypeFactoryInterface $jsonSchemaTypeFactory,
ContainerInterface $filterLocator,
private readonly array $formats = [],
?Options $openApiOptions = null,
?PaginationOptions $paginationOptions = null,
private readonly ?RouterInterface $router = null,
) {
$this->filterLocator = $filterLocator;
$this->openApiOptions = $openApiOptions ?: new Options('API Platform');
$this->paginationOptions = $paginationOptions ?: new PaginationOptions();
Expand Down Expand Up @@ -181,15 +198,15 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection

if ($openapiAttribute instanceof Webhook) {
$pathItem = $openapiAttribute->getPathItem() ?: new PathItem();
$openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Model\Operation();
$openapiOperation = $pathItem->{'get'.ucfirst(strtolower($method))}() ?: new Operation();
} elseif (!\is_object($openapiAttribute)) {
$openapiOperation = new Model\Operation();
$openapiOperation = new Operation();
} else {
$openapiOperation = $openapiAttribute;
}

// Complete with defaults
$openapiOperation = new Model\Operation(
$openapiOperation = new Operation(
operationId: null !== $openapiOperation->getOperationId() ? $openapiOperation->getOperationId() : $this->normalizeOperationName($operationName),
tags: null !== $openapiOperation->getTags() ? $openapiOperation->getTags() : [$operation->getShortName() ?: $resourceShortName],
responses: null !== $openapiOperation->getResponses() ? $openapiOperation->getResponses() : [],
Expand Down Expand Up @@ -339,6 +356,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection

$existingResponses = $openapiOperation?->getResponses() ?: [];
$overrideResponses = $operation->getExtraProperties()[self::OVERRIDE_OPENAPI_RESPONSES] ?? $this->openApiOptions->getOverrideResponses();
if ($operation instanceof HttpOperation && null !== ($errors = $operation->getErrors())) {
$openapiOperation = $this->addOperationErrors($openapiOperation, $errors, $responseMimeTypes, $resourceMetadataCollection, $schema, $schemas);
}

if ($overrideResponses || !$existingResponses) {
// Create responses
switch ($method) {
Expand Down Expand Up @@ -433,7 +454,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
'3.1',
'The "openapiContext" option is deprecated, use "openapi" instead.'
);
$allowedProperties = array_map(fn (\ReflectionProperty $reflProperty): string => $reflProperty->getName(), (new \ReflectionClass(Model\Operation::class))->getProperties());
$allowedProperties = array_map(fn (\ReflectionProperty $reflProperty): string => $reflProperty->getName(), (new \ReflectionClass(Operation::class))->getProperties());
foreach ($operation->getOpenapiContext() as $key => $value) {
$value = match ($key) {
'externalDocs' => new ExternalDocumentation(description: $value['description'] ?? '', url: $value['url'] ?? ''),
Expand All @@ -460,7 +481,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
}
}

private function buildOpenApiResponse(array $existingResponses, int|string $status, string $description, ?Model\Operation $openapiOperation = null, ?HttpOperation $operation = null, ?array $responseMimeTypes = null, ?array $operationOutputSchemas = null, ?ResourceMetadataCollection $resourceMetadataCollection = null): Model\Operation
private function buildOpenApiResponse(array $existingResponses, int|string $status, string $description, ?Operation $openapiOperation = null, ?HttpOperation $operation = null, ?array $responseMimeTypes = null, ?array $operationOutputSchemas = null, ?ResourceMetadataCollection $resourceMetadataCollection = null): Operation
{
if (isset($existingResponses[$status])) {
return $openapiOperation;
Expand Down Expand Up @@ -491,6 +512,9 @@ private function buildContent(array $responseMimeTypes, array $operationSchemas)
return $content;
}

/**
* @return array[array<string, string>, array<string, string>]
*/
private function getMimeTypes(HttpOperation $operation): array
{
$requestFormats = $operation->getInputFormats() ?: [];
Expand All @@ -502,6 +526,11 @@ private function getMimeTypes(HttpOperation $operation): array
return [$requestMimeTypes, $responseMimeTypes];
}

/**
* @param array<string, string[]> $responseFormats
*
* @return array<string, string>
*/
private function flattenMimeTypes(array $responseFormats): array
{
$responseMimeTypes = [];
Expand Down Expand Up @@ -803,7 +832,7 @@ private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $de
/**
* @return array{0: int, 1: Parameter}|null
*/
private function hasParameter(Model\Operation $operation, Parameter $parameter): ?array
private function hasParameter(Operation $operation, Parameter $parameter): ?array
{
foreach ($operation->getParameters() as $key => $existingParameter) {
if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) {
Expand Down Expand Up @@ -843,4 +872,55 @@ private function mergeParameter(Parameter $actual, Parameter $defined): Paramete

return $actual;
}

/**
* @param string[] $errors
* @param array<string, string> $responseMimeTypes
*/
private function addOperationErrors(Operation $operation, array $errors, array $responseMimeTypes, ResourceMetadataCollection $resourceMetadataCollection, Schema $schema, \ArrayObject $schemas): Operation
{
$existingResponses = null;
foreach ($errors as $error) {
if (!is_a($error, ProblemExceptionInterface::class, true)) {
throw new RuntimeException(\sprintf('The error class "%s" does not implement "%s". Did you forget a use statement?', $error, ProblemExceptionInterface::class));
}

$status = null;
$description = null;

try {
/** @var ProblemExceptionInterface $exception */
$exception = new $error();
$status = $exception->getStatus();
$description = $exception->getTitle();
} catch (\TypeError) {
}

try {
$errorOperation = $this->resourceMetadataFactory->create($error)->getOperation();
if (!is_a($errorOperation, Error::class)) {
throw new RuntimeException(\sprintf('The error class %s is not an ErrorResource', $error));
}
} catch (ResourceClassNotFoundException|OperationNotFoundException) {
$errorOperation = null;
}
$status ??= $errorOperation?->getStatus();
$description ??= $errorOperation?->getDescription();

if (!$status) {
throw new RuntimeException(\sprintf('The error class %s has no status defined, please either implement ProblemExceptionInterface, or make it an ErrorResource with a status', $error));
}

$operationErrorSchemas = [];
foreach ($responseMimeTypes as $operationFormat) {
$operationErrorSchema = $this->jsonSchemaFactory->buildSchema($error, $operationFormat, Schema::TYPE_OUTPUT, null, $schema);
$operationErrorSchemas[$operationFormat] = $operationErrorSchema;
$this->appendSchemaDefinitions($schemas, $operationErrorSchema->getDefinitions());
}

$operation = $this->buildOpenApiResponse($existingResponses ??= $operation->getResponses() ?: [], $status, $description ?? '', $operation, $errorOperation, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection);
}

return $operation;
}
}
Loading
Loading