Skip to content

Commit

Permalink
Merge pull request #1842 from zdenekdrahos/html-dump
Browse files Browse the repository at this point in the history
Dump documentation to a static HTML and YAML file
  • Loading branch information
GuilhemN committed Sep 3, 2021
2 parents 67bd715 + 22b1a7e commit b53edda
Show file tree
Hide file tree
Showing 18 changed files with 790 additions and 105 deletions.
57 changes: 38 additions & 19 deletions Command/DumpCommand.php
Expand Up @@ -11,26 +11,31 @@

namespace Nelmio\ApiDocBundle\Command;

use Psr\Container\ContainerInterface;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class DumpCommand extends Command
{
/**
* @var ContainerInterface
* @var RenderOpenApi
*/
private $generatorLocator;
private $renderOpenApi;

/**
* DumpCommand constructor.
* @var mixed[]
*/
public function __construct(ContainerInterface $generatorLocator)
private $defaultHtmlConfig = [
'assets_mode' => AssetsMode::CDN,
'swagger_ui_config' => [],
];

public function __construct(RenderOpenApi $renderOpenApi)
{
$this->generatorLocator = $generatorLocator;
$this->renderOpenApi = $renderOpenApi;

parent::__construct();
}
Expand All @@ -40,34 +45,48 @@ public function __construct(ContainerInterface $generatorLocator)
*/
protected function configure()
{
$availableFormats = $this->renderOpenApi->getAvailableFormats();
$this
->setDescription('Dumps documentation in OpenAPI JSON format')
->setDescription('Dumps documentation in OpenAPI format to: '.implode(', ', $availableFormats))
->addOption('area', '', InputOption::VALUE_OPTIONAL, '', 'default')
->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format output')
->addOption(
'format',
'',
InputOption::VALUE_REQUIRED,
'Output format like: '.implode(', ', $availableFormats),
RenderOpenApi::JSON
)
->addOption('server-url', '', InputOption::VALUE_REQUIRED, 'URL where live api doc is served')
->addOption('html-config', '', InputOption::VALUE_REQUIRED, '', json_encode($this->defaultHtmlConfig))
->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format JSON output')
;
}

/**
* @throws InvalidArgumentException If the area to dump is not valid
*
* @return int|void
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$area = $input->getOption('area');
$format = $input->getOption('format');

if (!$this->generatorLocator->has($area)) {
throw new InvalidArgumentException(sprintf('Area "%s" is not supported.', $area));
$options = [];
if (RenderOpenApi::HTML === $format) {
$rawHtmlConfig = json_decode($input->getOption('html-config'), true);
$options = is_array($rawHtmlConfig) ? $rawHtmlConfig : $this->defaultHtmlConfig;
} elseif (RenderOpenApi::JSON === $format) {
$options = [
'no-pretty' => $input->hasParameterOption(['--no-pretty']),
];
}

$spec = $this->generatorLocator->get($area)->generate();

if ($input->hasParameterOption(['--no-pretty'])) {
$output->writeln(json_encode($spec));
} else {
$output->writeln(json_encode($spec, JSON_PRETTY_PRINT));
if ($input->getOption('server-url')) {
$options['server_url'] = $input->getOption('server-url');
}

$docs = $this->renderOpenApi->render($format, $area, $options);
$output->writeln($docs, OutputInterface::OUTPUT_RAW);

return 0;
}
}
28 changes: 12 additions & 16 deletions Controller/DocumentationController.php
Expand Up @@ -11,35 +11,31 @@

namespace Nelmio\ApiDocBundle\Controller;

use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

final class DocumentationController
{
private $generatorLocator;
/**
* @var RenderOpenApi
*/
private $renderOpenApi;

public function __construct(ContainerInterface $generatorLocator)
public function __construct(RenderOpenApi $renderOpenApi)
{
$this->generatorLocator = $generatorLocator;
$this->renderOpenApi = $renderOpenApi;
}

public function __invoke(Request $request, $area = 'default')
{
if (!$this->generatorLocator->has($area)) {
try {
return JsonResponse::fromJsonString(
$this->renderOpenApi->renderFromRequest($request, RenderOpenApi::JSON, $area)
);
} catch (InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
}

/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();

if ('' !== $request->getBaseUrl()) {
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}

return new JsonResponse($spec);
}
}
53 changes: 20 additions & 33 deletions Controller/SwaggerUiController.php
Expand Up @@ -11,57 +11,44 @@

namespace Nelmio\ApiDocBundle\Controller;

use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Twig\Environment;

final class SwaggerUiController
{
private $generatorLocator;
/**
* @var RenderOpenApi
*/
private $renderOpenApi;

private $twig;

public function __construct(ContainerInterface $generatorLocator, $twig)
public function __construct(RenderOpenApi $renderOpenApi)
{
if (!$twig instanceof \Twig_Environment && !$twig instanceof Environment) {
throw new \InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', get_class($twig)));
}

$this->generatorLocator = $generatorLocator;
$this->twig = $twig;
$this->renderOpenApi = $renderOpenApi;
}

public function __invoke(Request $request, $area = 'default')
{
if (!$this->generatorLocator->has($area)) {
try {
$response = new Response(
$this->renderOpenApi->renderFromRequest($request, RenderOpenApi::HTML, $area, [
'assets_mode' => AssetsMode::BUNDLE,
]),
Response::HTTP_OK,
['Content-Type' => 'text/html']
);

return $response->setCharset('UTF-8');
} catch (InvalidArgumentException $e) {
$advice = '';
if (false !== strpos($area, '.json')) {
$advice = ' Since the area provided contains `.json`, the issue is likely caused by route priorities. Try switching the Swagger UI / the json documentation routes order.';
}

throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice));
}

/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();

if ('' !== $request->getBaseUrl()) {
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}

return new Response(
$this->twig->render(
'@NelmioApiDoc/SwaggerUi/index.html.twig',
['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]]
),
Response::HTTP_OK,
['Content-Type' => 'text/html']
);

return $response->setCharset('UTF-8');
}
}
35 changes: 17 additions & 18 deletions Controller/YamlDocumentationController.php
Expand Up @@ -11,37 +11,36 @@

namespace Nelmio\ApiDocBundle\Controller;

use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use Psr\Container\ContainerInterface;
use InvalidArgumentException;
use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

final class YamlDocumentationController
{
private $generatorLocator;
/**
* @var RenderOpenApi
*/
private $renderOpenApi;

public function __construct(ContainerInterface $generatorLocator)
public function __construct(RenderOpenApi $renderOpenApi)
{
$this->generatorLocator = $generatorLocator;
$this->renderOpenApi = $renderOpenApi;
}

public function __invoke(Request $request, $area = 'default')
{
if (!$this->generatorLocator->has($area)) {
try {
$response = new Response(
$this->renderOpenApi->renderFromRequest($request, RenderOpenApi::YAML, $area),
Response::HTTP_OK,
['Content-Type' => 'text/x-yaml']
);

return $response->setCharset('UTF-8');
} catch (InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
}

/** @var OpenApi $spec */
$spec = $this->generatorLocator->get($area)->generate();

if ('' !== $request->getBaseUrl()) {
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
}

return new Response($spec->toYaml(), 200, [
'Content-Type' => 'text/x-yaml',
]);
}
}
19 changes: 19 additions & 0 deletions Render/Html/AssetsMode.php
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\Render\Html;

class AssetsMode
{
public const BUNDLE = 'bundle';
public const CDN = 'cdn';
public const OFFLINE = 'offline';
}
96 changes: 96 additions & 0 deletions Render/Html/GetNelmioAsset.php
@@ -0,0 +1,96 @@
<?php

/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nelmio\ApiDocBundle\Render\Html;

use Symfony\Bridge\Twig\Extension\AssetExtension;
use Twig\TwigFunction;

/**
* @internal
*/
class GetNelmioAsset
{
private $assetExtension;
private $resourcesDir;
private $cdnUrl;
private $assetsMode = AssetsMode::BUNDLE;

public function __construct(AssetExtension $assetExtension)
{
$this->assetExtension = $assetExtension;
$this->cdnUrl = 'https://cdn.jsdelivr.net/gh/nelmio/NelmioApiDocBundle/Resources/public';
$this->resourcesDir = __DIR__.'/../../Resources/public';
}

public function toTwigFunction($assetsMode): TwigFunction
{
$this->assetsMode = $assetsMode;

return new TwigFunction('nelmioAsset', $this, ['is_safe' => ['html']]);
}

public function __invoke($asset)
{
[$extension, $mode] = $this->getExtension($asset);
[$resource, $isInline] = $this->getResource($asset, $mode);
if ('js' == $extension) {
return $this->renderJavascript($resource, $isInline);
} elseif ('css' == $extension) {
return $this->renderCss($resource, $isInline);
} else {
return $resource;
}
}

private function getExtension($asset)
{
$extension = mb_substr($asset, -3, 3, 'utf-8');
if ('.js' === $extension) {
return ['js', $this->assetsMode];
} elseif ('png' === $extension) {
return ['png', AssetsMode::OFFLINE == $this->assetsMode ? AssetsMode::CDN : $this->assetsMode];
} else {
return ['css', $this->assetsMode];
}
}

private function getResource($asset, $mode)
{
if (filter_var($asset, FILTER_VALIDATE_URL)) {
return [$asset, false];
} elseif (AssetsMode::OFFLINE === $mode) {
return [file_get_contents($this->resourcesDir.'/'.$asset), true];
} elseif (AssetsMode::CDN === $mode) {
return [$this->cdnUrl.'/'.$asset, false];
} else {
return [$this->assetExtension->getAssetUrl(sprintf('bundles/nelmioapidoc/%s', $asset)), false];
}
}

private function renderJavascript(string $script, bool $isInline)
{
if ($isInline) {
return sprintf('<script>%s</script>', $script);
} else {
return sprintf('<script src="%s"></script>', $script);
}
}

private function renderCss(string $stylesheet, bool $isInline)
{
if ($isInline) {
return sprintf('<style>%s</style>', $stylesheet);
} else {
return sprintf('<link rel="stylesheet" href="%s">', $stylesheet);
}
}
}

0 comments on commit b53edda

Please sign in to comment.