Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 68 additions & 13 deletions Annotations/AnnotationGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -437,15 +437,25 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
if (count($typeHints) > 1 && in_array('bool', $typeHints)) {
$typeHints = array_diff($typeHints, ['bool']);
}
foreach ($typeHints as $typePart) {
$typePart = trim($typePart, ' ()');
$normalisedType = $this->getOpenApiTypeFromPhpType($typePart);
// If the type is array, check if there's a subType
$subType = null;
if ($normalisedType === 'array' && $typePart !== 'array' && strpos($typePart, '[]') !== false) {
$subType = substr($typePart, 0, strpos($typePart, '[]'));

$allTypeHintsAreStringLiterals = $this->areAllTypeHintsStringLiterals($typeHints);
$enumValues = [];
if ($allTypeHintsAreStringLiterals) {
$typesMap['string'] = null;
foreach ($typeHints as $typeHint) {
$enumValues[] = trim(trim($typeHint), '\'"');
}
} else {
foreach ($typeHints as $typePart) {
$typePart = trim($typePart, ' ()');
$normalisedType = $this->getOpenApiTypeFromPhpType($typePart);
// If the type is array, check if there's a subType
$subType = null;
if ($normalisedType === 'array' && $typePart !== 'array' && strpos($typePart, '[]') !== false) {
$subType = substr($typePart, 0, strpos($typePart, '[]'));
}
$typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType;
}
$typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType;
}

$isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue;
Expand Down Expand Up @@ -474,14 +484,45 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
$default = json_encode($default);
}

return [
$paramData = [
'name' => $paramName,
'types' => $typesMap,
'description' => $description,
'required' => $isRequired ? 'true' : 'false',
'default' => !$isRequired ? $default : NoDefaultValue::class,
'example' => $example,
];

if (!empty($enumValues)) {
$paramData['enum'] = $enumValues;
}

return $paramData;
}

/**
* Determine whether all type hints are quoted string literals.
*
* @param array $typeHints
*
* @return bool
*/
protected function areAllTypeHintsStringLiterals(array $typeHints): bool
{
if (empty($typeHints)) {
return false;
}

foreach ($typeHints as $typeHint) {
$typeHint = trim(strval($typeHint));
$firstChar = $typeHint[0] ?? '';
$lastChar = $typeHint[strlen($typeHint) - 1] ?? '';
if (!(($firstChar === "'" && $lastChar === "'") || ($firstChar === '"' && $lastChar === '"'))) {
return false;
}
}

return true;
}

/**
Expand Down Expand Up @@ -1715,12 +1756,19 @@ public function buildLinesForAnnotationObject(string $objectName, array $objectP
* @param string $subType This can specify the subtype for arrays. E.g. integer for int[] or string for string[].
* @param string $default The optional default value for the type. Default is no value.
* @param string $example The optional example value for the type. Default is empty string which indicated no value.
* @param array $enum The optional enum values for the type.
*
* @return array[]
*/
public function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class, string $example = ''): array
public function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class, string $example = '', array $enum = []): array
{
$schemaMap = ['type="' . $type . '"'];
if (!empty($enum) && $type === 'string') {
$enumStringValues = array_map(static function ($value) {
return '"' . str_replace('"', '\"', strval($value)) . '"';
}, $enum);
$schemaMap[] = 'enum={' . implode(',', $enumStringValues) . '}';
}
if (($example) !== '') {
$schemaMap[] = 'example=' . $this->wrapStringWithQuotes($example, $type);
}
Expand Down Expand Up @@ -1800,14 +1848,16 @@ public function shouldIncludeDefault(string $type, string $default = NoDefaultVa
* default is set.
* @param string $example The value to use as the example property of the schema. If it's an empty string, no
* example is set.
* @param array $enum The optional enum values to apply to string schemas.
*
* @return array[] The collection of lines which make up the schema annotation object.
*/
public function buildSchemaObjectArrays(array $typesMap, string $default = '', string $example = ''): array
public function buildSchemaObjectArrays(array $typesMap, string $default = '', string $example = '', array $enum = []): array
{
$schemas = [];
foreach ($typesMap as $type => $subType) {
$schemas[] = $this->buildSchemaObjectArray($type, $subType ?? '', $default, $example);
$schemaEnum = $type === 'string' ? $enum : [];
$schemas[] = $this->buildSchemaObjectArray($type, $subType ?? '', $default, $example, $schemaEnum);
}

if (count($schemas) === 1) {
Expand Down Expand Up @@ -1867,7 +1917,12 @@ public function compileOperationLines(string $path, string $opId, string $plugin
// Escape quotes differently for the annotation examples
$exampleString = str_replace('\"', '""', $exampleString);
}
$paramMap[] = $this->buildSchemaObjectArrays($param['types'], strval($param['default']), strval($exampleString));
$paramMap[] = $this->buildSchemaObjectArrays(
$param['types'],
strval($param['default']),
strval($exampleString),
$param['enum'] ?? []
);
$operationValuesMap[] = ['@OA\Parameter' => $paramMap];
}
foreach ($responses as $response) {
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Changelog

5.0.2-b1 - 2026-02-16
- Added support for string literal union types

5.0.1-b1 - 2026-02-16
- Added class and function level docs
Expand Down
4 changes: 2 additions & 2 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "OpenApiDocs",
"description": "Generate API documentation for Matomo in the OpenAPI format.",
"version": "5.0.1-b1",
"version": "5.0.2-b1",
"theme": false,
"keywords": [
"API",
Expand All @@ -28,4 +28,4 @@
"source": "https:\/\/github.com\/matomo-org\/plugin-OpenApiDocs"
},
"category": "development"
}
}
55 changes: 52 additions & 3 deletions tests/Unit/AnnotationGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,38 @@ public function getTestDataForBuildParameterAnnotationData(): iterable
'default' => 'Piwik\API\NoDefaultValue',
'example' => '',
]];
yield 'should extract enum values when docInfo is a union of string literals' => ['period', [], [
'type' => "'day'|'week'|'month'",
], [
'name' => 'period',
'types' => ['string' => null],
'description' => '',
'required' => 'true',
'default' => 'Piwik\API\NoDefaultValue',
'example' => '',
'enum' => ['day', 'week', 'month'],
]];
yield 'should extract enum values when docInfo uses double-quoted string literals' => ['format', [], [
'type' => '"json"|"xml"',
], [
'name' => 'format',
'types' => ['string' => null],
'description' => '',
'required' => 'true',
'default' => 'Piwik\API\NoDefaultValue',
'example' => '',
'enum' => ['json', 'xml'],
]];
yield 'should not add enum when union mixes string literal and non-literal type' => ['period', [], [
'type' => "'day'|int",
], [
'name' => 'period',
'types' => ['string' => null, 'integer' => null],
'description' => '',
'required' => 'true',
'default' => 'Piwik\API\NoDefaultValue',
'example' => '',
]];
yield 'should allow multiple types when metadata type is string' => ['someParam', [
'type' => 'string',
], [
Expand Down Expand Up @@ -1081,10 +1113,27 @@ public function testBuildLinesForAnnotationObject(): void
$this->expectNotToPerformAssertions();
}

public function testBuildSchemaObjectArray(): void
public function testBuildSchemaObjectArrayWithStringEnum(): void
{
// TODO - buildSchemaObjectArray method
$this->expectNotToPerformAssertions();
$expectedWithEnum = [
'@OA\Schema' => [
'type="string"',
'enum={"day","week"}',
'example="day"',
],
];
$this->assertEquals($expectedWithEnum, $this->annotationGenerator->buildSchemaObjectArray('string', '', NoDefaultValue::class, 'day', ['day', 'week']));
}

public function testBuildSchemaObjectArrayIgnoresEnumForNonStringTypes(): void
{
$expectedWithoutEnum = [
'@OA\Schema' => [
'type="integer"',
'example=1',
],
];
$this->assertEquals($expectedWithoutEnum, $this->annotationGenerator->buildSchemaObjectArray('integer', '', NoDefaultValue::class, '1', ['1', '2']));
}

/**
Expand Down