From 097395d98f17f4f6b313e16aee536aefe78865ba Mon Sep 17 00:00:00 2001 From: Craig Smith <952595+phpsa@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:22:57 +1300 Subject: [PATCH 1/6] fix: Symfony\Component\HttpKernel\Exception\ types --- .../HttpExceptionToResponseExtension.php | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php index eb93b847..6b6c452c 100644 --- a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php @@ -11,6 +11,22 @@ use Dedoc\Scramble\Support\Type\ObjectType; use Dedoc\Scramble\Support\Type\Type; use Symfony\Component\HttpKernel\Exception\HttpException; +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\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; +use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; +use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; +use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; class HttpExceptionToResponseExtension extends ExceptionToResponseExtension { @@ -34,7 +50,8 @@ public function toResponse(Type $type) ? ($type->templateTypes[7] ?? null) : ($type->templateTypes[0] ?? null); - if (! $codeType instanceof LiteralIntegerType) { + $responseCode = $this->parseResponseCode($codeType, $type); + if ($responseCode === null) { return null; } @@ -51,11 +68,63 @@ public function toResponse(Type $type) ) ->setRequired(['message']); - return Response::make($codeType->value) - ->description('An error') + return Response::make($responseCode) + ->description($this->parseDescription($type)) ->setContent( 'application/json', Schema::fromType($responseBodyType) ); } + + protected function parseResponseCode(?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(NotFoundHttpException::class) => 404, + $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 => 500, + }; + } + + return $codeType->value; + } + protected function parseDescription(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', + }; + } } From 5ccfb50f460289275031d634d2021eb069441de9 Mon Sep 17 00:00:00 2001 From: Craig Smith <952595+phpsa@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:07:44 +1300 Subject: [PATCH 2/6] added test and removed notfound as is handled seperatly --- .../HttpExceptionToResponseExtension.php | 17 +- tests/ErrorsResponsesTest.php | 252 +++++++++++++----- 2 files changed, 188 insertions(+), 81 deletions(-) diff --git a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php index 6b6c452c..1618a705 100644 --- a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php @@ -37,7 +37,7 @@ public function shouldHandle(Type $type) } /** - * @param ObjectType $type + * @param ObjectType $type */ public function toResponse(Type $type) { @@ -58,13 +58,15 @@ public function toResponse(Type $type) $responseBodyType = (new OpenApiTypes\ObjectType) ->addProperty( 'message', - tap((new OpenApiTypes\StringType)->setDescription('Error overview.'), function (OpenApiTypes\StringType $t) use ($type) { - $messageType = $type->templateTypes[1] ?? null; - if (! $messageType instanceof LiteralStringType) { - return; + tap( + (new OpenApiTypes\StringType)->setDescription('Error overview.'), function (OpenApiTypes\StringType $t) use ($type) { + $messageType = $type->templateTypes[1] ?? null; + if (! $messageType instanceof LiteralStringType) { + return; + } + $t->example($messageType->value); } - $t->example($messageType->value); - }) + ) ) ->setRequired(['message']); @@ -88,7 +90,6 @@ protected function parseResponseCode(?Type $codeType, Type $type): ?int $type->isInstanceOf(LockedHttpException::class) => 423, $type->isInstanceOf(MethodNotAllowedHttpException::class) => 405, $type->isInstanceOf(NotAcceptableHttpException::class) => 406, - $type->isInstanceOf(NotFoundHttpException::class) => 404, $type->isInstanceOf(PreconditionFailedHttpException::class) => 412, $type->isInstanceOf(PreconditionRequiredHttpException::class) => 428, $type->isInstanceOf(ServiceUnavailableHttpException::class) => 503, diff --git a/tests/ErrorsResponsesTest.php b/tests/ErrorsResponsesTest.php index 8d8f53a8..35b02983 100644 --- a/tests/ErrorsResponsesTest.php +++ b/tests/ErrorsResponsesTest.php @@ -4,94 +4,178 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; +use Illuminate\Http\Request; 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 () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response']); +it( + 'adds validation error response', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response']); - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - assertMatchesSnapshot($openApiDocument); -}); - -it('adds validation error response with facade made validators', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response_with_facade_made_validators']); - - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - - assertMatchesSnapshot($openApiDocument); -}); - -it('adds errors responses with custom requests', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_errors_with_custom_request']); - - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + assertMatchesSnapshot($openApiDocument); + } +); - assertMatchesSnapshot($openApiDocument); -}); +it( + 'adds validation error response with facade made validators', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response_with_facade_made_validators']); -it('doesnt add errors with custom request when errors producing methods are not defined', function () { - $openApiDocument = generateForRoute(function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined']); - }); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - expect($openApiDocument['paths']['/test']['get']['responses']) - ->toHaveKeys([200]) - ->toHaveCount(1); -}); + assertMatchesSnapshot($openApiDocument); + } +); -it('adds authorization error response', function () { - $openApiDocument = generateForRoute(function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']); - }); +it( + 'adds errors responses with custom requests', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_errors_with_custom_request']); - assertMatchesSnapshot($openApiDocument); -}); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); -it('adds authentication error response', function () { - $openApiDocument = generateForRoute(function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']) - ->middleware('auth'); - }); + assertMatchesSnapshot($openApiDocument); + } +); + +it( + 'doesnt add errors with custom request when errors producing methods are not defined', function () { + $openApiDocument = generateForRoute( + function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined']); + } + ); + + expect($openApiDocument['paths']['/test']['get']['responses']) + ->toHaveKeys([200]) + ->toHaveCount(1); + } +); - expect($openApiDocument) - ->toHaveKey('components.responses.AuthenticationException') - ->and($openApiDocument['paths']['/test']['get']['responses'][401]) - ->toBe([ - '$ref' => '#/components/responses/AuthenticationException', - ]); -}); +it( + 'adds authorization error response', function () { + $openApiDocument = generateForRoute( + function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']); + } + ); -it('adds not found error response', function () { - $openApiDocument = generateForRoute(function () { - return RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response']) - ->middleware('can:update,post'); - }); + assertMatchesSnapshot($openApiDocument); + } +); + +it( + 'adds authentication error response', function () { + $openApiDocument = generateForRoute( + function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']) + ->middleware('auth'); + } + ); + + expect($openApiDocument) + ->toHaveKey('components.responses.AuthenticationException') + ->and($openApiDocument['paths']['/test']['get']['responses'][401]) + ->toBe( + [ + '$ref' => '#/components/responses/AuthenticationException', + ] + ); + } +); + +it( + 'adds not found error response', function () { + $openApiDocument = generateForRoute( + function () { + return RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response']) + ->middleware('can:update,post'); + } + ); + + assertMatchesSnapshot($openApiDocument); + } +); - assertMatchesSnapshot($openApiDocument); -}); +it( + 'adds validation error response when documented in phpdoc', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'phpdoc_exception_response']); -it('adds validation error response when documented in phpdoc', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'phpdoc_exception_response']); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + assertMatchesSnapshot($openApiDocument); + } +); - assertMatchesSnapshot($openApiDocument); -}); +it( + 'adds http error response exception extending HTTP exception is thrown', function () { + $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'custom_exception_response'])); -it('adds http error response exception extending HTTP exception is thrown', function () { - $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'custom_exception_response'])); + 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, 'symphony_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'); + } +); - expect($openApiDocument['paths']['/test']['get']['responses'][409])->toHaveKey('content.application/json.schema.type', 'object'); -}); class ErrorsResponsesTest_Controller extends Controller { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; @@ -107,28 +191,46 @@ public function adds_validation_error_response_with_facade_made_validators(Illum ->validate(); } - public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) {} + public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) + { + } - public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) {} + public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) + { + } public function adds_authorization_error_response(Illuminate\Http\Request $request) { $this->authorize('read'); } - public function adds_authentication_error_response(Illuminate\Http\Request $request) {} + public function adds_authentication_error_response(Illuminate\Http\Request $request) + { + } - public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) {} + public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) + { + } /** * @throws \Illuminate\Validation\ValidationException */ - public function phpdoc_exception_response(Illuminate\Http\Request $request) {} + public function phpdoc_exception_response(Illuminate\Http\Request $request) + { + } 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 symphony_http_exception_response(Illuminate\Http\Request $request) + { + + } } class BusinessException extends \Symfony\Component\HttpKernel\Exception\HttpException @@ -139,7 +241,9 @@ public function __construct(string $message = '', ?\Throwable $previous = null, } } -class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model {} +class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model +{ +} class ErrorsResponsesTest_Controller_CustomRequest extends \Illuminate\Foundation\Http\FormRequest { @@ -154,4 +258,6 @@ public function rules() } } -class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest {} +class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest +{ +} From e42d7d4f0de1f8d968a9dffa0b7d001efe530048 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Wed, 12 Feb 2025 08:48:59 +0200 Subject: [PATCH 3/6] fix formatting --- .../HttpExceptionToResponseExtension.php | 9 ++-- tests/ErrorsResponsesTest.php | 42 +++++-------------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php index 1618a705..8859c24c 100644 --- a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php @@ -10,20 +10,20 @@ use Dedoc\Scramble\Support\Type\Literal\LiteralStringType; use Dedoc\Scramble\Support\Type\ObjectType; use Dedoc\Scramble\Support\Type\Type; -use Symfony\Component\HttpKernel\Exception\HttpException; 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\NotFoundHttpException; 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\TooManyRequestsHttpException; 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; @@ -37,7 +37,7 @@ public function shouldHandle(Type $type) } /** - * @param ObjectType $type + * @param ObjectType $type */ public function toResponse(Type $type) { @@ -104,6 +104,7 @@ protected function parseResponseCode(?Type $codeType, Type $type): ?int return $codeType->value; } + protected function parseDescription(Type $type): string { diff --git a/tests/ErrorsResponsesTest.php b/tests/ErrorsResponsesTest.php index 35b02983..0cbe7942 100644 --- a/tests/ErrorsResponsesTest.php +++ b/tests/ErrorsResponsesTest.php @@ -4,7 +4,6 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; -use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Route as RouteFacade; @@ -90,7 +89,7 @@ function () { $openApiDocument = generateForRoute( function () { return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']) - ->middleware('auth'); + ->middleware('auth'); } ); @@ -99,7 +98,7 @@ function () { ->and($openApiDocument['paths']['/test']['get']['responses'][401]) ->toBe( [ - '$ref' => '#/components/responses/AuthenticationException', + '$ref' => '#/components/responses/AuthenticationException', ] ); } @@ -110,7 +109,7 @@ function () { $openApiDocument = generateForRoute( function () { return RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response']) - ->middleware('can:update,post'); + ->middleware('can:update,post'); } ); @@ -141,8 +140,6 @@ function () { 'adds http error response exception extending sympony HTTP exception is thrown', function () { $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'symphony_http_exception_response'])); - - // AccessDeniedHttpException expect($openApiDocument['paths']['/test']['get']['responses'][403])->toHaveKey('content.application/json.schema.type', 'object'); // BadRequestHttpException @@ -191,33 +188,23 @@ public function adds_validation_error_response_with_facade_made_validators(Illum ->validate(); } - public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) - { - } + public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) {} - public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) - { - } + public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) {} public function adds_authorization_error_response(Illuminate\Http\Request $request) { $this->authorize('read'); } - public function adds_authentication_error_response(Illuminate\Http\Request $request) - { - } + public function adds_authentication_error_response(Illuminate\Http\Request $request) {} - public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) - { - } + public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) {} /** * @throws \Illuminate\Validation\ValidationException */ - public function phpdoc_exception_response(Illuminate\Http\Request $request) - { - } + public function phpdoc_exception_response(Illuminate\Http\Request $request) {} public function custom_exception_response(Illuminate\Http\Request $request) { @@ -227,10 +214,7 @@ public function custom_exception_response(Illuminate\Http\Request $request) /** * @throws AccessDeniedHttpException|BadRequestHttpException|ConflictHttpException|GoneHttpException|LengthRequiredHttpException|LockedHttpException|MethodNotAllowedHttpException|NotAcceptableHttpException|PreconditionFailedHttpException|PreconditionRequiredHttpException|ServiceUnavailableHttpException|TooManyRequestsHttpException|UnauthorizedHttpException|UnprocessableEntityHttpException|UnsupportedMediaTypeHttpException */ - public function symphony_http_exception_response(Illuminate\Http\Request $request) - { - - } + public function symphony_http_exception_response(Illuminate\Http\Request $request) {} } class BusinessException extends \Symfony\Component\HttpKernel\Exception\HttpException @@ -241,9 +225,7 @@ public function __construct(string $message = '', ?\Throwable $previous = null, } } -class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model -{ -} +class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model {} class ErrorsResponsesTest_Controller_CustomRequest extends \Illuminate\Foundation\Http\FormRequest { @@ -258,6 +240,4 @@ public function rules() } } -class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest -{ -} +class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest {} From 278bd9e3ac35234cad2841103c72e96db2f08f60 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Wed, 12 Feb 2025 09:09:43 +0200 Subject: [PATCH 4/6] fix formatting and updated logic --- .../HttpExceptionToResponseExtension.php | 39 ++- tests/ErrorsResponsesTest.php | 294 +++++++++--------- 2 files changed, 157 insertions(+), 176 deletions(-) diff --git a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php index 8859c24c..81bdadfb 100644 --- a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php @@ -50,7 +50,7 @@ public function toResponse(Type $type) ? ($type->templateTypes[7] ?? null) : ($type->templateTypes[0] ?? null); - $responseCode = $this->parseResponseCode($codeType, $type); + $responseCode = $this->getResponseCode($codeType, $type); if ($responseCode === null) { return null; } @@ -71,14 +71,14 @@ public function toResponse(Type $type) ->setRequired(['message']); return Response::make($responseCode) - ->description($this->parseDescription($type)) + ->description($this->getDescription($type)) ->setContent( 'application/json', Schema::fromType($responseBodyType) ); } - protected function parseResponseCode(?Type $codeType, Type $type): ?int + protected function getResponseCode(?Type $codeType, Type $type): ?int { if (! $codeType instanceof LiteralIntegerType) { return match (true) { @@ -97,36 +97,33 @@ protected function parseResponseCode(?Type $codeType, Type $type): ?int $type->isInstanceOf(UnauthorizedHttpException::class) => 401, $type->isInstanceOf(UnprocessableEntityHttpException::class) => 422, $type->isInstanceOf(UnsupportedMediaTypeHttpException::class) => 415, - - default => 500, + default => null, }; } return $codeType->value; } - protected function parseDescription(Type $type): string + protected function getDescription(Type $type): string { - return match (true) { - $type->isInstanceOf(AccessDeniedHttpException::class) => 'Access Denied', - $type->isInstanceOf(BadRequestHttpException::class) => 'Bad Request', + $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(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(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', + $type->isInstanceOf(UnprocessableEntityHttpException::class) => 'Unprocessable entity', + $type->isInstanceOf(UnsupportedMediaTypeHttpException::class) => 'Unsupported media type', + default => 'An error', }; } } diff --git a/tests/ErrorsResponsesTest.php b/tests/ErrorsResponsesTest.php index 0cbe7942..76e51de0 100644 --- a/tests/ErrorsResponsesTest.php +++ b/tests/ErrorsResponsesTest.php @@ -25,153 +25,121 @@ use function Spatie\Snapshots\assertMatchesSnapshot; -it( - 'adds validation error response', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response']); - - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - - assertMatchesSnapshot($openApiDocument); - } -); - -it( - 'adds validation error response with facade made validators', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response_with_facade_made_validators']); - - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - - assertMatchesSnapshot($openApiDocument); - } -); - -it( - 'adds errors responses with custom requests', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_errors_with_custom_request']); - - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - - assertMatchesSnapshot($openApiDocument); - } -); - -it( - 'doesnt add errors with custom request when errors producing methods are not defined', function () { - $openApiDocument = generateForRoute( - function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined']); - } - ); - - expect($openApiDocument['paths']['/test']['get']['responses']) - ->toHaveKeys([200]) - ->toHaveCount(1); - } -); - -it( - 'adds authorization error response', function () { - $openApiDocument = generateForRoute( - function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']); - } - ); - - assertMatchesSnapshot($openApiDocument); - } -); - -it( - 'adds authentication error response', function () { - $openApiDocument = generateForRoute( - function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']) - ->middleware('auth'); - } - ); - - expect($openApiDocument) - ->toHaveKey('components.responses.AuthenticationException') - ->and($openApiDocument['paths']['/test']['get']['responses'][401]) - ->toBe( - [ - '$ref' => '#/components/responses/AuthenticationException', - ] - ); - } -); - -it( - 'adds not found error response', function () { - $openApiDocument = generateForRoute( - function () { - return RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response']) - ->middleware('can:update,post'); - } - ); - - assertMatchesSnapshot($openApiDocument); - } -); - -it( - 'adds validation error response when documented in phpdoc', function () { - RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'phpdoc_exception_response']); - - Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); - $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); - - assertMatchesSnapshot($openApiDocument); - } -); - -it( - 'adds http error response exception extending HTTP exception is thrown', function () { - $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'custom_exception_response'])); - - 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, 'symphony_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'); - } -); +it('adds validation error response', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response']); + + Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + + assertMatchesSnapshot($openApiDocument); +}); + +it('adds validation error response with facade made validators', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response_with_facade_made_validators']); + + Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + + assertMatchesSnapshot($openApiDocument); +}); + +it('adds errors responses with custom requests', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_errors_with_custom_request']); + + Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + + assertMatchesSnapshot($openApiDocument); +}); + +it('doesnt add errors with custom request when errors producing methods are not defined', function () { + $openApiDocument = generateForRoute( + function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined']); + } + ); + + expect($openApiDocument['paths']['/test']['get']['responses']) + ->toHaveKeys([200]) + ->toHaveCount(1); +}); + +it('adds authorization error response', function () { + $openApiDocument = generateForRoute( + function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']); + } + ); + + assertMatchesSnapshot($openApiDocument); +}); + +it('adds authentication error response', function () { + $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response'])->middleware('auth')); + + expect($openApiDocument) + ->toHaveKey('components.responses.AuthenticationException') + ->and($openApiDocument['paths']['/test']['get']['responses'][401]) + ->toBe([ + '$ref' => '#/components/responses/AuthenticationException', + ]); +}); + +it('adds not found error response', function () { + $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response'])->middleware('can:update,post')); + + assertMatchesSnapshot($openApiDocument); +}); + +it('adds validation error response when documented in phpdoc', function () { + RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'phpdoc_exception_response']); + + Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); + + assertMatchesSnapshot($openApiDocument); +}); + +it('adds http error response exception extending HTTP exception is thrown', function () { + $openApiDocument = generateForRoute(fn() => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'custom_exception_response'])); + + 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 { @@ -188,23 +156,33 @@ public function adds_validation_error_response_with_facade_made_validators(Illum ->validate(); } - public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) {} + public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) + { + } - public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) {} + public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) + { + } public function adds_authorization_error_response(Illuminate\Http\Request $request) { $this->authorize('read'); } - public function adds_authentication_error_response(Illuminate\Http\Request $request) {} + public function adds_authentication_error_response(Illuminate\Http\Request $request) + { + } - public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) {} + public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) + { + } /** * @throws \Illuminate\Validation\ValidationException */ - public function phpdoc_exception_response(Illuminate\Http\Request $request) {} + public function phpdoc_exception_response(Illuminate\Http\Request $request) + { + } public function custom_exception_response(Illuminate\Http\Request $request) { @@ -214,7 +192,9 @@ public function custom_exception_response(Illuminate\Http\Request $request) /** * @throws AccessDeniedHttpException|BadRequestHttpException|ConflictHttpException|GoneHttpException|LengthRequiredHttpException|LockedHttpException|MethodNotAllowedHttpException|NotAcceptableHttpException|PreconditionFailedHttpException|PreconditionRequiredHttpException|ServiceUnavailableHttpException|TooManyRequestsHttpException|UnauthorizedHttpException|UnprocessableEntityHttpException|UnsupportedMediaTypeHttpException */ - public function symphony_http_exception_response(Illuminate\Http\Request $request) {} + public function symfony_http_exception_response(Illuminate\Http\Request $request) + { + } } class BusinessException extends \Symfony\Component\HttpKernel\Exception\HttpException @@ -225,7 +205,9 @@ public function __construct(string $message = '', ?\Throwable $previous = null, } } -class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model {} +class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model +{ +} class ErrorsResponsesTest_Controller_CustomRequest extends \Illuminate\Foundation\Http\FormRequest { @@ -240,4 +222,6 @@ public function rules() } } -class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest {} +class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest +{ +} From d60509f2a3d1f3f91608585dfe66751848cdb739 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Wed, 12 Feb 2025 09:14:26 +0200 Subject: [PATCH 5/6] formatting fix --- tests/ErrorsResponsesTest.php | 67 ++++++++++++++--------------------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/tests/ErrorsResponsesTest.php b/tests/ErrorsResponsesTest.php index 76e51de0..065f3645 100644 --- a/tests/ErrorsResponsesTest.php +++ b/tests/ErrorsResponsesTest.php @@ -22,13 +22,12 @@ 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 () { RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response']); - Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); assertMatchesSnapshot($openApiDocument); @@ -37,7 +36,7 @@ it('adds validation error response with facade made validators', function () { RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_validation_error_response_with_facade_made_validators']); - Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); assertMatchesSnapshot($openApiDocument); @@ -46,18 +45,16 @@ it('adds errors responses with custom requests', function () { RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_errors_with_custom_request']); - Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); assertMatchesSnapshot($openApiDocument); }); it('doesnt add errors with custom request when errors producing methods are not defined', function () { - $openApiDocument = generateForRoute( - function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined']); - } - ); + $openApiDocument = generateForRoute(function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined']); + }); expect($openApiDocument['paths']['/test']['get']['responses']) ->toHaveKeys([200]) @@ -65,17 +62,18 @@ function () { }); it('adds authorization error response', function () { - $openApiDocument = generateForRoute( - function () { - return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']); - } - ); + $openApiDocument = generateForRoute(function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']); + }); assertMatchesSnapshot($openApiDocument); }); it('adds authentication error response', function () { - $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response'])->middleware('auth')); + $openApiDocument = generateForRoute(function () { + return RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'adds_authorization_error_response']) + ->middleware('auth'); + }); expect($openApiDocument) ->toHaveKey('components.responses.AuthenticationException') @@ -86,7 +84,10 @@ function () { }); it('adds not found error response', function () { - $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response'])->middleware('can:update,post')); + $openApiDocument = generateForRoute(function () { + return RouteFacade::get('api/test/{user}', [ErrorsResponsesTest_Controller::class, 'adds_not_found_error_response']) + ->middleware('can:update,post'); + }); assertMatchesSnapshot($openApiDocument); }); @@ -94,18 +95,19 @@ function () { it('adds validation error response when documented in phpdoc', function () { RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'phpdoc_exception_response']); - Scramble::routes(fn(Route $r) => $r->uri === 'api/test'); + Scramble::routes(fn (Route $r) => $r->uri === 'api/test'); $openApiDocument = app()->make(\Dedoc\Scramble\Generator::class)(); assertMatchesSnapshot($openApiDocument); }); it('adds http error response exception extending HTTP exception is thrown', function () { - $openApiDocument = generateForRoute(fn() => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'custom_exception_response'])); + $openApiDocument = generateForRoute(fn () => RouteFacade::get('api/test', [ErrorsResponsesTest_Controller::class, 'custom_exception_response'])); 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'])); @@ -140,7 +142,6 @@ function () { // UnsupportedMediaTypeHttpException expect($openApiDocument['paths']['/test']['get']['responses'][415])->toHaveKey('content.application/json.schema.type', 'object'); }); - class ErrorsResponsesTest_Controller extends Controller { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; @@ -156,33 +157,23 @@ public function adds_validation_error_response_with_facade_made_validators(Illum ->validate(); } - public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) - { - } + public function adds_errors_with_custom_request(ErrorsResponsesTest_Controller_CustomRequest $request) {} - public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) - { - } + public function doesnt_add_errors_with_custom_request_when_errors_producing_methods_not_defined(ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods $request) {} public function adds_authorization_error_response(Illuminate\Http\Request $request) { $this->authorize('read'); } - public function adds_authentication_error_response(Illuminate\Http\Request $request) - { - } + public function adds_authentication_error_response(Illuminate\Http\Request $request) {} - public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) - { - } + public function adds_not_found_error_response(Illuminate\Http\Request $request, UserModel_ErrorsResponsesTest $user) {} /** * @throws \Illuminate\Validation\ValidationException */ - public function phpdoc_exception_response(Illuminate\Http\Request $request) - { - } + public function phpdoc_exception_response(Illuminate\Http\Request $request) {} public function custom_exception_response(Illuminate\Http\Request $request) { @@ -205,9 +196,7 @@ public function __construct(string $message = '', ?\Throwable $previous = null, } } -class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model -{ -} +class UserModel_ErrorsResponsesTest extends \Illuminate\Database\Eloquent\Model {} class ErrorsResponsesTest_Controller_CustomRequest extends \Illuminate\Foundation\Http\FormRequest { @@ -222,6 +211,4 @@ public function rules() } } -class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest -{ -} +class ErrorsResponsesTest_Controller_CustomRequestWithoutErrorCreatingMethods extends \Illuminate\Foundation\Http\FormRequest {} From 542638fbf21ec6764a2f1785238c3f686b836697 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Wed, 12 Feb 2025 09:15:16 +0200 Subject: [PATCH 6/6] undo formatting --- .../HttpExceptionToResponseExtension.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php index 81bdadfb..2343dc92 100644 --- a/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php +++ b/src/Support/ExceptionToResponseExtensions/HttpExceptionToResponseExtension.php @@ -58,15 +58,13 @@ public function toResponse(Type $type) $responseBodyType = (new OpenApiTypes\ObjectType) ->addProperty( 'message', - tap( - (new OpenApiTypes\StringType)->setDescription('Error overview.'), function (OpenApiTypes\StringType $t) use ($type) { - $messageType = $type->templateTypes[1] ?? null; - if (! $messageType instanceof LiteralStringType) { - return; - } - $t->example($messageType->value); + tap((new OpenApiTypes\StringType)->setDescription('Error overview.'), function (OpenApiTypes\StringType $t) use ($type) { + $messageType = $type->templateTypes[1] ?? null; + if (! $messageType instanceof LiteralStringType) { + return; } - ) + $t->example($messageType->value); + }) ) ->setRequired(['message']);