Skip to content
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

Added support for Symfony\Component\HttpKernel\Exception\* documentation when annotated in exceptions #674

Merged
merged 7 commits into from
Feb 12, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@
use Dedoc\Scramble\Support\Type\Literal\LiteralStringType;
use Dedoc\Scramble\Support\Type\ObjectType;
use Dedoc\Scramble\Support\Type\Type;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\LockedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;

class HttpExceptionToResponseExtension extends ExceptionToResponseExtension
{
Expand All @@ -34,7 +50,8 @@ public function toResponse(Type $type)
? ($type->templateTypes[7] ?? null)
: ($type->templateTypes[0] ?? null);

if (! $codeType instanceof LiteralIntegerType) {
$responseCode = $this->getResponseCode($codeType, $type);
if ($responseCode === null) {
return null;
}

Expand All @@ -51,11 +68,60 @@ public function toResponse(Type $type)
)
->setRequired(['message']);

return Response::make($codeType->value)
->description('An error')
return Response::make($responseCode)
->description($this->getDescription($type))
->setContent(
'application/json',
Schema::fromType($responseBodyType)
);
}

protected function getResponseCode(?Type $codeType, Type $type): ?int
{
if (! $codeType instanceof LiteralIntegerType) {
return match (true) {
$type->isInstanceOf(AccessDeniedHttpException::class) => 403,
$type->isInstanceOf(BadRequestHttpException::class) => 400,
$type->isInstanceOf(ConflictHttpException::class) => 409,
$type->isInstanceOf(GoneHttpException::class) => 410,
$type->isInstanceOf(LengthRequiredHttpException::class) => 411,
$type->isInstanceOf(LockedHttpException::class) => 423,
$type->isInstanceOf(MethodNotAllowedHttpException::class) => 405,
$type->isInstanceOf(NotAcceptableHttpException::class) => 406,
$type->isInstanceOf(PreconditionFailedHttpException::class) => 412,
$type->isInstanceOf(PreconditionRequiredHttpException::class) => 428,
$type->isInstanceOf(ServiceUnavailableHttpException::class) => 503,
$type->isInstanceOf(TooManyRequestsHttpException::class) => 429,
$type->isInstanceOf(UnauthorizedHttpException::class) => 401,
$type->isInstanceOf(UnprocessableEntityHttpException::class) => 422,
$type->isInstanceOf(UnsupportedMediaTypeHttpException::class) => 415,
default => null,
};
}

return $codeType->value;
}

protected function getDescription(Type $type): string
{
return match (true) {
$type->isInstanceOf(AccessDeniedHttpException::class) => 'Access denied',
$type->isInstanceOf(BadRequestHttpException::class) => 'Bad request',
$type->isInstanceOf(ConflictHttpException::class) => 'Conflict',
$type->isInstanceOf(GoneHttpException::class) => 'Gone',
$type->isInstanceOf(LengthRequiredHttpException::class) => 'Length required',
$type->isInstanceOf(LockedHttpException::class) => 'Locked',
$type->isInstanceOf(MethodNotAllowedHttpException::class) => 'Method not allowed',
$type->isInstanceOf(NotAcceptableHttpException::class) => 'Not acceptable',
$type->isInstanceOf(NotFoundHttpException::class) => 'Not found',
$type->isInstanceOf(PreconditionFailedHttpException::class) => 'Precondition failed',
$type->isInstanceOf(PreconditionRequiredHttpException::class) => 'Precondition required',
$type->isInstanceOf(ServiceUnavailableHttpException::class) => 'Service unavailable',
$type->isInstanceOf(TooManyRequestsHttpException::class) => 'Too many requests',
$type->isInstanceOf(UnauthorizedHttpException::class) => 'Unauthorized',
$type->isInstanceOf(UnprocessableEntityHttpException::class) => 'Unprocessable entity',
$type->isInstanceOf(UnsupportedMediaTypeHttpException::class) => 'Unsupported media type',
default => 'An error',
};
}
}
59 changes: 58 additions & 1 deletion tests/ErrorsResponsesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@
use Illuminate\Routing\Controller;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\LockedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use function Spatie\Snapshots\assertMatchesSnapshot;

it('adds validation error response', function () {
Expand Down Expand Up @@ -92,6 +106,42 @@

expect($openApiDocument['paths']['/test']['get']['responses'][409])->toHaveKey('content.application/json.schema.type', 'object');
});


it('adds http error response exception extending sympony HTTP exception is thrown', function () {
$openApiDocument = generateForRoute(fn() => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'symfony_http_exception_response']));

// AccessDeniedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][403])->toHaveKey('content.application/json.schema.type', 'object');
// BadRequestHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][400])->toHaveKey('content.application/json.schema.type', 'object');
// ConflictHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][409])->toHaveKey('content.application/json.schema.type', 'object');
// GoneHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][410])->toHaveKey('content.application/json.schema.type', 'object');
// LengthRequiredHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][411])->toHaveKey('content.application/json.schema.type', 'object');
// LockedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][423])->toHaveKey('content.application/json.schema.type', 'object');
// MethodNotAllowedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][405])->toHaveKey('content.application/json.schema.type', 'object');
// NotAcceptableHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][406])->toHaveKey('content.application/json.schema.type', 'object');
// PreconditionFailedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][412])->toHaveKey('content.application/json.schema.type', 'object');
// PreconditionRequiredHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][428])->toHaveKey('content.application/json.schema.type', 'object');
// ServiceUnavailableHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][503])->toHaveKey('content.application/json.schema.type', 'object');
// TooManyRequestsHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][429])->toHaveKey('content.application/json.schema.type', 'object');
// UnauthorizedHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][401])->toHaveKey('content.application/json.schema.type', 'object');
// UnprocessableEntityHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][422])->toHaveKey('content.application/json.schema.type', 'object');
// UnsupportedMediaTypeHttpException
expect($openApiDocument['paths']['/test']['get']['responses'][415])->toHaveKey('content.application/json.schema.type', 'object');
});
class ErrorsResponsesTest_Controller extends Controller
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
Expand Down Expand Up @@ -129,6 +179,13 @@ public function custom_exception_response(Illuminate\Http\Request $request)
{
throw new BusinessException('The business error');
}

/**
* @throws AccessDeniedHttpException|BadRequestHttpException|ConflictHttpException|GoneHttpException|LengthRequiredHttpException|LockedHttpException|MethodNotAllowedHttpException|NotAcceptableHttpException|PreconditionFailedHttpException|PreconditionRequiredHttpException|ServiceUnavailableHttpException|TooManyRequestsHttpException|UnauthorizedHttpException|UnprocessableEntityHttpException|UnsupportedMediaTypeHttpException
*/
public function symfony_http_exception_response(Illuminate\Http\Request $request)
{
}
}

class BusinessException extends \Symfony\Component\HttpKernel\Exception\HttpException
Expand Down