<?php
namespace Knuckles\Scribe\Writing;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Knuckles\Camel\Camel;
use Knuckles\Camel\Extraction\Response;
use Knuckles\Camel\Output\OutputEndpointData;
use Knuckles\Camel\Output\Parameter;
use Knuckles\Scribe\Extracting\ParamHelpers;
use Knuckles\Scribe\Tools\DocumentationConfig;
use Knuckles\Scribe\Tools\Utils;
use Knuckles\Scribe\Writing\OpenApiSpecGenerators\Base31Generator;
use Knuckles\Scribe\Writing\OpenApiSpecGenerators\BaseGenerator;
use Knuckles\Scribe\Writing\OpenApiSpecGenerators\OpenApiGenerator;
use Knuckles\Scribe\Writing\OpenApiSpecGenerators\OverridesGenerator;
use Knuckles\Scribe\Writing\OpenApiSpecGenerators\SecurityGenerator;
use function array_map;
class OpenAPISpecWriter
{
use ParamHelpers;
const SPEC_VERSION = '3.0.3';
private DocumentationConfig $config;
/**
* @var Collection<int, OpenApiGenerator>
*/
private Collection $generators;
public function __construct(?DocumentationConfig $config = null)
{
$this->config = $config ?: new DocumentationConfig(config('scribe', []));
$generators = [
$this->isOpenApi31OrLater() ? Base31Generator::class : BaseGenerator::class,
SecurityGenerator::class,
OverridesGenerator::class,
];
$this->generators = collect($generators)
->merge($this->config->get('openapi.generators',[]))
->map(fn($generatorClass) => app()->makeWith($generatorClass, ['config' => $this->config]));
}
/**
* Get the OpenAPI spec version to use from config, defaulting to 3.0.3.
* Supported versions: '3.0.3', '3.1.0'
*
* @return string The OpenAPI version
*/
public function getSpecVersion(): string
{
return $this->config->get('openapi.version', self::SPEC_VERSION);
}
/**
* See https://swagger.io/specification/
*
* @param array<int, array{description: string, name: string, endpoints: OutputEndpointData[]}> $groupedEndpoints
*
* @return array
*/
public function generateSpecContent(array $groupedEndpoints): array
{
$paths = ['paths' => $this->generatePathsSpec($groupedEndpoints)];
$content = [];
foreach ($this->generators as $generator) {
$content = $generator->root($content, $groupedEndpoints);
}
return array_replace_recursive($content, $paths);
}
/**
* @param array<int, array{description: string, name: string, endpoints: OutputEndpointData[]}> $groupedEndpoints
*
* @return array
*/
protected function generatePathsSpec(array $groupedEndpoints): array
{
$allEndpoints = collect($groupedEndpoints)->map->endpoints->flatten(1);
// OpenAPI groups endpoints by path, then method
$groupedByPath = $allEndpoints->groupBy(function ($endpoint) {
$path = str_replace("?}", "}", $endpoint->uri); // Remove optional parameters indicator in path
return '/' . ltrim($path, '/');
});
return $groupedByPath->mapWithKeys(function (Collection $endpoints, $path) use ($groupedEndpoints) {
$operations = $endpoints->mapWithKeys(function (OutputEndpointData $endpoint) use ($groupedEndpoints) {
$spec = [];
foreach ($this->generators as $generator) {
$spec = $generator->pathItem($spec, $groupedEndpoints, $endpoint);
}
return [strtolower($endpoint->httpMethods[0]) => $spec];
});
$pathItem = $operations;
// Placing all URL parameters at the path level, since it's the same path anyway
/** @var OutputEndpointData $urlParameterEndpoint */
$urlParameterEndpoint = $endpoints[0];
$parameters = [];
foreach ($this->generators as $generator) {
$parameters = $generator->pathParameters($parameters, $endpoints->all(), $urlParameterEndpoint->urlParameters);
}
if (!empty($parameters)) {
$pathItem['parameters'] = array_values($parameters);
}
return [$path => $pathItem];
})->toArray();
}
protected function isOpenApi31OrLater(): bool
{
$version = $this->config->get('openapi.version', OpenAPISpecWriter::SPEC_VERSION);
return version_compare($version, '3.1.0', '>=');
}
}