Skip to content

Commit

Permalink
Merge pull request #350 from dedoc/143-add-ability-to-manually-regist…
Browse files Browse the repository at this point in the history
…er-docs-routes-or-some-other-way-to-customize-routes

Ability to configure more API versions docs, customize docs domains and paths
  • Loading branch information
romalytvynenko authored Mar 25, 2024
2 parents 23574d2 + 511a1b2 commit 9872528
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 114 deletions.
16 changes: 10 additions & 6 deletions config/scramble.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@
*/
'export_path' => 'api.json',

/*
* Define the theme of the documentation.
* Available options are `light` and `dark`.
*/
'theme' => 'light',

'info' => [
/*
* API version.
Expand All @@ -42,6 +36,16 @@
* Customize Stoplight Elements UI
*/
'ui' => [
/*
* Define the title of the documentation's website. App name is used when this config is `null`.
*/
'title' => null,

/*
* Define the theme of the documentation. Available options are `light` and `dark`.
*/
'theme' => 'light',

/*
* Hide the `Try It` feature. Enabled by default.
*/
Expand Down
18 changes: 12 additions & 6 deletions resources/views/docs.blade.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<!doctype html>
<html lang="en" data-theme="{{ config('scramble.theme', 'light') }}">
<html lang="en" data-theme="{{ $config->get('ui.theme', 'light') }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ config('app.name') }} - API Docs</title>
<title>{{ $config->get('ui.title', config('app.name') . ' - API Docs') }}</title>

<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
Expand Down Expand Up @@ -50,11 +50,17 @@
</head>
<body style="height: 100vh; overflow-y: hidden">
<elements-api
apiDescriptionUrl="{{ route('scramble.docs.index') }}"
tryItCredentialsPolicy="{{ config('scramble.ui.try_it_credentials_policy', 'include') }}"
id="docs"
tryItCredentialsPolicy="{{ $config->get('ui.try_it_credentials_policy', 'include') }}"
router="hash"
@if(config('scramble.ui.hide_try_it')) hideTryIt="true" @endif
logo="{{ config('scramble.ui.logo') }}"
@if($config->get('ui.hide_try_it')) hideTryIt="true" @endif
logo="{{ $config->get('ui.logo') }}"
/>
<script>
(async () => {
const docs = document.getElementById('docs');
docs.apiDescriptionDocument = @json($spec);
})();
</script>
</body>
</html>
11 changes: 3 additions & 8 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
<?php

use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
use Illuminate\Support\Facades\Route;
use Dedoc\Scramble\Scramble;

Route::middleware(config('scramble.middleware', [RestrictedDocsAccess::class]))->group(function () {
Route::get('docs/api.json', function (Dedoc\Scramble\Generator $generator) {
return response()->json($generator(), options: JSON_PRETTY_PRINT);
})->name('scramble.docs.index');
Scramble::registerUiRoute(path: 'docs/api')->name('scramble.docs.ui');

Route::view('docs/api', 'scramble::docs')->name('scramble.docs.api');
});
Scramble::registerJsonSpecificationRoute(path: 'docs/api.json')->name('scramble.docs.document');
32 changes: 32 additions & 0 deletions src/Console/Commands/ExportDocumentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Dedoc\Scramble\Console\Commands;

use Dedoc\Scramble\Generator;
use Dedoc\Scramble\Scramble;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class ExportDocumentation extends Command
{
protected $signature = 'scramble:export
{--path= : The path to save the exported JSON file}
{--api=default : The API to export a documentation for}
';

protected $description = 'Export the OpenAPI document to a JSON file.';

public function handle(Generator $generator): void
{
$config = Scramble::getGeneratorConfig($api = $this->option('api'));

$specification = json_encode($generator($config));

/** @var string $filename */
$filename = $this->option('path') ?? $config->get('export_path', 'api'.($api === 'default' ? '' : "-$api").'.json');

File::put($filename, $specification);

$this->info("OpenAPI document exported to {$filename}.");
}
}
42 changes: 0 additions & 42 deletions src/Console/Commands/ExportSpecifications.php

This file was deleted.

70 changes: 25 additions & 45 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,24 @@

class Generator
{
private TypeTransformer $transformer;

private OperationBuilder $operationBuilder;

private ServerFactory $serverFactory;

private FileParser $fileParser;

private Infer $infer;

public function __construct(
TypeTransformer $transformer,
OperationBuilder $operationBuilder,
ServerFactory $serverFactory,
FileParser $fileParser,
Infer $infer
private TypeTransformer $transformer,
private OperationBuilder $operationBuilder,
private ServerFactory $serverFactory,
private FileParser $fileParser,
private Infer $infer
) {
$this->transformer = $transformer;
$this->operationBuilder = $operationBuilder;
$this->serverFactory = $serverFactory;
$this->fileParser = $fileParser;
$this->infer = $infer;
}

public function __invoke()
public function __invoke(?GeneratorConfig $config = null)
{
$openApi = $this->makeOpenApi();
$config ??= (new GeneratorConfig(config('scramble')))
->routes(Scramble::$routeResolver)
->afterOpenApiGenerated(Scramble::$openApiExtender);

$this->getRoutes()
$openApi = $this->makeOpenApi($config);

$this->getRoutes($config)
->map(function (Route $route) use ($openApi) {
try {
return $this->routeToOperation($openApi, $route);
Expand All @@ -70,7 +59,7 @@ public function __invoke()
->each(fn (Operation $operation) => $openApi->addPath(
Path::make(
(string) Str::of($operation->path)
->replaceFirst(config('scramble.api_path', 'api'), '')
->replaceFirst($config->get('api_path', 'api'), '')
->trim('/')
)->addOperation($operation)
))
Expand All @@ -80,28 +69,28 @@ public function __invoke()

$this->moveSameAlternativeServersToPath($openApi);

if (isset(Scramble::$openApiExtender)) {
(Scramble::$openApiExtender)($openApi);
if ($afterOpenApiGenerated = $config->afterOpenApiGenerated()) {
$afterOpenApiGenerated($openApi);
}

return $openApi->toArray();
}

private function makeOpenApi()
private function makeOpenApi(GeneratorConfig $config)
{
$openApi = OpenApi::make('3.1.0')
->setComponents($this->transformer->getComponents())
->setInfo(
InfoObject::make(config('app.name'))
->setVersion(config('scramble.info.version', '0.0.1'))
->setDescription(config('scramble.info.description', ''))
InfoObject::make($config->get('ui.title', $default = config('app.name')) ?: $default)
->setVersion($config->get('info.version', '0.0.1'))
->setDescription($config->get('info.description', ''))
);

[$defaultProtocol] = explode('://', url('/'));
$servers = config('scramble.servers') ?: [
'' => ($domain = config('scramble.api_domain'))
? $defaultProtocol.'://'.$domain.'/'.config('scramble.api_path', 'api')
: config('scramble.api_path', 'api'),
$servers = $config->get('servers') ?: [
'' => ($domain = $config->get('api_domain'))
? $defaultProtocol.'://'.$domain.'/'.$config->get('api_path', 'api')
: $config->get('api_path', 'api'),
];
foreach ($servers as $description => $url) {
$openApi->addServer(
Expand All @@ -112,7 +101,7 @@ private function makeOpenApi()
return $openApi;
}

private function getRoutes(): Collection
private function getRoutes(GeneratorConfig $config): Collection
{
return collect(RouteFacade::getRoutes())
->pipe(function (Collection $c) {
Expand All @@ -137,16 +126,7 @@ private function getRoutes(): Collection
->filter(function (Route $route) {
return ! ($name = $route->getAction('as')) || ! Str::startsWith($name, 'scramble');
})
->filter(function (Route $route) {
$routeResolver = Scramble::$routeResolver ?? function (Route $route) {
$expectedDomain = config('scramble.api_domain');

return Str::startsWith($route->uri, config('scramble.api_path', 'api'))
&& (! $expectedDomain || $route->getDomain() === $expectedDomain);
};

return $routeResolver($route);
})
->filter($config->routes())
->filter(fn (Route $r) => $r->getAction('controller'))
->values();
}
Expand Down
58 changes: 58 additions & 0 deletions src/GeneratorConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Dedoc\Scramble;

use Closure;
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class GeneratorConfig
{
public function __construct(
private array $config = [],
private ?Closure $routeResolver = null,
private ?Closure $afterOpenApiGenerated = null,
) {
$this->routeResolver = $this->routeResolver ?? function (Route $route) {
$expectedDomain = $this->get('api_domain');

return Str::startsWith($route->uri, $this->get('api_path', 'api'))
&& (! $expectedDomain || $route->getDomain() === $expectedDomain);
};
}

public function config(array $config)
{
$this->config = $config;

return $this;
}

public function routes(?Closure $routeResolver = null)
{
if (count(func_get_args()) === 0) {
return $this->routeResolver;
}

$this->routeResolver = $routeResolver;

return $this;
}

public function afterOpenApiGenerated(?Closure $afterOpenApiGenerated = null)
{
if (count(func_get_args()) === 0) {
return $this->afterOpenApiGenerated;
}

$this->afterOpenApiGenerated = $afterOpenApiGenerated;

return $this;
}

public function get(string $key, mixed $default = null)
{
return Arr::get($this->config, $key, $default);
}
}
Loading

0 comments on commit 9872528

Please sign in to comment.