diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php
index da5ec93..8d0b400 100644
--- a/Annotations/AnnotationGenerator.php
+++ b/Annotations/AnnotationGenerator.php
@@ -138,7 +138,7 @@ protected function buildAnnotationForMethod(array $rules, string $pluginName, \R
);
$params = $this->determineParameters($rules, $pluginName, $methodName, $reflectionMethod);
- $responses = $this->determineResponses($rules, $pluginName, $methodName);
+ $responses = $this->determineResponses($rules, $pluginName, $methodName, $reflectionMethod);
$isPost = !empty($rules['plugins'][$pluginName]['methodsRequiringPost'])
&& in_array($methodName, $rules['plugins'][$pluginName]['methodsRequiringPost']);
@@ -159,7 +159,8 @@ protected function getParamInfoFromDocBlock(string $docBlock): array
$name = ltrim($param->parameterName, '$');
$params[$name] = [
'type' => (string) $param->type,
- 'desc' => $param->description,
+ // Normalise the description. E.g. remove linebreaks and indentation
+ 'desc' => trim(preg_replace(['/^\h+/m', '/\R+/u',], ['', ' '], $param->description)),
'byRef' => $param->isReference,
'variadic' => $param->isVariadic,
];
@@ -167,9 +168,37 @@ protected function getParamInfoFromDocBlock(string $docBlock): array
return $params;
}
+ protected function getResponseInfoFromDocBlock(string $docBlock): array
+ {
+ $lexer = new Lexer();
+ $tokens = $lexer->tokenize($docBlock);
+ $expressionParser = new ConstExprParser();
+ $parser = new PhpDocParser(new TypeParser($expressionParser), $expressionParser);
+ $node = $parser->parse(new TokenIterator($tokens));
+
+ $responseInfo = ['type' => null];
+ $returnTags = $node->getReturnTagValues();
+ if (empty($returnTags)) {
+ return $responseInfo;
+ }
+
+ $returnTag = $returnTags[0];
+ $tagValue = strval($returnTag->type);
+ $responseInfo['type'] = $this->getOpenApiTypeFromPhpType($tagValue);
+ if ($responseInfo['type'] === 'string' && !empty($tagValue) && strtolower($tagValue) !== 'string') {
+ $responseInfo['type'] = '';
+ $responseInfo['description'] = 'Response of unknown type';
+ }
+ if (!empty($returnTag->description)) {
+ $responseInfo['description'] = $returnTag->description;
+ }
+
+ return $responseInfo;
+ }
+
protected function buildVirtualPath(string $virtualPathTemplate, string $plugin, string $method): string
{
- return str_replace([ '{plugin}', '{method}' ], [ $plugin, $method ], $virtualPathTemplate);
+ return str_replace(['{plugin}', '{method}'], [$plugin, $method], $virtualPathTemplate);
}
protected function buildParameterAnnotation(string $paramName, array $paramMetadata, array $paramDocInfo): array
@@ -177,6 +206,10 @@ protected function buildParameterAnnotation(string $paramName, array $paramMetad
$docType = strtolower(trim($paramDocInfo['type'] ?? ''));
$metaType = strtolower(trim($paramMetadata['type'] ?? $docType));
$type = $metaType === 'string' && $docType !== 'string' ? $docType : $metaType;
+ // If the signature type is array, but the type hinting provides more, use that instead
+ if ($type === 'array' && strpos($docType, '[]') !== false && strpos($docType, '|') === false) {
+ $type = $docType;
+ }
$typesMap = [];
// Check for pipes and try to list possible types
foreach (explode('|', $type) as $typePart) {
@@ -197,7 +230,7 @@ protected function buildParameterAnnotation(string $paramName, array $paramMetad
'types' => $typesMap,
'description' => $paramDocInfo['desc'] ?? '',
'required' => $isRequired ? 'true' : 'false',
- 'default' => !$isRequired ? json_encode($paramMetadata['default']) : '',
+ 'default' => !$isRequired ? json_encode($paramMetadata['default']) : NoDefaultValue::class,
];
}
@@ -300,8 +333,8 @@ protected function getApplicableDemoExampleUrls(string $pluginName, string $meth
protected function getExampleIfAvailable(string $url): array
{
- // Simply return the URL for TSV
- if (stripos($url, 'format=tsv') !== false) {
+ // Simply return the URL for anything other than JSON until we figure out how to better format those examples
+ if (stripos($url, 'format=json') === false) {
return ['externalValue' => $url];
}
@@ -319,16 +352,10 @@ protected function getExampleIfAvailable(string $url): array
curl_close($ch);
// If the example didn't load or is too big, simply include the URL instead of the string value
- if ($body === false || $status !== 200 || strlen($body) > 1000 || strpos($body, 'Error: ') === 0) {
+ if ($body === false || $status !== 200 || strlen($body) > 2000 || strpos($body, 'Error: ') === 0) {
return ['externalValue' => $url];
}
- // Clean up XML formatting a bit
- $body = trim($body);
- if (stripos($url, 'format=xml') !== false) {
- $body = str_replace(['', "\n", "\t", '"'], ['', '', '', '\"'], $body);
- }
-
// The annotation expects an objects and not arrays
if (stripos($url, 'format=json') !== false && stripos($body, '[') === 0) {
$body = str_replace(['[', ']'], ['{', '}'], $body);
@@ -337,21 +364,61 @@ protected function getExampleIfAvailable(string $url): array
return ['value' => $body];
}
- protected function determineResponses(array $rules, string $plugin, string $method): array
+ protected function determineResponses(array $rules, string $plugin, string $method, \ReflectionMethod $reflectionMethod): array
{
$responses = [];
- // TODO - Try to determine the success response using the return type and/or doc-block return type
+ // Try to determine the success response using the return type and/or doc-block return type
+ $returnType = $reflectionMethod->getReturnType();
+ $responseInfo = $this->getResponseInfoFromDocBlock($reflectionMethod->getDocComment());
+ if (!empty($returnType) && $returnType->isBuiltin()) {
+ $responseInfo['type'] = $this->getOpenApiTypeFromPhpType(strval($returnType));
+ }
$successRef = null;
$successArray = ['code' => 200];
if (isset($rules['plugins'][$plugin]['successResponseByMethod'][$method])) {
$successRef = $rules['plugins'][$plugin]['successResponseByMethod'][$method];
}
+ // TODO - See if there's a way to auto-handle custom objects, especially common stuff like DataTable\DataTableInterface
if ($successRef) {
$successArray['ref'] = $successRef;
}
+ // If the return type is void, use the generic response type
+ if (empty($successArray['ref']) && !empty($returnType) && strval($returnType) === 'void') {
+ $successArray['ref'] = '#/components/responses/GenericSuccessNoBody';
+ }
+
+ // 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'])) {
+ $ref = '';
+ switch ($responseInfo['type']) {
+ case 'array':
+ $ref = '#/components/responses/GenericArray';
+ break;
+ case 'integer':
+ $ref = '#/components/responses/GenericInteger';
+ break;
+ case 'boolean':
+ $ref = '#/components/responses/GenericBoolean';
+ break;
+ case 'string':
+ $ref = '#/components/responses/GenericString';
+ break;
+ }
+
+ if (!empty($ref)) {
+ $successArray['ref'] = $ref;
+ }
+ }
+
+ if (!empty($responseInfo['description'])) {
+ $successArray['desc'] = $responseInfo['description'];
+ }
+
+ $responseSchema = !empty($responseInfo['type']) ? $this->buildSchemaObjectArray($responseInfo['type']) : [];
+
$mediaTypes = [];
// This simply reuses the example URLs used by the current documentation, but some endpoints don't work because authentication is required
// TODO - Come up with a way to demo examples for endpoints which require authentication. E.g. hit a live endpoint server-side and replace any potentially sensitive data...
@@ -370,13 +437,21 @@ protected function determineResponses(array $rules, string $plugin, string $meth
$value = substr($value, 1, -1);
}
$exampleProperties[] = $valueKey . '=' . $value;
- $mediaTypes[] = [
+ $mediaType = [
'mediaType="' . $contentType . '"',
'@OA\Examples' => $exampleProperties,
];
+ // If a type was found, add it as a schema to the media type
+ if (!empty($responseSchema)) {
+ $mediaType = array_merge($mediaType, $responseSchema);
+ }
+ $mediaTypes[] = $mediaType;
}
if (!empty($mediaTypes)) {
$successArray['mediaTypes'] = $mediaTypes;
+ } else {
+ // Make sure the schema is included in there are no examples
+ $successArray['schema'] = $responseSchema;
}
$responses[] = $successArray;
@@ -434,7 +509,7 @@ protected function buildLinesForAnnotationObject(string $objectName, array $obje
return array_merge([$indentString . $objectName . $openingCharacter], $lines, [$indentString . $closingCharacter . ',']);
}
- protected function buildSchemaObjectArray(string $type, string $subType = '', string $default = ''): array
+ protected function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class): array
{
$schemaMap = ['type="' . $type . '"'];
$subTypeString = '';
@@ -448,14 +523,32 @@ protected function buildSchemaObjectArray(string $type, string $subType = '', st
}
}
- if ($default !== '') {
- // TODO - Add some logic to only add default if it matches the type. E.g. false isn't a good default for string
- $schemaMap[] = 'default="' . $default . '"';
+ if ($this->shouldIncludeDefault($type, $default)) {
+ $doubleQuote = '"';
+ // Don't wrap with quotes for certain values
+ if (in_array($default, ['{}', 'false', 'true', "{$doubleQuote}{$doubleQuote}"])) {
+ $doubleQuote = '';
+ }
+ $schemaMap[] = "default={$doubleQuote}{$default}{$doubleQuote}";
}
return ['@OA\Schema' => $schemaMap];
}
+ protected function shouldIncludeDefault(string $type, string $default = NoDefaultValue::class): bool
+ {
+ if ($default === NoDefaultValue::class) {
+ return false;
+ }
+
+ // Don't use true or false for default if it's not a boolean type
+ if ($type !== 'boolean' && in_array(strtolower($default), ['false', 'true'])) {
+ return false;
+ }
+
+ return true;
+ }
+
protected function buildSchemaObjectArrays(array $typesMap, string $default = ''): array
{
$schemas = [];
@@ -493,7 +586,8 @@ protected function compileOperationLines(string $path, string $opId, string $plu
$operationValuesMap[] = ['@OA\Parameter' => $paramMap];
}
foreach ($responses as $response) {
- if (isset($response['ref'])) {
+ // 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 . '"';
$operationValuesMap[] = '@OA\Response(response=' . $codeFormatted . ', ref="' . $response['ref'] . '")';
@@ -502,6 +596,9 @@ protected function compileOperationLines(string $path, string $opId, string $plu
'response=200',
'description="' . ($response['desc'] ?? 'OK') . '"',
];
+ if (!empty($response['schema'])) {
+ $responsePropertyArray = array_merge($responsePropertyArray, $response['schema']);
+ }
if (isset($response['mediaTypes']) && is_array($response['mediaTypes'])) {
foreach ($response['mediaTypes'] as $mediaType) {
$responsePropertyArray[] = ['@OA\MediaType' => $mediaType];
@@ -510,7 +607,8 @@ protected function compileOperationLines(string $path, string $opId, string $plu
$operationValuesMap[] = ['@OA\Response' => $responsePropertyArray];
}
}
- $operationValuesMap[] = 'x={"runtime"={"entry":"index.php","query":{"module":"API","method":"' . $plugin . '.' . $method . '"}}}';
+ // TODO - Remove this if it's determined that we won't ever use it
+ //$operationValuesMap[] = 'x={"runtime"={"entry":"index.php","query":{"module":"API","method":"' . $plugin . '.' . $method . '"}}}';
$lines = $this->buildLinesForAnnotationObject('@OA\\' . ($isPost ? 'Post' : 'Get'), $operationValuesMap);
diff --git a/Annotations/GlobalApiComponents.php b/Annotations/GlobalApiComponents.php
index af76b57..2e2f30b 100644
--- a/Annotations/GlobalApiComponents.php
+++ b/Annotations/GlobalApiComponents.php
@@ -11,7 +11,9 @@
/**
* Global components for generating OpenAPI specs for the Matomo Reporting API.
- *
+ */
+
+/**
* @OA\OpenApi(
* openapi="3.1.0",
* security={{"MatomoToken": {}}},
@@ -51,14 +53,46 @@
*
* Generic Error object
* @OA\Schema(
+ * schema="GenericSuccess",
+ * type="object",
+ * description="Generic Matomo success payload.",
+ * required={"result","message"},
+ * additionalProperties=true,
+ * @OA\Property(property="result", type="string", enum={"success"}, example="success"),
+ * @OA\Property(property="message", type="string", example="ok"),
+ * @OA\Property(property="code", type="integer", example="200")
+ * )
+ *
+ * Generic Error object
+ * @OA\Schema(
* schema="Error",
* type="object",
* description="Generic Matomo error payload.",
* required={"result","message"},
* additionalProperties=true,
* @OA\Property(property="result", type="string", enum={"error"}, example="error"),
- * @OA\Property(property="message", type="string", example="You can't access this resource"),
- * @OA\Property(property="code", type="integer", nullable=true, example=401)
+ * @OA\Property(property="message", type="string", example="There was an error"),
+ * @OA\Property(property="code", type="integer")
+ * )
+ *
+ * @OA\Schema(
+ * schema="ErrorXml",
+ * type="object",
+ * description="Generic Matomo error payload in XML.",
+ * @OA\Xml(
+ * name="result"
+ * ),
+ * @OA\Property(
+ * property="error",
+ * type="object",
+ * @OA\Xml(name="error"),
+ * @OA\Property(
+ * property="message",
+ * type="string",
+ * xml=@OA\Xml(attribute=true),
+ * example="There was an error"
+ * )
+ * )
* )
*
* Common responses which should be used by each API endpoint
@@ -66,62 +100,81 @@
* response="BadRequest",
* description="Bad request (validation or missing parameters).",
* @OA\JsonContent(ref="#/components/schemas/Error"),
- * @OA\XmlContent(ref="#/components/schemas/Error"),
- * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string")),
- * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"))
+ * @OA\XmlContent(ref="#/components/schemas/ErrorXml"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Error: There was an error."),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="There was an error.")
* )
*
* @OA\Response(
* response="Unauthorized",
* description="Authentication failed or missing token.",
* @OA\JsonContent(ref="#/components/schemas/Error"),
- * @OA\XmlContent(ref="#/components/schemas/Error"),
- * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string")),
- * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"))
+ * @OA\XmlContent(ref="#/components/schemas/ErrorXml"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Error: You must be logged in to access this functionality."),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="You must be logged in to access this functionality.")
* )
*
* @OA\Response(
* response="Forbidden",
* description="Authenticated but not allowed to access the resource.",
* @OA\JsonContent(ref="#/components/schemas/Error"),
- * @OA\XmlContent(ref="#/components/schemas/Error"),
- * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string")),
- * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"))
+ * @OA\XmlContent(ref="#/components/schemas/ErrorXml"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Error: Not authorised."),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="Not authorised.")
* )
*
* @OA\Response(
* response="NotFound",
* description="Resource not found.",
* @OA\JsonContent(ref="#/components/schemas/Error"),
- * @OA\XmlContent(ref="#/components/schemas/Error"),
- * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string")),
- * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"))
+ * @OA\XmlContent(ref="#/components/schemas/ErrorXml"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Error: The method is not available."),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="The method is not available.")
* )
*
* @OA\Response(
* response="ServerError",
* description="Unexpected server error.",
* @OA\JsonContent(ref="#/components/schemas/Error"),
- * @OA\XmlContent(ref="#/components/schemas/Error"),
- * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string")),
- * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"))
+ * @OA\XmlContent(ref="#/components/schemas/ErrorXml"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Error: There was an error."),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="There was an error.")
* )
*
* @OA\Response(
* response="DefaultError",
* description="Default error response (any non-2xx).",
* @OA\JsonContent(ref="#/components/schemas/Error"),
- * @OA\XmlContent(ref="#/components/schemas/Error"),
- * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string")),
- * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"))
+ * @OA\XmlContent(ref="#/components/schemas/ErrorXml"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Error: There was an error."),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="There was an error.")
* )
*
* Generic responses which can be used by endpoints
* @OA\Response(
- * response="GenericSuccess",
+ * response="GenericSuccessNoBody",
* description="Generic 200 response with no body"
* )
*
+ * Generic responses which can be used by endpoints
+ * @OA\Response(
+ * response="GenericSuccess",
+ * description="Generic 200 response",
+ * @OA\JsonContent(ref="#/components/schemas/GenericSuccess"),
+ * @OA\XmlContent(ref="#/components/schemas/GenericSuccess"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Result: success"),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="success")
+ * )
+ *
+ * @OA\Response(
+ * response="GenericString",
+ * description="Generic 200 response with only a string body",
+ * @OA\JsonContent(type="string"),
+ * @OA\XmlContent(type="string"),
+ * @OA\MediaType(mediaType="text/plain", @OA\Schema(type="string"), example="Result: success"),
+ * @OA\MediaType(mediaType="text/html", @OA\Schema(type="string"), example="success")
+ * )
+ *
* @OA\Response(
* response="GenericBoolean",
* description="Generic 200 response with only true or false as the body",
@@ -157,7 +210,7 @@
* in="query",
* description="Always `API` for Reporting API requests.",
* required=true,
- * @OA\Schema(type="string", default="API", example="API")
+ * @OA\Schema(type="string", default="API")
* )
*
* @OA\Parameter(
@@ -166,8 +219,7 @@
* in="query",
* description="API method, e.g. `VisitsSummary.get` or `CustomAlerts.getAlert`.",
* required=true,
- * @OA\Schema(type="string"),
- * example="CustomAlerts.getAlert"
+ * @OA\Schema(type="string", example="CustomAlerts.getAlert")
* )
*
* @OA\Parameter(
@@ -180,8 +232,7 @@
* type="string",
* enum={"xml","json","csv","tsv","html","rss","original"},
* default="xml"
- * ),
- * example="xml"
+ * )
* )
*
* @OA\Parameter(
@@ -194,8 +245,7 @@
* type="string",
* enum={"xml","json","csv","tsv","html","rss","original"},
* default="xml"
- * ),
- * example="xml"
+ * )
* )
*
* Commonly used parameters. If there are parameters not required by every endpoint, it will be declared as both
@@ -206,8 +256,7 @@
* in="query",
* description="Matomo site ID.",
* required=true,
- * @OA\Schema(type="integer"),
- * example=1
+ * @OA\Schema(type="integer", example=1)
* )
*
* @OA\Parameter(
@@ -216,8 +265,7 @@
* in="query",
* description="Matomo site ID.",
* required=false,
- * @OA\Schema(type="integer"),
- * example=1
+ * @OA\Schema(type="integer", example=1)
* )
*
* @OA\Parameter(
@@ -226,8 +274,7 @@
* in="query",
* description="Reporting period.",
* required=true,
- * @OA\Schema(type="string", enum={"day","week","month","year","range"}),
- * example="day"
+ * @OA\Schema(type="string", enum={"day","week","month","year","range"}, example="day")
* )
*
* @OA\Parameter(
@@ -236,8 +283,7 @@
* in="query",
* description="Reporting period.",
* required=false,
- * @OA\Schema(type="string", enum={"day","week","month","year","range"}),
- * example="day"
+ * @OA\Schema(type="string", enum={"day","week","month","year","range"}, example="day")
* )
*
* @OA\Parameter(
@@ -246,8 +292,7 @@
* in="query",
* description="Date or range (e.g. `2025-08-01`, `yesterday`, `last30`, or `2025-08-01,2025-08-11`).",
* required=true,
- * @OA\Schema(type="string"),
- * example="yesterday"
+ * @OA\Schema(type="string", example="today")
* )
*
* @OA\Parameter(
@@ -256,8 +301,7 @@
* in="query",
* description="Date or range (e.g. `2025-08-01`, `yesterday`, `last30`, or `2025-08-01,2025-08-11`).",
* required=false,
- * @OA\Schema(type="string"),
- * example="yesterday"
+ * @OA\Schema(type="string", example="today")
* )
*
* @OA\Parameter(
@@ -281,14 +325,14 @@
* Parameters specific to DataTables and Views
* @OA\Parameter(parameter="flatOptional", name="flat", in="query",
* description="Flatten subtables into the parent table.", required=false,
- * @OA\Schema(type="integer", enum={0,1}), example=0)
+ * @OA\Schema(type="integer", enum={0,1}, example=0))
*
* @OA\Parameter(parameter="filter_patternOptional", name="filter_pattern", in="query",
* description="Regex to keep matching rows.", required=false, @OA\Schema(type="string"))
*
* @OA\Parameter(parameter="filter_columnOptional", name="filter_column", in="query",
* description="Column to apply the regex to (e.g., `label`).", required=false,
- * @OA\Schema(type="string"), example="label")
+ * @OA\Schema(type="string", example="label"))
*
* @OA\Parameter(parameter="filter_pattern_recursiveOptional", name="filter_pattern_recursive", in="query",
* description="Recursive regex filter.", required=false, @OA\Schema(type="string"))
@@ -301,14 +345,14 @@
*
* @OA\Parameter(parameter="filter_excludelowpop_valueOptional", name="filter_excludelowpop_value", in="query",
* description="Minimum value threshold for `filter_excludelowpop`.", required=false,
- * @OA\Schema(type="number"), example=0)
+ * @OA\Schema(type="number", example=0))
*
* @OA\Parameter(parameter="filter_sort_columnOptional", name="filter_sort_column", in="query",
* description="Column to sort by.", required=false, @OA\Schema(type="string"))
*
* @OA\Parameter(parameter="filter_sort_orderOptional", name="filter_sort_order", in="query",
* description="Sort direction.", required=false,
- * @OA\Schema(type="string", enum={"asc","desc"}), example="desc")
+ * @OA\Schema(type="string", enum={"asc","desc"}, example="desc"))
*
* @OA\Parameter(parameter="filter_truncateOptional", name="filter_truncate", in="query",
* description="Row index after which rows are removed.", required=false, @OA\Schema(type="integer"))
@@ -321,15 +365,15 @@
*
* @OA\Parameter(parameter="keep_summary_rowOptional", name="keep_summary_row", in="query",
* description="Keep the summary row.", required=false,
- * @OA\Schema(type="integer", enum={0,1}), example=1)
+ * @OA\Schema(type="integer", enum={0,1}, example=1))
*
* @OA\Parameter(parameter="disable_generic_filtersOptional", name="disable_generic_filters", in="query",
* description="Disable generic filters (those above).", required=false,
- * @OA\Schema(type="integer", enum={0,1}), example=0)
+ * @OA\Schema(type="integer", enum={0,1}, example=0))
*
* @OA\Parameter(parameter="disable_queued_filtersOptional", name="disable_queued_filters", in="query",
* description="Skip queued filters.", required=false,
- * @OA\Schema(type="integer", enum={0,1}), example=0)
+ * @OA\Schema(type="integer", enum={0,1}, example=0))
*
* @OA\Parameter(parameter="hideColumnsOptional", name="hideColumns", in="query",
* description="Comma-separated list of columns to hide.", required=false, @OA\Schema(type="string"))
diff --git a/Commands/GenerateSpecFile.php b/Commands/GenerateSpecFile.php
index c89940c..3b13b46 100644
--- a/Commands/GenerateSpecFile.php
+++ b/Commands/GenerateSpecFile.php
@@ -29,7 +29,10 @@ protected function configure()
{
$this->setName('openapidocs:generate-spec-file');
$this->setDescription('Generate the OpenAPI documentation file for the Matomo APIs.');
- $this->addRequiredValueOption('plugin', null, 'Name of the plugin to document');
+ $this->addRequiredValueOption('plugin', 'p', 'Name of the plugin to document');
+ $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.');
}
/**
@@ -71,13 +74,24 @@ protected function doExecute(): int
$output = $this->getOutput();
$plugin = $input->getOption('plugin') ?: 'Matomo';
+ $format = $input->getOption('format') ?: 'json';
+ $version = $input->getOption('version') ?: '1.0.0';
+ $notDryRun = $input->getOption('not-dry-run') ?: false;
$message = sprintf('Generating documentation for: %s', $plugin);
$output->writeln($message);
- $output->writeln((new SpecGenerator())->generatePluginDoc($plugin));
+ $result = (new SpecGenerator())->generatePluginDoc($plugin, $format, $version, $notDryRun);
- return self::SUCCESS;
+ if ($notDryRun) {
+ $output->writeln('Results written to ' . $plugin . ' plugin\'s /OpenApi/Specs directory.');
+
+ return $result ? self::SUCCESS : self::FAILURE;
+ }
+
+ $output->writeln($result);
+
+ return $result ? self::SUCCESS : self::FAILURE;
}
}
diff --git a/Specs/SpecGenerator.php b/Specs/SpecGenerator.php
index bc33cc6..0229699 100644
--- a/Specs/SpecGenerator.php
+++ b/Specs/SpecGenerator.php
@@ -30,13 +30,19 @@ public function __construct()
}
}
- public function generatePluginDoc(string $pluginName, string $format = 'json', bool $writeToFile = false): string
+ public function generatePluginDoc(string $pluginName, string $format = 'json', string $version = '1.0.0', bool $writeToFile = false): string
{
BaseValidator::check('plugin', $pluginName, [new NotEmpty()]);
Manager::getInstance()->checkIsPluginActivated($pluginName);
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');
$pluginDir = Manager::getInstance()::getPluginDirectory($pluginName);
+ $pluginSpecDir = $pluginDir . '/OpenApi/Specs';
+ $pluginSpecPath = $pluginSpecDir . '/' . $pluginName . '_v' . $version . '.' . strtolower($format);
+ // If the directory doesn't exist yet, create it
+ if ($writeToFile && !is_dir($pluginSpecDir)) {
+ mkdir($pluginSpecDir, 0777, true);
+ }
// Check if the API class has been annotated and use the generated annotations file if it hasn't
$pluginAnnotationsSource = $pluginDir . '/API.php';
@@ -54,7 +60,6 @@ public function generatePluginDoc(string $pluginName, string $format = 'json', b
}
$generator = new Generator(StaticContainer::get(LoggerInterface::class));
- $generator->setVersion(OpenApi::DEFAULT_VERSION);
$openapi = $generator->generate([
$currentPluginDir . '/Annotations/GlobalApiComponents.php',
@@ -64,12 +69,19 @@ public function generatePluginDoc(string $pluginName, string $format = 'json', b
// Update title with plugin name
$openapi->info->title .= ' for ' . $pluginName . ' plugin';
+ $openapi->info->version = $version ?: '1.0.0';
+
// Remove the current server so that it isn't used when saving the spec file. It should only leave demo
if ($writeToFile && is_array($openapi->servers) && count($openapi->servers) > 1) {
unset($openapi->servers[0]);
$openapi->servers = array_values($openapi->servers);
}
- return strtolower($format) === 'yaml' ? $openapi->toYaml() : $openapi->toJson();
+ $specContents = strtolower($format) === 'yaml' ? $openapi->toYaml() : $openapi->toJson();
+ if ($writeToFile) {
+ file_put_contents($pluginSpecPath, $specContents);
+ }
+
+ return $specContents;
}
}