Skip to content
31 changes: 23 additions & 8 deletions Annotations/AnnotationGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ public function getResponseInfoFromDocBlock(string $docBlock): array
$responseInfo['description'] = 'Response of unknown type';
}
if (!empty($returnTag->getDescription())) {
$responseInfo['description'] = $returnTag->getDescription();
$responseInfo['description'] = $this->getDescriptionText($returnTag->getDescription());
}

return $responseInfo;
Expand Down Expand Up @@ -547,6 +547,22 @@ protected function normaliseDescriptionText(string $description): string
return str_replace('"', '""', $description);
}

/**
* Normalise phpDocumentor description values into plain strings.
*
* @param mixed $description
*
* @return string
*/
protected function getDescriptionText($description): string
{
if ($description instanceof Description) {
return $description->getBodyTemplate();
}

return is_string($description) ? $description : '';
}

/**
* Add an entry to the map of warnings about missing important information, like type and description of parameters
* and returns.
Expand Down Expand Up @@ -1149,10 +1165,7 @@ protected function determineResponses(array $rules, string $plugin, string $meth
$successArray['ref'] = '#/components/responses/GenericSuccess';
}

$description = $responseInfo['description'] ?? null;
if ($description instanceof Description) {
$description = $description->getBodyTemplate();
}
$description = $this->getDescriptionText($responseInfo['description'] ?? null);

// If it's a generic type and there's no custom description, use one of the global generic responses
if (empty($successArray['ref']) && !empty($responseInfo['type']) && empty($description)) {
Expand Down Expand Up @@ -1927,17 +1940,19 @@ public function compileOperationLines(string $path, string $opId, string $plugin
$operationValuesMap[] = ['@OA\Parameter' => $paramMap];
}
foreach ($responses as $response) {
$responseDescription = $this->getDescriptionText($response['description'] ?? null);

// Don't use the reference if there are media type examples
if (isset($response['ref']) && empty($response['mediaTypes'])) {
$code = $response['code'];
$codeFormatted = is_numeric($code) ? (string)$code : '"' . $code . '"';
$description = !empty($response['description']) && strpos($response['description'], 'Example links: [') !== false
? ', description="' . $this->normaliseDescriptionText($response['description']) . '"' : '';
$description = $responseDescription !== '' && strpos($responseDescription, 'Example links: [') !== false
? ', description="' . $this->normaliseDescriptionText($responseDescription) . '"' : '';
$operationValuesMap[] = '@OA\Response(response=' . $codeFormatted . $description . ', ref="' . $response['ref'] . '")';
} else {
$responsePropertyArray = [
'response=200',
'description="' . $this->normaliseDescriptionText($response['description'] ?? 'OK') . '"',
'description="' . $this->normaliseDescriptionText($responseDescription !== '' ? $responseDescription : 'OK') . '"',
];
if (!empty($response['schema'])) {
$responsePropertyArray = array_merge($responsePropertyArray, $response['schema']);
Expand Down
47 changes: 30 additions & 17 deletions Commands/GenerateSpecFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

use Piwik\Container\StaticContainer;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugins\OpenApiDocs\Annotations\AnnotationGenerator;
use Piwik\Plugins\OpenApiDocs\Specs\SpecGenerator;
use Piwik\Plugins\OpenApiDocs\Generation\SpecGenerationService;
use Piwik\Plugins\OpenApiDocs\OpenApiDocs;

/**
* This class lets you define a new command. To read more about commands have a look at our Matomo Console guide on
Expand All @@ -23,6 +23,18 @@
*/
class GenerateSpecFile extends ConsoleCommand
{
/**
* @var SpecGenerationService
*/
private $specGenerationService;

public function __construct(?SpecGenerationService $specGenerationService = null)
{
$this->specGenerationService = $specGenerationService ?: StaticContainer::get(SpecGenerationService::class);

parent::__construct();
}

/**
* This method allows you to configure your command. Here you can define the name and description of your command
* as well as all options and arguments you expect when executing it.
Expand All @@ -31,7 +43,7 @@ protected function configure()
{
$this->setName('openapidocs:generate-spec-file');
$this->setDescription('Generate the OpenAPI documentation file for the Matomo APIs.');
$this->addRequiredValueOption('plugin', 'p', 'Name of the plugin to document, use all to process every plugin');
$this->addRequiredValueOption('plugin', 'p', 'Name of the plugin to document. Multiple plugins can be comma-separated');
$this->addRequiredValueOption('format', 'f', 'Format of the spec file (JSON or YAML). Default is JSON');
$this->addRequiredValueOption('api-version', null, 'Version of the spec file. Default is 1.0.0');
$this->addNoValueOption('not-dry-run', null, 'Flag to allow writing to file instead of outputting a dry run.');
Expand Down Expand Up @@ -78,42 +90,43 @@ protected function doExecute(): int

$plugin = $input->getOption('plugin');


if (empty($plugin)) {
throw new \RuntimeException('Please specify a plugin name.');
}

if (strtolower($plugin) == 'all') {
$plugins = require __DIR__ . '/../config/plugins.php';
$plugin = implode(',', $plugins);
}
$pluginNames = array_values(array_filter(array_map('trim', explode(',', $plugin)), static function (string $pluginName): bool {
return $pluginName !== '';
}));
$format = $input->getOption('format') ?: 'json';
$version = $input->getOption('version') ?: '1.0.0';
$version = $input->getOption('api-version') ?: OpenApiDocs::DEFAULT_SPEC_VERSION;
$notDryRun = $input->getOption('not-dry-run') ?: false;
$addAnnotations = $input->getOption('add-annotations') ?: false;

$message = sprintf('<info>Generating documentation for: %s</info>', $plugin);

$output->writeln($message);

$result = $this->specGenerationService->generateSpecForPlugins(
$plugin,
$format,
$version,
$notDryRun,
$addAnnotations
);

if ($addAnnotations) {
$pluginsArray = explode(',', $plugin);
foreach ($pluginsArray as $pluginName) {
(StaticContainer::get(AnnotationGenerator::class))->generatePluginApiAnnotations($pluginName, true);
foreach ($pluginNames as $pluginName) {
$output->writeln('<info>Created Annotations for ' . $pluginName . ' and wrote results to plugins/OpenApiDocs/tmp/annotations.</info>');
}
}

$result = (new SpecGenerator())->generatePluginDoc($plugin, $format, $version, $notDryRun);

if ($notDryRun) {
$output->writeln('<info>Results written to plugins/OpenApiDocs/tmp/specs/ directory.</info>');

return $result ? self::SUCCESS : self::FAILURE;
return self::SUCCESS;
}

$output->writeln($result);

return $result ? self::SUCCESS : self::FAILURE;
return self::SUCCESS;
}
}
88 changes: 88 additions & 0 deletions Generation/SpecGenerationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

declare(strict_types=1);

namespace Piwik\Plugins\OpenApiDocs\Generation;

use Piwik\Plugins\OpenApiDocs\Annotations\AnnotationGenerator;
use Piwik\Plugins\OpenApiDocs\OpenApiDocs;
use Piwik\Plugins\OpenApiDocs\Specs\SpecGenerator;

class SpecGenerationService
{
/**
* @var AnnotationGenerator
*/
private $annotationGenerator;

/**
* @var SpecGenerator
*/
private $specGenerator;

public function __construct(AnnotationGenerator $annotationGenerator, SpecGenerator $specGenerator)
{
$this->annotationGenerator = $annotationGenerator;
$this->specGenerator = $specGenerator;
}

/**
* Generate an OpenAPI spec for one or more comma-separated plugin names.
*
* @param string $pluginNames Comma-separated plugin names to include in the generated spec.
* @param string $format Output format for the spec, for example `json` or `yaml`.
* @param string $version Version string written into the generated OpenAPI spec.
* @param bool $writeToFile Whether the generated spec should also be written to the plugin tmp specs directory.
* @param bool $addAnnotations Whether API annotations should be regenerated before building the spec.
* @return string The generated OpenAPI spec contents.
* @throws \RuntimeException If no non-empty plugin names are provided.
*/
public function generateSpecForPlugins(
string $pluginNames,
string $format = 'json',
string $version = OpenApiDocs::DEFAULT_SPEC_VERSION,
bool $writeToFile = false,
bool $addAnnotations = false
): string {
$parsedPluginNames = $this->getPluginNames($pluginNames);

if ($addAnnotations) {
$this->generateAnnotations($parsedPluginNames);
}

return $this->specGenerator->generateSpec($parsedPluginNames, $format, $version, $writeToFile);
}

/**
* @param string[] $pluginNames
*/
private function generateAnnotations(array $pluginNames): void
{
foreach ($pluginNames as $pluginName) {
$this->annotationGenerator->generatePluginApiAnnotations($pluginName, true);
}
}

/**
* @return string[]
*/
private function getPluginNames(string $pluginNames): array
{
$plugins = array_filter(array_map('trim', explode(',', $pluginNames)), static function (string $pluginName): bool {
return $pluginName !== '';
});

if (empty($plugins)) {
throw new \RuntimeException('At least one plugin name is required.');
}

return array_values($plugins);
}
}
14 changes: 6 additions & 8 deletions Specs/SpecGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ public function __construct()
*/
public function generatePluginDoc(string $pluginName, string $format = 'json', string $version = OpenApiDocs::DEFAULT_SPEC_VERSION, bool $writeToFile = false): string
{
BaseValidator::check('plugin', $pluginName, [new NotEmpty()]);

foreach (explode(',', $pluginName) as $currentPluginName) {
if (in_array($currentPluginName, OpenApiDocs::PLUGIN_BLOCKLIST, true)) {
throw new \RuntimeException('OpenAPI doc generation is blocked for ' . $currentPluginName . '.');
}
}

return $this->generateSpec(explode(',', $pluginName), $format, $version, $writeToFile);
}

Expand All @@ -74,6 +66,12 @@ public function generateSpec(array $pluginNames, string $format = 'json', string
BaseValidator::check('pluginNames', $pluginNames, [new NotEmpty()]);
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');

foreach ($pluginNames as $currentPluginName) {
if (in_array($currentPluginName, OpenApiDocs::PLUGIN_BLOCKLIST, true)) {
throw new \RuntimeException('OpenAPI doc generation is blocked for ' . $currentPluginName . '.');
}
}

$pluginDirs = [];
foreach ($pluginNames as $pluginName) {
BaseValidator::check('pluginName', $pluginName, [new NotEmpty()]);
Expand Down
72 changes: 72 additions & 0 deletions Tasks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

declare(strict_types=1);

namespace Piwik\Plugins\OpenApiDocs;

use Piwik\Config;
use Piwik\Log\LoggerInterface;
use Piwik\Plugins\OpenApiDocs\Generation\SpecGenerationService;

class Tasks extends \Piwik\Plugin\Tasks
{
/**
* @var SpecGenerationService
*/
private $specGenerationService;

/**
* @var LoggerInterface
*/
private $logger;

public function __construct(SpecGenerationService $specGenerationService, LoggerInterface $logger)
{
$this->specGenerationService = $specGenerationService;
$this->logger = $logger;
}

public function schedule()
{
if ($this->isSpecGenerationEnabled()) {
$this->daily('generateConfiguredPluginSpecs');
}
}

public function generateConfiguredPluginSpecs(): void
{
$pluginNames = require __DIR__ . '/config/plugins.php';

foreach ($pluginNames as $pluginName) {
try {
$this->specGenerationService->generateSpecForPlugins(
$pluginName,
Comment thread
AltamashShaikh marked this conversation as resolved.
'json',
OpenApiDocs::DEFAULT_SPEC_VERSION,
true,
true
);
} catch (\Throwable $e) {
$this->logger->error(
'OpenApiDocs scheduled generation failed for plugin {plugin}: {error}',
[
'plugin' => $pluginName,
'error' => $e->getMessage(),
]
);
}
}
}

private function isSpecGenerationEnabled(): bool
{
return (bool) (Config::getInstance()->OpenApiDocs['enable_spec_generation_task'] ?? 0);
}
}
Loading
Loading