diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php index 0bc6917..029183f 100644 --- a/Annotations/AnnotationGenerator.php +++ b/Annotations/AnnotationGenerator.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\OpenApiDocs\Annotations; +use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Description; use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Tags\Param; use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlock\Tags\TagWithType; use Matomo\Dependencies\OpenApiDocs\phpDocumentor\Reflection\DocBlockFactory; @@ -121,15 +122,15 @@ public function generatePluginApiAnnotations(string $pluginName, bool $writeToFi $pluginMetadata = Proxy::getInstance()->getMetadata()[$className] ?? []; $annotations = [[sprintf('@OA\Tag(name="%s")', $pluginName)]]; - // I decided to not include the description in the tag annotation so that it automatically pulls the API class comment as the description. -// if (!empty($pluginMetadata['__documentation'])) { -// $tagLines = $this->buildLinesForAnnotationObject('@OA\Tag', [ -// sprintf('name="%s"', $pluginName), -// sprintf('description="%s"', $this->normaliseDescriptionText($pluginMetadata['__documentation'])), -// ]); -// $this->removeTrailingCommaFromLastLine($tagLines); -// $annotations[] = $tagLines; -// } + + if (!empty($pluginMetadata['__documentation'])) { + $tagLines = $this->buildLinesForAnnotationObject('@OA\Tag', [ + sprintf('name="%s"', $pluginName), + sprintf('description="%s"', $this->normaliseDescriptionText($pluginMetadata['__documentation'])), + ]); + $this->removeTrailingCommaFromLastLine($tagLines); + $annotations[] = $tagLines; + } foreach (array_keys($pluginMetadata) as $metadataMethod) { if (!$reflectionClass->hasMethod($metadataMethod)) { @@ -254,11 +255,12 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R $params = $this->determineParameters($rules, $pluginName, $methodName, $reflectionMethod); $responses = $this->determineResponses($rules, $pluginName, $methodName, $reflectionMethod, $params); + $description = $this->determineDescription($pluginName, $methodName, $reflectionMethod); $isPost = !empty($rules['plugins'][$pluginName]['methodsRequiringPost']) && in_array($methodName, $rules['plugins'][$pluginName]['methodsRequiringPost']); - return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $isPost); + return $this->compileOperationLines($path, $opId, $pluginName, $params, $responses, $description, $isPost); } /** @@ -354,6 +356,24 @@ public function getResponseInfoFromDocBlock(string $docBlock): array } /** + * Extract the description/summary from a given docblock + * + * @param string $docBlock The comment block from a method, which hopefully contains a description. + * + * @return string Description/summary extracted from the docblock + */ + public function getDescriptionFromDocBlock(string $docBlock): string + { + $factory = DocBlockFactory::createInstance(); + $docBlockObject = $factory->create($docBlock); + + return $docBlockObject->getSummary(); + } + + + + + /** * This is a helper method for building the path used for an operation annotation. It takes a path template, like * the one from the config array and populates it with the plugin name and API method name. * @@ -449,12 +469,17 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa // Clean up the descriptions a little more like removing linebreaks and escaping double-quotes $description = $this->normaliseDescriptionText($description); + $default = $paramMetadata['default'] ?? null; + if (!is_string($default)) { + $default = json_encode($default); + } + return [ 'name' => $paramName, 'types' => $typesMap, 'description' => $description, 'required' => $isRequired ? 'true' : 'false', - 'default' => !$isRequired ? json_encode($paramMetadata['default']) : NoDefaultValue::class, + 'default' => !$isRequired ? $default : NoDefaultValue::class, 'example' => $example, ]; } @@ -578,6 +603,30 @@ protected function determineParameters(array $rules, string $plugin, string $met ]; } + + + + /** + * Get the description/summary of a given method + * + * @param string $plugin Name of the plugin. E.g. TagManager. + * @param string $method The name of the method being annotated. + * @param \ReflectionMethod $reflectionMethod The reflective representation of the method to provide metadata. + * + * @return string Description of the method + */ + protected function determineDescription(string $plugin, string $method, \ReflectionMethod $reflectionMethod): string + { + $description = ''; + $docBlock = $reflectionMethod->getDocComment(); + if (!empty($docBlock)) { + $description = $this->getDescriptionFromDocBlock($docBlock); + } + + return $description; + } + + /** * Map the PHP type to the OpenAPI type. The currently available types for v3.1.1 are the following: “null”, * “boolean”, “object”, “array”, “number”, “string”, or “integer”. @@ -614,6 +663,9 @@ public function getOpenApiTypeFromPhpType(string $type): string case 'double': $type = 'number'; break; + case 'void': + $type = 'null'; + break; default: $type = 'string'; } @@ -1037,8 +1089,13 @@ 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(); + } + // 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($responseInfo['description'])) { + if (empty($successArray['ref']) && !empty($responseInfo['type']) && empty($description)) { $ref = ''; switch ($responseInfo['type']) { case 'array': @@ -1053,6 +1110,8 @@ protected function determineResponses(array $rules, string $plugin, string $meth case 'string': $ref = '#/components/responses/GenericString'; break; + case 'null': + $ref = '#/components/responses/GenericSuccess'; } if (!empty($ref)) { @@ -1713,16 +1772,18 @@ public function buildSchemaObjectArrays(array $typesMap, string $default = '', s * @param string $plugin The name of the plugin. E.g. CustomReports * @param array $params The compiled list of method parameters and key information about them, like type. * @param array $responses compiled list of method expected responses and key information about them, like type. + * @param string $description The method level description * @param bool $isPost Indicates whether the operation is a POST. The default is false, meaning it's GET. * * @return string[] The array of all the lines of the operation annotation object. */ - public function compileOperationLines(string $path, string $opId, string $plugin, array $params, array $responses, bool $isPost = false): array + public function compileOperationLines(string $path, string $opId, string $plugin, array $params, array $responses, string $description, bool $isPost = false): array { $operationValuesMap = [ 'path="' . $path . '"', 'operationId="' . $opId . '"', 'tags={"' . $plugin . '"}', + 'description="' . $this->normaliseDescriptionText($description) . '"', ]; foreach ($params['refs'] ?? [] as $ref) { $operationValuesMap[] = '@OA\Parameter(ref="' . $ref . '")'; diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf906b..beaea2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## Changelog + +5.0.1-b1 +- 16-02-2026 - Initial implementation of plugin and POC generating documentation from annotations + 5.0.0-b1 - Initial implementation of plugin and POC generating documentation from annotations diff --git a/Specs/SpecGenerator.php b/Specs/SpecGenerator.php index 3ba2544..e7a2b78 100644 --- a/Specs/SpecGenerator.php +++ b/Specs/SpecGenerator.php @@ -72,8 +72,7 @@ public function generateSpec(array $pluginNames, string $format = 'json', string BaseValidator::check('pluginName', $pluginName, [new NotEmpty()]); Manager::getInstance()->checkIsPluginActivated($pluginName); - $pluginDir = Manager::getInstance()::getPluginDirectory($pluginName); - $pluginAnnotationsSource = $pluginDir . '/API.php'; + $pluginAnnotationsSource = $currentPluginDir . '/tmp/annotations/' . $pluginName . 'GeneratedAnnotations.php'; try { $openapi = (new Generator(StaticContainer::get(NullLogger::class)))->generate([ $pluginAnnotationsSource, diff --git a/plugin.json b/plugin.json index 81eb46f..71c1c66 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "OpenApiDocs", "description": "Generate API documentation for Matomo in the OpenAPI format.", - "version": "5.0.0-b1", + "version": "5.0.1-b1", "theme": false, "keywords": [ "API", diff --git a/tests/Unit/AnnotationGeneratorTest.php b/tests/Unit/AnnotationGeneratorTest.php index 75aaa46..5f34842 100644 --- a/tests/Unit/AnnotationGeneratorTest.php +++ b/tests/Unit/AnnotationGeneratorTest.php @@ -435,7 +435,7 @@ public function getTestDataForBuildParameterAnnotationData(): iterable 'types' => ['string' => null], 'description' => '', 'required' => 'false', - 'default' => '"SomeDefaultValue"', + 'default' => 'SomeDefaultValue', 'example' => '', ]]; yield 'should not wrap metadata default value when boolean type' => ['someParam', [ @@ -467,7 +467,7 @@ public function getTestDataForBuildParameterAnnotationData(): iterable 'types' => ['string' => null], 'description' => '', 'required' => 'false', - 'default' => '""', + 'default' => '', 'example' => '', ]]; yield 'should not count the NoDefaultValue class as a default value' => ['someParam', [