Skip to content

Commit

Permalink
[OpenApi] Specific service to convert error to http status code
Browse files Browse the repository at this point in the history
  • Loading branch information
mpoiriert committed May 6, 2024
1 parent a9fe129 commit 270a457
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use Draw\Component\OpenApi\Extraction\Extractor\PhpDoc\OperationExtractor;
use Draw\Component\OpenApi\Extraction\Extractor\TypeSchemaExtractor;
use Draw\Component\OpenApi\Extraction\ExtractorInterface;
use Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter\ConfigurableErrorToHttpCodeConverter;
use Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter\HttpExceptionToHttpCodeConverter;
use Draw\Component\OpenApi\Naming\AliasesClassNamingFilter;
use Draw\Component\OpenApi\OpenApi;
use Draw\Component\OpenApi\Request\ValueResolver\RequestBody;
Expand Down Expand Up @@ -330,6 +332,8 @@ private function configResponseExceptionHandler(array $config, PhpFileLoader $lo
{
if (!$this->isConfigEnabled($container, $config)) {
$container->removeDefinition(ResponseApiExceptionListener::class);
$container->removeDefinition(ConfigurableErrorToHttpCodeConverter::class);
$container->removeDefinition(HttpExceptionToHttpCodeConverter::class);

return;
}
Expand All @@ -351,15 +355,17 @@ private function configResponseExceptionHandler(array $config, PhpFileLoader $lo
'$debug',
new Parameter('kernel.debug')
)
->setArgument(
'$errorCodes',
$codes
)
->setArgument(
'$violationKey',
$config['violationKey']
);

$container->getDefinition(ConfigurableErrorToHttpCodeConverter::class)
->setArgument(
'$errorCodes',
$codes
);

$operationExtractorDefinition = $container->getDefinition(OperationExtractor::class);

foreach ($codes as $exceptionClass => $code) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
use Draw\Component\OpenApi\Extraction\Extractor\Symfony\RouteOperationExtractor;
use Draw\Component\OpenApi\Extraction\Extractor\Symfony\RouterRootSchemaExtractor;
use Draw\Component\OpenApi\Extraction\Extractor\TypeSchemaExtractor;
use Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter\ConfigurableErrorToHttpCodeConverter;
use Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter\HttpExceptionToHttpCodeConverter;
use Draw\Component\OpenApi\Naming\AliasesClassNamingFilter;
use Draw\Component\OpenApi\OpenApi;
use Draw\Component\OpenApi\Request\ValueResolver\RequestBodyValueResolver;
Expand Down Expand Up @@ -481,6 +483,18 @@ function (Definition $definition): void {
'draw.open_api.event_listener.request_validation_listener',
[RequestValidationListener::class]
),
new ServiceConfiguration(
'draw.open_api.http_foundation.error_to_http_code_converter.configurable_error_to_http_code_converter',
[
ConfigurableErrorToHttpCodeConverter::class,
]
),
new ServiceConfiguration(
'draw.open_api.http_foundation.error_to_http_code_converter.http_exception_to_http_code_converter',
[
HttpExceptionToHttpCodeConverter::class,
]
),
],
[
RouteDefaultApiRouteVersionMatcher::class => [
Expand Down
37 changes: 14 additions & 23 deletions packages/open-api/EventListener/ResponseApiExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,20 @@

use Draw\Component\OpenApi\Event\PreDumpRootSchemaEvent;
use Draw\Component\OpenApi\Exception\ConstraintViolationListException;
use Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter\ConfigurableErrorToHttpCodeConverter;
use Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter\ErrorToHttpCodeConverterInterface;
use Draw\Component\OpenApi\Schema\Response;
use Draw\Component\OpenApi\Schema\Schema;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;

final class ResponseApiExceptionListener implements EventSubscriberInterface
{
/**
* @var array<string,int>
*/
private array $errorCodes;

private const DEFAULT_STATUS_CODE = 500;

public static function getSubscribedEvents(): array
{
return [
Expand All @@ -32,11 +27,15 @@ public static function getSubscribedEvents(): array
}

public function __construct(
/**
* @var ErrorToHttpCodeConverterInterface[]
*/
#[TaggedIterator(ErrorToHttpCodeConverterInterface::class)]
private iterable $errorToHttpCodeConverters = [],
private bool $debug = false,
array $errorCodes = [],
private string $violationKey = 'errors',
) {
$this->errorCodes = array_filter($errorCodes);
$this->errorToHttpCodeConverters ??= new ConfigurableErrorToHttpCodeConverter();
}

public function addErrorDefinition(PreDumpRootSchemaEvent $event): void
Expand Down Expand Up @@ -183,22 +182,14 @@ private function getExceptionDetail(\Throwable $e): array
return $result;
}

private function getStatusCode(\Throwable $exception): int
private function getStatusCode(\Throwable $error): int
{
if ($exception instanceof HttpException) {
return $exception->getStatusCode();
}

$exceptionClass = $exception::class;

foreach ($this->errorCodes as $exceptionMapClass => $value) {
switch (true) {
case $exceptionClass === $exceptionMapClass:
case is_a($exception, $exceptionMapClass, true):
return $value;
foreach ($this->errorToHttpCodeConverters as $converter) {
if (null !== $statusCode = $converter->convertToHttpCode($error)) {
return $statusCode;
}
}

return self::DEFAULT_STATUS_CODE;
return 500;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter;

class ConfigurableErrorToHttpCodeConverter implements ErrorToHttpCodeConverterInterface
{
private const DEFAULT_STATUS_CODE = 500;

/**
* @var array<string,int>
*/
private array $errorCodes;

public static function getDefaultPriority(): int
{
return -1000;
}

public function __construct(array $errorCodes = [])
{
$this->errorCodes = array_filter($errorCodes);
}

public function convertToHttpCode(\Throwable $error): int
{
$exceptionClass = $error::class;

foreach ($this->errorCodes as $exceptionMapClass => $value) {
switch (true) {
case $exceptionClass === $exceptionMapClass:
case is_a($error, $exceptionMapClass, true):
return $value;
}
}

return self::DEFAULT_STATUS_CODE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag(ErrorToHttpCodeConverterInterface::class)]
interface ErrorToHttpCodeConverterInterface
{
public static function getDefaultPriority(): int;

public function convertToHttpCode(\Throwable $error): ?int;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Draw\Component\OpenApi\HttpFoundation\ErrorToHttpCodeConverter;

use Symfony\Component\HttpKernel\Exception\HttpException;

class HttpExceptionToHttpCodeConverter implements ErrorToHttpCodeConverterInterface
{
public static function getDefaultPriority(): int
{
return 100;
}

public function convertToHttpCode(\Throwable $error): ?int
{
if ($error instanceof HttpException) {
return $error->getStatusCode();
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use Draw\Component\OpenApi\Schema\Response as OpenResponse;
use Draw\Component\OpenApi\Schema\Root;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -26,6 +25,7 @@
class ResponseApiExceptionListenerTest extends TestCase
{
private ResponseApiExceptionListener $object;

private HttpKernelInterface $httpKernel;
private \Exception $exception;
private ExceptionEvent $exceptionEvent;
Expand Down Expand Up @@ -146,7 +146,9 @@ public function testOnKernelExceptionDebugFalse(): void
static::assertArrayNotHasKey(
'detail',
json_decode(
$this->onKernelException(new ResponseApiExceptionListener(false))->getContent(),
$this->onKernelException(
new ResponseApiExceptionListener(debug: false)
)->getContent(),
true,
512,
\JSON_THROW_ON_ERROR
Expand All @@ -170,7 +172,7 @@ public function testOnKernelExceptionDebugTrue(): void
);

$responseData = json_decode(
$this->onKernelException(new ResponseApiExceptionListener(true))->getContent(),
$this->onKernelException(new ResponseApiExceptionListener(debug: true))->getContent(),
true,
512,
\JSON_THROW_ON_ERROR
Expand Down Expand Up @@ -202,72 +204,6 @@ public function testOnKernelExceptionDebugTrue(): void
);
}

public function testOnKernelExceptionDefaultStatusCode500(): void
{
static::assertSame(
500,
$this->onKernelException()->getStatusCode()
);
}

public static function provideOnKernelExceptionStatusCode(): iterable
{
yield 'ChangeDefault' => [
new \Exception(),
[\Exception::class => 400],
400,
];

yield 'FallbackOnDefault' => [
new \Exception(),
[\RuntimeException::class => 400],
500,
];

yield 'MultipleConfiguration' => [
new \Exception(),
[\RuntimeException::class => 400, \Exception::class => 300],
300,
];

yield 'Extend' => [
new \OutOfBoundsException(),
[\RuntimeException::class => 400, \Exception::class => 300],
400,
];

$exception = new class() extends \Exception implements \JsonSerializable {
public function jsonSerialize(): void
{
}
};

yield 'Implements' => [
$exception,
[\RuntimeException::class => 400, \JsonSerializable::class => 300],
300,
];
}

/**
* @param array<string,int> $errorCodes
*/
#[DataProvider('provideOnKernelExceptionStatusCode')]
public function testOnKernelExceptionErrorCode(\Throwable $throwable, array $errorCodes, int $errorCode): void
{
$this->exceptionEvent = new ExceptionEvent(
$this->httpKernel,
$this->request,
HttpKernelInterface::MAIN_REQUEST,
$throwable
);

static::assertSame(
$errorCode,
$this->onKernelException(new ResponseApiExceptionListener(false, $errorCodes))->getStatusCode()
);
}

private function createConstraintListExceptionEvent(?Constraint $constraint = null): void
{
$exception = new ConstraintViolationListException(
Expand Down Expand Up @@ -299,7 +235,7 @@ public function testOnKernelExceptionPayload(): void
$this->createConstraintListExceptionEvent($constraint = new NotNull(['payload' => uniqid('payload-')]));

$value = json_decode(
$this->onKernelException(new ResponseApiExceptionListener(false, [], 'errors'))->getContent(),
$this->onKernelException(new ResponseApiExceptionListener())->getContent(),
null,
512,
\JSON_THROW_ON_ERROR
Expand Down
Loading

0 comments on commit 270a457

Please sign in to comment.