diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e0f975b..c903bd7 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,27 +10,13 @@ jobs: strategy: fail-fast: false matrix: - php: [7.4, 8.0, 8.1, 8.2] - laravel: [8.*, 9.*, 10.*] + php: [8.1, 8.2] + laravel: [9.*, 10.*] dependency-version: [prefer-lowest, prefer-stable] exclude: - php: 8.2 laravel: 9.* dependency-version: prefer-lowest - - php: 8.2 - laravel: 8.* - - php: 8.1 - laravel: 8.* - dependency-version: prefer-lowest - - php: 8.0 - laravel: 10.* - - php: 8.0 - laravel: 8.* - dependency-version: prefer-lowest - - php: 7.4 - laravel: 9.* - - php: 7.4 - laravel: 10.* name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} diff --git a/README.md b/README.md index ca9d32b..303bff6 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ protected function gate() ``` In the published `config/swagger-ui.php` file, you edit the path to the Swagger UI and the location of the Swagger (OpenAPI v3) file. By default, the package expects to find the OpenAPI file in 'resources/swagger' directory. You can also provide an url if the OpenAPI file is not present in the Laravel project itself. +This is also where you can define multiple versions for the same api. ```php // in config/swagger-ui.php @@ -53,7 +54,9 @@ return [ 'path' => 'swagger', - 'file' => resource_path('swagger/openapi.json'), + 'versions' => [ + 'v1' => resource_path('swagger/openapi.json') + ], // ... ]; diff --git a/composer.json b/composer.json index 0208491..3504cb1 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,9 @@ } ], "require": { - "php": "^7.4|^8.0|^8.1|^8.2", + "php": "^8.1|^8.2", "ext-json": "*", - "laravel/framework": "^8.0|^9.0|^10.0" + "laravel/framework": "^9.0|^10.0" }, "suggest": { "ext-yaml": "*" @@ -29,6 +29,7 @@ "require-dev": { "adamwojs/php-cs-fixer-phpdoc-force-fqcn": "^2.0", "friendsofphp/php-cs-fixer": "^3.0", + "jasonmccreary/laravel-test-assertions": "^2.3", "orchestra/testbench": "^6.0|^7.0|^8.0", "phpunit/phpunit": "^9.1", "squizlabs/php_codesniffer": "^3.6" diff --git a/config/swagger-ui.php b/config/swagger-ui.php index 5dedac2..f00cfa0 100644 --- a/config/swagger-ui.php +++ b/config/swagger-ui.php @@ -3,73 +3,49 @@ use NextApps\SwaggerUi\Http\Middleware\EnsureUserIsAuthorized; return [ - /* - |-------------------------------------------------------------------------- - | Swagger UI - Path - |-------------------------------------------------------------------------- - | - | This is the URI path where SwaggerUI will be accessible from. Feel free - | to change this path to anything you like. - | - */ - - 'path' => 'swagger', - - /* - |-------------------------------------------------------------------------- - | Swagger UI - Route Middleware - |-------------------------------------------------------------------------- - | - | These middleware will be assigned to every Swagger UI route. - | - */ - - 'middleware' => [ - 'web', - EnsureUserIsAuthorized::class, - ], - - /* - |-------------------------------------------------------------------------- - | Swagger UI - OpenAPI File - |-------------------------------------------------------------------------- - | - | This is the location of the project's OpenAPI / Swagger JSON file. It's - | this file that will be used in Swagger UI. This can either be a local - | file or an url to a file. - | - */ - - 'file' => resource_path('swagger/openapi.json'), - - /* - |-------------------------------------------------------------------------- - | Swagger UI - Modify File - |-------------------------------------------------------------------------- - | - | If this option is enabled, then the file will be changed before it is - | used by Swagger UI. The server url and oauth urls will be changed to - | the base url of this Laravel application. - | - */ - - 'modify_file' => true, - - /* - |-------------------------------------------------------------------------- - | Swagger UI - OAuth Config - |-------------------------------------------------------------------------- - | - | This allows you to configure oauth within Swagger UI. It makes it easier - | to authenticate in Swagger UI by prefilling certain values. - | - */ - 'oauth' => [ - 'token_path' => 'oauth/token', - 'refresh_path' => 'oauth/token', - 'authorization_path' => 'oauth/authorize', - - 'client_id' => env('SWAGGER_UI_OAUTH_CLIENT_ID'), - 'client_secret' => env('SWAGGER_UI_OAUTH_CLIENT_SECRET'), + 'files' => [ + [ + /* + * The path where the swagger file is served. + */ + 'path' => 'swagger', + + /* + * The versions of the swagger file. The key is the version name and the value is the path to the file. + */ + 'versions' => [ + 'v1' => resource_path('swagger/openapi.json'), + ], + + /* + * The default version that is loaded when the route is accessed. + */ + 'default' => 'v1', + + /* + * The middleware that is applied to the route. + */ + 'middleware' => [ + 'web', + EnsureUserIsAuthorized::class, + ], + + /* + * If enabled the file will be modified to set the server url and oauth urls. + */ + 'modify_file' => true, + + /* + * The oauth configuration for the swagger file. + */ + 'oauth' => [ + 'token_path' => 'oauth/token', + 'refresh_path' => 'oauth/token', + 'authorization_path' => 'oauth/authorize', + + 'client_id' => env('SWAGGER_UI_OAUTH_CLIENT_ID'), + 'client_secret' => env('SWAGGER_UI_OAUTH_CLIENT_SECRET'), + ], + ], ], ]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f2862a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "laravel-swagger-ui", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index d75c928..a777e60 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -27,22 +27,32 @@
+ diff --git a/src/Http/Controllers/OpenApiJsonController.php b/src/Http/Controllers/OpenApiJsonController.php index 1fe0700..f14a1d8 100644 --- a/src/Http/Controllers/OpenApiJsonController.php +++ b/src/Http/Controllers/OpenApiJsonController.php @@ -2,26 +2,42 @@ namespace NextApps\SwaggerUi\Http\Controllers; +use Exception; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Support\ItemNotFoundException; use Illuminate\Support\Str; use RuntimeException; class OpenApiJsonController { - public function __invoke() : JsonResponse + public function __invoke(Request $request, string $filename) : JsonResponse { - $json = $this->getJson(); + $path = $request->segment(1); + + try { + $file = collect(config('swagger-ui.files'))->filter(function ($values) use ($filename, $path) { + return isset($values['versions'][$filename]) && ltrim($values['path'], '/') === $path; + })->firstOrFail(); + } catch (ItemNotFoundException) { + return abort(404); + } + + $json = $this->getJson($file['versions'][$filename]); - $json = $this->configureServer($json); - $json = $this->configureOAuth($json); + $json = $this->configureServer($file, $json); + $json = $this->configureOAuth($file, $json); return response()->json($json); } - protected function getJson() : array + protected function getJson(string $path) : array { - $path = config('swagger-ui.file'); - $content = file_get_contents($path); + try { + $content = file_get_contents($path); + } catch (Exception $e) { + throw new RuntimeException('OpenAPI file can not be read'); + } if (Str::endsWith($path, '.yaml')) { if (! extension_loaded('yaml')) { @@ -34,9 +50,9 @@ protected function getJson() : array return json_decode($content, true); } - protected function configureServer(array $json) : array + protected function configureServer(array $file, array $json) : array { - if (! config('swagger-ui.modify_file')) { + if (! $file['modify_file']) { return $json; } @@ -47,28 +63,28 @@ protected function configureServer(array $json) : array return $json; } - protected function configureOAuth(array $json) : array + protected function configureOAuth(array $file, array $json) : array { - if (empty($json['components']['securitySchemes']) || ! config('swagger-ui.modify_file')) { + if (empty($json['components']['securitySchemes']) || ! $file['modify_file']) { return $json; } - $securitySchemes = collect($json['components']['securitySchemes'])->map(function ($scheme) { + $securitySchemes = collect($json['components']['securitySchemes'])->map(function ($scheme) use ($file) { if ($scheme['type'] !== 'oauth2') { return $scheme; } - $scheme['flows'] = collect($scheme['flows'])->map(function ($flow) { + $scheme['flows'] = collect($scheme['flows'])->map(function ($flow) use ($file) { if (isset($flow['tokenUrl'])) { - $flow['tokenUrl'] = url(config('swagger-ui.oauth.token_path')); + $flow['tokenUrl'] = url($file['oauth']['token_path']); } if (isset($flow['refreshUrl'])) { - $flow['refreshUrl'] = url(config('swagger-ui.oauth.refresh_path')); + $flow['refreshUrl'] = url($file['oauth']['refresh_path']); } if (isset($flow['authorizationUrl'])) { - $flow['authorizationUrl'] = url(config('swagger-ui.oauth.authorization_path')); + $flow['authorizationUrl'] = url($file['oauth']['authorization_path']); } return $flow; diff --git a/src/Http/Controllers/SwaggerViewController.php b/src/Http/Controllers/SwaggerViewController.php new file mode 100644 index 0000000..e06c240 --- /dev/null +++ b/src/Http/Controllers/SwaggerViewController.php @@ -0,0 +1,22 @@ +filter(function ($values) use ($request) { + return ltrim($values['path'], '/') === $request->path(); + })->firstOrFail(); + } catch (ItemNotFoundException) { + return abort(404); + } + + return view('swagger-ui::index', ['data' => collect($file)]); + } +} diff --git a/src/SwaggerUiServiceProvider.php b/src/SwaggerUiServiceProvider.php index d801e32..4380269 100644 --- a/src/SwaggerUiServiceProvider.php +++ b/src/SwaggerUiServiceProvider.php @@ -6,7 +6,7 @@ use Illuminate\Support\ServiceProvider; use NextApps\SwaggerUi\Console\InstallCommand; use NextApps\SwaggerUi\Http\Controllers\OpenApiJsonController; -use NextApps\SwaggerUi\Http\Middleware\EnsureUserIsAuthorized; +use NextApps\SwaggerUi\Http\Controllers\SwaggerViewController; class SwaggerUiServiceProvider extends ServiceProvider { @@ -36,12 +36,13 @@ public function register() : void protected function loadRoutes() : void { - Route::middleware(config('swagger-ui.middleware', ['web', EnsureUserIsAuthorized::class])) - ->prefix(config('swagger-ui.path')) - ->group(function () { - Route::view('/', 'swagger-ui::index')->name('swagger-ui'); - - Route::get('openapi.json', OpenApiJsonController::class)->name('swagger-openapi-json'); - }); + collect(config('swagger-ui.files'))->each(function ($values) { + Route::middleware($values['middleware']) + ->group(function () use ($values) { + Route::get($values['path'], SwaggerViewController::class)->name($values['path'] . '.index'); + + Route::get($values['path'] . '/{filename}', OpenApiJsonController::class)->name($values['path'] . '.json'); + }); + }); } } diff --git a/tests/AuthorizationTest.php b/tests/AuthorizationTest.php index 5bc0e37..445a8c8 100644 --- a/tests/AuthorizationTest.php +++ b/tests/AuthorizationTest.php @@ -1,11 +1,10 @@ set('swagger-ui.file', __DIR__ . '/testfiles/openapi.json'); + config()->set('swagger-ui.files.0.versions', ['v1' => __DIR__ . '/testfiles/openapi.json']); } protected function getPackageProviders($app) : array @@ -25,7 +24,7 @@ protected function getPackageProviders($app) : array public function it_denies_access_in_default_installation() { $this->get('swagger')->assertStatus(403); - $this->get('swagger/openapi.json')->assertStatus(403); + $this->get('swagger/v1')->assertStatus(403); } /** @test */ @@ -34,7 +33,7 @@ public function it_denies_access_in_default_installation_for_any_auth_user() $this->actingAs(new Authenticated()); $this->get('swagger')->assertStatus(403); - $this->get('swagger/openapi.json')->assertStatus(403); + $this->get('swagger/v1')->assertStatus(403); } /** @test */ @@ -43,7 +42,7 @@ public function it_denies_access_for_guests() Gate::define('viewSwaggerUI', fn () => true); $this->get('swagger')->assertStatus(403); - $this->get('swagger/openapi.json')->assertStatus(403); + $this->get('swagger/v1')->assertStatus(403); } /** @test */ @@ -56,7 +55,7 @@ public function it_allows_access_to_user_if_allowed_by_gate() }); $this->get('swagger')->assertStatus(200); - $this->get('swagger/openapi.json')->assertStatus(200); + $this->get('swagger/v1')->assertStatus(200); } /** @test */ @@ -67,7 +66,7 @@ public function it_denies_access_to_user_if_not_allowed_by_gate() Gate::define('viewSwaggerUI', fn () => false); $this->get('swagger')->assertStatus(403); - $this->get('swagger/openapi.json')->assertStatus(403); + $this->get('swagger/v1')->assertStatus(403); } /** @test */ @@ -76,7 +75,7 @@ public function it_allows_access_to_guest_if_allowed_by_gate() Gate::define('viewSwaggerUI', fn (?Authenticated $user) => true); $this->get('swagger')->assertStatus(200); - $this->get('swagger/openapi.json')->assertStatus(200); + $this->get('swagger/v1')->assertStatus(200); } } diff --git a/tests/OpenApiRouteTest.php b/tests/OpenApiRouteTest.php index 1ad2ceb..5f769e3 100644 --- a/tests/OpenApiRouteTest.php +++ b/tests/OpenApiRouteTest.php @@ -1,11 +1,10 @@ set('swagger-ui.file', __DIR__ . '/testfiles/openapi.json'); + config()->set('swagger-ui.files.0.versions', ['v1' => __DIR__ . '/testfiles/openapi.json']); Gate::define('viewSwaggerUI', fn (?Authenticatable $user) => true); } @@ -38,11 +37,11 @@ public function openApiFileProvider() : array */ public function it_sets_server_to_current_app_url_if_modify_file_is_enabled($openApiFile) { - config()->set('swagger-ui.file', $openApiFile); - config()->set('swagger-ui.modify_file', true); + config()->set('swagger-ui.files.0.versions', ['v1' => $openApiFile]); + config()->set('swagger-ui.files.0.modify_file', true); config()->set('app.url', 'http://foo.bar'); - $this->get('swagger/openapi.json') + $this->get('swagger/v1') ->assertStatus(200) ->assertJsonCount(1, 'servers') ->assertJsonPath('servers.0.url', 'http://foo.bar'); @@ -55,13 +54,11 @@ public function it_sets_server_to_current_app_url_if_modify_file_is_enabled($ope */ public function it_sets_oauth_urls_by_combining_configured_paths_with_current_app_url_if_modify_file_is_enabled($openApiFile) { - config()->set('swagger-ui.file', $openApiFile); - config()->set('swagger-ui.modify_file', true); - config()->set('swagger-ui.oauth.token_path', 'this-is-token-path'); - config()->set('swagger-ui.oauth.refresh_path', 'this-is-refresh-path'); - config()->set('swagger-ui.oauth.authorization_path', 'this-is-authorization-path'); + config()->set('swagger-ui.files.0.versions', ['v1' => $openApiFile]); + config()->set('swagger-ui.files.0.modify_file', true); + config()->set('swagger-ui.files.0.oauth', ['token_path' => 'this-is-token-path', 'refresh_path' => 'this-is-refresh-path', 'authorization_path' => 'this-is-authorization-path']); - $this->get('swagger/openapi.json') + $this->get('swagger/v1') ->assertStatus(200) ->assertJsonPath('components.securitySchemes.Foobar.flows.password.tokenUrl', 'http://localhost/this-is-token-path') ->assertJsonPath('components.securitySchemes.Foobar.flows.password.refreshUrl', 'http://localhost/this-is-refresh-path') @@ -77,11 +74,12 @@ public function it_sets_oauth_urls_by_combining_configured_paths_with_current_ap */ public function it_doesnt_sets_server_to_current_app_url_if_modify_file_is_disabled($openApiFile) { - config()->set('swagger-ui.file', $openApiFile); - config()->set('swagger-ui.modify_file', false); + config()->set('swagger-ui.files.0.versions', ['v1' => $openApiFile]); + config()->set('swagger-ui.files.0.modify_file', false); + config()->set('app.url', 'http://foo.bar'); - $this->get('swagger/openapi.json') + $this->get('swagger/v1') ->assertStatus(200) ->assertJsonCount(1, 'servers') ->assertJsonPath('servers.0.url', 'http://localhost:3000'); @@ -94,13 +92,11 @@ public function it_doesnt_sets_server_to_current_app_url_if_modify_file_is_disab */ public function it_doesnt_sets_oauth_urls_by_combining_configured_paths_with_current_app_url_if_modify_file_is_disabled($openApiFile) { - config()->set('swagger-ui.file', $openApiFile); - config()->set('swagger-ui.modify_file', false); - config()->set('swagger-ui.oauth.token_path', 'this-is-token-path'); - config()->set('swagger-ui.oauth.refresh_path', 'this-is-refresh-path'); - config()->set('swagger-ui.oauth.authorization_path', 'this-is-authorization-path'); + config()->set('swagger-ui.files.0.versions', ['v1' => $openApiFile]); + config()->set('swagger-ui.files.0.modify_file', false); + config()->set('swagger-ui.files.0.oauth', ['token_path' => 'this-is-token-path', 'refresh_path' => 'this-is-refresh-path', 'authorization_path' => 'this-is-authorization-path']); - $this->get('swagger/openapi.json') + $this->get('swagger/v1') ->assertStatus(200) ->assertJsonPath('components.securitySchemes.Foobar.flows.password.tokenUrl', 'http://localhost:3000/password/tokenUrl') ->assertJsonPath('components.securitySchemes.Foobar.flows.password.refreshUrl', 'http://localhost:3000/password/refreshUrl') @@ -108,4 +104,25 @@ public function it_doesnt_sets_oauth_urls_by_combining_configured_paths_with_cur ->assertJsonPath('components.securitySchemes.Foobar.flows.authorizationCode.tokenUrl', 'http://localhost:3000/authorizationCode/tokenUrl') ->assertJsonPath('components.securitySchemes.Foobar.flows.authorizationCode.refreshUrl', 'http://localhost:3000/authorizationCode/refreshUrl'); } + + /** @test */ + public function it_returns_not_found_response_if_provided_version_does_not_exist_in_file() + { + $this->getJson('swagger/v4') + ->assertStatus(404); + } + + /** @test */ + public function it_returns_not_found_response_if_provided_file_does_not_exist_even_when_provided_version_exists() + { + $this->getJson('foo-bar/v1') + ->assertStatus(404); + } + + /** @test */ + public function it_returns_not_found_response_if_provided_route_does_not_exist() + { + $this->getJson('foo-bar') + ->assertStatus(404); + } } diff --git a/tests/SwaggerUiRouteTest.php b/tests/SwaggerUiRouteTest.php index 10281a9..a035f7a 100644 --- a/tests/SwaggerUiRouteTest.php +++ b/tests/SwaggerUiRouteTest.php @@ -1,11 +1,11 @@ set('swagger-ui.file', __DIR__ . '/testfiles/openapi.json'); + config()->set('swagger-ui.files.0.versions', ['v1' => __DIR__ . '/testfiles/openapi.json']); + config()->set('swagger-ui.files.0.oauth', ['client_id' => 1, 'client_secret' => 'foobar']); Gate::define('viewSwaggerUI', fn (?Authenticatable $user) => true); } @@ -26,23 +27,34 @@ protected function getPackageProviders($app) : array /** @test */ public function it_provides_openapi_route_as_url() { - config()->set('swagger-ui.oauth.client_id', 1); - config()->set('swagger-ui.oauth.client_secret', 'foobar'); - $this->get('swagger') ->assertStatus(200) - ->assertSee('url: \'' . route('swagger-openapi-json', [], false) . '\'', false); + ->assertSee('url: \'swagger/v1\'', false); } /** @test */ public function it_fills_oauth_client_id_and_secret_from_config() { - config()->set('swagger-ui.oauth.client_id', 1); - config()->set('swagger-ui.oauth.client_secret', 'foobar'); - $this->get('swagger') ->assertStatus(200) ->assertSee('clientId: \'1\',', false) ->assertSee('clientSecret: \'foobar\',', false); } + + /** @test */ + public function it_supports_multiple_verions() + { + $this->get('swagger-with-versions') + ->assertStatus(200) + ->assertSee('url: \'swagger-with-versions/v1\'', false) + ->assertSee('url: \'swagger-with-versions/v2\'', false); + } + + /** @test */ + public function it_applies_middleware_from_config() + { + $this->assertRouteUsesMiddleware('swagger.index', ['web', EnsureUserIsAuthorized::class]); + + $this->assertRouteUsesMiddleware('swagger-with-versions.index', ['web']); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..6ac2378 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,22 @@ +set('swagger-ui.files.1', config('swagger-ui.files.0')); + $app['config']->set('swagger-ui.files.1.versions', [ + 'v1' => __DIR__ . '/testfiles/openapi.json', + 'v2' => __DIR__ . '/testfiles/openapi-v2.json', + ]); + $app['config']->set('swagger-ui.files.1.path', 'swagger-with-versions'); + $app['config']->set('swagger-ui.files.1.middleware', ['web']); + } +}