diff --git a/config/auto-doc.php b/config/auto-doc.php index 6a43b1fc..379a2a7e 100644 --- a/config/auto-doc.php +++ b/config/auto-doc.php @@ -101,6 +101,15 @@ '204' => 'Operation successfully done', '404' => 'This entity not found', ], + + /* + |-------------------------------------------------------------------------- + | Error Template + |-------------------------------------------------------------------------- + | + | You can use your custom description view for errors. + */ + 'error' => 'auto-doc::error', ], /* diff --git a/resources/views/error.blade.php b/resources/views/error.blade.php new file mode 100644 index 00000000..0b9291cb --- /dev/null +++ b/resources/views/error.blade.php @@ -0,0 +1 @@ +{{ $message }} \ No newline at end of file diff --git a/src/AutoDocServiceProvider.php b/src/AutoDocServiceProvider.php index 0e6d435b..7b92fb17 100644 --- a/src/AutoDocServiceProvider.php +++ b/src/AutoDocServiceProvider.php @@ -19,6 +19,10 @@ public function boot() __DIR__ . '/../resources/views/swagger-description.blade.php' => resource_path('views/vendor/auto-doc/swagger-description.blade.php'), ], 'view'); + $this->publishes([ + __DIR__ . '/../resources/views/error.blade.php' => resource_path('views/vendor/auto-doc/error.blade.php'), + ], 'view'); + if (!$this->app->routesAreCached()) { require __DIR__ . '/Http/routes.php'; } diff --git a/src/Drivers/LocalDriver.php b/src/Drivers/LocalDriver.php index ab0c233a..bd87a1bd 100755 --- a/src/Drivers/LocalDriver.php +++ b/src/Drivers/LocalDriver.php @@ -30,7 +30,7 @@ public function saveData(): void public function getDocumentation(): array { if (!file_exists($this->prodFilePath)) { - throw new FileNotFoundException(); + throw new FileNotFoundException($this->prodFilePath); } $fileContent = file_get_contents($this->prodFilePath); diff --git a/src/Drivers/RemoteDriver.php b/src/Drivers/RemoteDriver.php index 0baa6601..b85921ac 100755 --- a/src/Drivers/RemoteDriver.php +++ b/src/Drivers/RemoteDriver.php @@ -36,7 +36,7 @@ public function getDocumentation(): array list($content, $statusCode) = $this->makeHttpRequest('get', $this->getUrl()); if (empty($content) || $statusCode !== 200) { - throw new FileNotFoundException(); + throw new FileNotFoundException('Documentation file not found.'); } return json_decode($content, true); diff --git a/src/Drivers/StorageDriver.php b/src/Drivers/StorageDriver.php index 4dbdbdf9..2389f651 100755 --- a/src/Drivers/StorageDriver.php +++ b/src/Drivers/StorageDriver.php @@ -34,7 +34,7 @@ public function saveData(): void public function getDocumentation(): array { if (!$this->disk->exists($this->prodFilePath)) { - throw new FileNotFoundException(); + throw new FileNotFoundException("Documentation file not found :{$this->prodFilePath}"); } $fileContent = $this->disk->get($this->prodFilePath); diff --git a/src/Services/SwaggerService.php b/src/Services/SwaggerService.php index 571b0ecf..e13a1343 100755 --- a/src/Services/SwaggerService.php +++ b/src/Services/SwaggerService.php @@ -23,6 +23,7 @@ use RonasIT\AutoDoc\Traits\GetDependenciesTrait; use RonasIT\AutoDoc\Validators\SwaggerSpecValidator; use Symfony\Component\HttpFoundation\Response; +use Exception; /** * @property SwaggerDriverContract $driver @@ -160,6 +161,20 @@ protected function generateEmptyData(): array return $data; } + protected function generateBaseDataObject(): array + { + return [ + 'openapi' => self::OPEN_API_VERSION, + 'servers' => [ + ['url' => URL::query($this->config['basePath'])], + ], + 'paths' => [], + 'components' => [ + 'schemas' => $this->config['definitions'], + ], + ]; + } + protected function generateSecurityDefinition(): ?array { if (empty($this->security)) { @@ -817,9 +832,20 @@ public function saveProductionData() public function getDocFileContent() { - $documentation = $this->driver->getDocumentation(); + try { + $documentation = $this->driver->getDocumentation(); - $this->openAPIValidator->validate($documentation); + $this->openAPIValidator->validate($documentation); + } catch (Exception $exception) { + $data = $this->generateBaseDataObject(); + + $infoConfig = $this->config['info']; + $infoConfig['description'] = Arr::get($this->config, 'defaults.error'); + + $data['info'] = $this->prepareInfo($infoConfig, ['message' => $exception->getMessage()]); + + return $data; + } $additionalDocs = config('auto-doc.additional_paths', []); @@ -946,7 +972,7 @@ protected function getDefaultValueByType($type) return $values[$type]; } - protected function prepareInfo(array $info): array + protected function prepareInfo(array $info, array $descriptionData = []): array { if (empty($info)) { return $info; @@ -963,7 +989,7 @@ protected function prepareInfo(array $info): array } if (!empty($info['description'])) { - $info['description'] = view($info['description'])->render(); + $info['description'] = view($info['description'], $descriptionData)->render(); } return $info; diff --git a/tests/SwaggerServiceTest.php b/tests/SwaggerServiceTest.php index 51ed48f4..ce934bab 100644 --- a/tests/SwaggerServiceTest.php +++ b/tests/SwaggerServiceTest.php @@ -3,24 +3,11 @@ namespace RonasIT\AutoDoc\Tests; use Illuminate\Http\Testing\File; +use Illuminate\Support\Facades\View; use PHPUnit\Framework\Attributes\DataProvider; use RonasIT\AutoDoc\Exceptions\EmptyContactEmailException; use RonasIT\AutoDoc\Exceptions\InvalidDriverClassException; use RonasIT\AutoDoc\Exceptions\LegacyConfigException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\DuplicateFieldException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\DuplicateParamException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\DuplicatePathPlaceholderException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidFieldValueException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidPathException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidStatusCodeException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerSpecException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerVersionException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingExternalRefException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingFieldException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingLocalRefException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingPathParamException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingPathPlaceholderException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingRefFileException; use RonasIT\AutoDoc\Exceptions\SwaggerDriverClassNotFoundException; use RonasIT\AutoDoc\Exceptions\UnsupportedDocumentationViewerException; use RonasIT\AutoDoc\Exceptions\WrongSecurityConfigException; @@ -90,215 +77,178 @@ public static function getConstructorInvalidTmpData(): array return [ [ 'tmpDoc' => 'documentation/invalid_version', - 'exception' => InvalidSwaggerVersionException::class, - 'exceptionMessage' => "Unrecognized Swagger version '1.0'. Expected 3.1.0.", + 'exceptionMessage' => "Validation failed. Unrecognized Swagger version '1.0'. Expected 3.1.0.", ], [ 'tmpDoc' => 'documentation/invalid_format__array_parameter__no_items', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. paths./users.post.parameters.0 is an " . "array, so it must include an 'items' field.", ], [ 'tmpDoc' => 'documentation/invalid_format__array_response_body__no_items', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. paths./users.get.responses.200.schema is an array, " . "so it must include an 'items' field.", ], [ 'tmpDoc' => 'documentation/invalid_format__array_response_header__no_items', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. paths./users.get.responses.default.headers." . "Last-Modified is an array, so it must include an 'items' field.", ], [ 'tmpDoc' => 'documentation/invalid_format__body_and_form_params', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}.post' " . "has body and formData parameters. Only one or the other is allowed.", ], [ 'tmpDoc' => 'documentation/invalid_format__duplicate_header_params', - 'exception' => DuplicateParamException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}.get' " . "has multiple in:header parameters with name:foo.", ], [ 'tmpDoc' => 'documentation/invalid_format__duplicate_path_params', - 'exception' => DuplicateParamException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}.get' has " - . "multiple in:path parameters with name:username", + . "multiple in:path parameters with name:username.", ], [ 'tmpDoc' => 'documentation/invalid_format__duplicate_path_placeholders', - 'exception' => DuplicatePathPlaceholderException::class, 'exceptionMessage' => "Validation failed. Path '/users/{username}/profile/{username}/image/{img_id}' " . "has multiple path placeholders with name: username.", ], [ 'tmpDoc' => 'documentation/invalid_format__duplicate_operation_id', - 'exception' => DuplicateFieldException::class, 'exceptionMessage' => "Validation failed. Found multiple fields 'paths.*.*.operationId' " . "with values: addPet.", ], [ 'tmpDoc' => 'documentation/invalid_format__duplicate_tag', - 'exception' => DuplicateFieldException::class, 'exceptionMessage' => "Validation failed. Found multiple fields 'tags.*.name' with values: user.", ], [ 'tmpDoc' => 'documentation/invalid_format__file_invalid_consumes', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}/profile/image.post' " . "has body and formData parameters. Only one or the other is allowed.", ], [ 'tmpDoc' => 'documentation/invalid_format__file_no_consumes', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}/profile/image.post' " . "has body and formData parameters. Only one or the other is allowed.", ], [ 'tmpDoc' => 'documentation/invalid_format__multiple_body_params', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}.get' has 2 body " . "parameters. Only one is allowed.", ], [ 'tmpDoc' => 'documentation/invalid_format__no_path_params', - 'exception' => MissingPathParamException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}/{foo}.get' has " . "no params for placeholders: username, foo.", ], [ 'tmpDoc' => 'documentation/invalid_format__path_param_no_placeholder', - 'exception' => MissingPathPlaceholderException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{username}.post' has no " . "placeholders for params: foo.", ], [ 'tmpDoc' => 'documentation/invalid_format__invalid_value__path', - 'exception' => InvalidPathException::class, 'exceptionMessage' => "Validation failed. Incorrect 'paths.users'. Paths should only have path " . "names that starts with `/`.", ], [ 'tmpDoc' => 'documentation/invalid_format__invalid_value__status_code', - 'exception' => InvalidStatusCodeException::class, 'exceptionMessage' => "Validation failed. Operation at 'paths./users.get.responses.8888' should " . "only have three-digit status codes, `default`, and vendor extensions (`x-*`) as properties.", ], [ 'tmpDoc' => 'documentation/invalid_format__invalid_value__parameter_in', - 'exception' => InvalidFieldValueException::class, 'exceptionMessage' => "Validation failed. Field 'paths./auth/login.post.parameters.0.in' " . "has an invalid value: invalid_in. Allowed values: body, formData, query, path, header.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__paths', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. '' should have required fields: paths.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__operation_responses', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'paths./auth/login.post' should have required " . "fields: responses.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__parameter_in', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'paths./auth/login.post.parameters.0' should " . "have required fields: in.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__response_description', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'paths./auth/login.post.responses.200' should " . "have required fields: description.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__definition_type', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'components.schemas.authloginObject' should have " . "required fields: type.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__info_version', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'info' should have required fields: version.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__items_type', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'paths./pet/findByStatus.get.parameters.0.schema.items' " . "should have required fields: type.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__header_type', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'paths./user/login.get.responses.200.headers.X-Rate-Limit' " . "should have required fields: type.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_field__tag_name', - 'exception' => MissingFieldException::class, 'exceptionMessage' => "Validation failed. 'tags.0' should have required fields: name.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_local_ref', - 'exception' => MissingLocalRefException::class, 'exceptionMessage' => "Validation failed. Ref 'loginObject' is used in \$ref but not defined " . "in 'definitions' field.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_external_ref', - 'exception' => MissingExternalRefException::class, 'exceptionMessage' => "Validation failed. Ref 'authloginObject' is used in \$ref but not defined " . "in 'tests/fixtures/SwaggerServiceTest/documentation/with_definitions.json' file.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_ref_file', - 'exception' => MissingRefFileException::class, 'exceptionMessage' => "Validation failed. Filename 'invalid-filename.json' is used in \$ref but " . "file doesn't exist.", ], [ 'tmpDoc' => 'documentation/invalid_format__invalid_schema_type', - 'exception' => InvalidFieldValueException::class, 'exceptionMessage' => "Validation failed. Field 'paths./users.get.responses.200.schema.type' " . "has an invalid value: something. Allowed values: array, boolean, integer, number, " . "string, object, null, undefined, file.", ], [ 'tmpDoc' => 'documentation/invalid_format__missing_path_parameter', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Path parameters cannot be optional. " . "Set required=true for the 'username' parameters at operation 'paths./users.get'.", ], [ 'tmpDoc' => 'documentation/invalid_format__security_definition__type', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Field 'securityDefinitions.0.type' has an invalid value: invalid. Allowed values: basic, apiKey, oauth2.", ], [ 'tmpDoc' => 'documentation/invalid_format__security_definition__flow', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Field 'securityDefinitions.0.flow' has an invalid value: invalid. Allowed values: implicit, password, application, accessCode.", ], [ 'tmpDoc' => 'documentation/invalid_format__security_definition__in', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Field 'securityDefinitions.0.in' has an invalid value: invalid. Allowed values: query, header.", ], [ 'tmpDoc' => 'documentation/invalid_format__request_body__invalid_content', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. Operation 'paths./users/{id}.post' has invalid content types: image/png.", ], [ 'tmpDoc' => 'documentation/invalid_format__response__invalid_items', - 'exception' => InvalidSwaggerSpecException::class, 'exceptionMessage' => "Validation failed. 'paths./users/{id}.post.responses.200.schema.items' should have required fields: type.", ], ]; @@ -307,15 +257,17 @@ public static function getConstructorInvalidTmpData(): array #[DataProvider('getConstructorInvalidTmpData')] public function testGetDocFileContentInvalidTmpData( string $tmpDoc, - string $exception, string $exceptionMessage, ) { $this->mockDriverGetDocumentation($this->getJsonFixture($tmpDoc)); - $this->expectException($exception); - $this->expectExceptionMessage($exceptionMessage); - app(SwaggerService::class)->getDocFileContent(); + + View::addLocation(__DIR__ . '/../resources/views'); + + $rendered = view('error', ['message' => $exceptionMessage])->render(); + + $this->assertStringContainsString($exceptionMessage, html_entity_decode($rendered)); } public function testEmptyContactEmail()