Skip to content

Commit 2a89ff8

Browse files
authored
Merge pull request #21 from matomo-org/PG-5012-literal-unions
Added literal string unions to OpenApiDocs, #PG-5012
1 parent 6f37444 commit 2a89ff8

4 files changed

Lines changed: 124 additions & 18 deletions

File tree

Annotations/AnnotationGenerator.php

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -437,15 +437,25 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
437437
if (count($typeHints) > 1 && in_array('bool', $typeHints)) {
438438
$typeHints = array_diff($typeHints, ['bool']);
439439
}
440-
foreach ($typeHints as $typePart) {
441-
$typePart = trim($typePart, ' ()');
442-
$normalisedType = $this->getOpenApiTypeFromPhpType($typePart);
443-
// If the type is array, check if there's a subType
444-
$subType = null;
445-
if ($normalisedType === 'array' && $typePart !== 'array' && strpos($typePart, '[]') !== false) {
446-
$subType = substr($typePart, 0, strpos($typePart, '[]'));
440+
441+
$allTypeHintsAreStringLiterals = $this->areAllTypeHintsStringLiterals($typeHints);
442+
$enumValues = [];
443+
if ($allTypeHintsAreStringLiterals) {
444+
$typesMap['string'] = null;
445+
foreach ($typeHints as $typeHint) {
446+
$enumValues[] = trim(trim($typeHint), '\'"');
447+
}
448+
} else {
449+
foreach ($typeHints as $typePart) {
450+
$typePart = trim($typePart, ' ()');
451+
$normalisedType = $this->getOpenApiTypeFromPhpType($typePart);
452+
// If the type is array, check if there's a subType
453+
$subType = null;
454+
if ($normalisedType === 'array' && $typePart !== 'array' && strpos($typePart, '[]') !== false) {
455+
$subType = substr($typePart, 0, strpos($typePart, '[]'));
456+
}
457+
$typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType;
447458
}
448-
$typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType;
449459
}
450460

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

477-
return [
487+
$paramData = [
478488
'name' => $paramName,
479489
'types' => $typesMap,
480490
'description' => $description,
481491
'required' => $isRequired ? 'true' : 'false',
482492
'default' => !$isRequired ? $default : NoDefaultValue::class,
483493
'example' => $example,
484494
];
495+
496+
if (!empty($enumValues)) {
497+
$paramData['enum'] = $enumValues;
498+
}
499+
500+
return $paramData;
501+
}
502+
503+
/**
504+
* Determine whether all type hints are quoted string literals.
505+
*
506+
* @param array $typeHints
507+
*
508+
* @return bool
509+
*/
510+
protected function areAllTypeHintsStringLiterals(array $typeHints): bool
511+
{
512+
if (empty($typeHints)) {
513+
return false;
514+
}
515+
516+
foreach ($typeHints as $typeHint) {
517+
$typeHint = trim(strval($typeHint));
518+
$firstChar = $typeHint[0] ?? '';
519+
$lastChar = $typeHint[strlen($typeHint) - 1] ?? '';
520+
if (!(($firstChar === "'" && $lastChar === "'") || ($firstChar === '"' && $lastChar === '"'))) {
521+
return false;
522+
}
523+
}
524+
525+
return true;
485526
}
486527

487528
/**
@@ -1715,12 +1756,19 @@ public function buildLinesForAnnotationObject(string $objectName, array $objectP
17151756
* @param string $subType This can specify the subtype for arrays. E.g. integer for int[] or string for string[].
17161757
* @param string $default The optional default value for the type. Default is no value.
17171758
* @param string $example The optional example value for the type. Default is empty string which indicated no value.
1759+
* @param array $enum The optional enum values for the type.
17181760
*
17191761
* @return array[]
17201762
*/
1721-
public function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class, string $example = ''): array
1763+
public function buildSchemaObjectArray(string $type, string $subType = '', string $default = NoDefaultValue::class, string $example = '', array $enum = []): array
17221764
{
17231765
$schemaMap = ['type="' . $type . '"'];
1766+
if (!empty($enum) && $type === 'string') {
1767+
$enumStringValues = array_map(static function ($value) {
1768+
return '"' . str_replace('"', '\"', strval($value)) . '"';
1769+
}, $enum);
1770+
$schemaMap[] = 'enum={' . implode(',', $enumStringValues) . '}';
1771+
}
17241772
if (($example) !== '') {
17251773
$schemaMap[] = 'example=' . $this->wrapStringWithQuotes($example, $type);
17261774
}
@@ -1800,14 +1848,16 @@ public function shouldIncludeDefault(string $type, string $default = NoDefaultVa
18001848
* default is set.
18011849
* @param string $example The value to use as the example property of the schema. If it's an empty string, no
18021850
* example is set.
1851+
* @param array $enum The optional enum values to apply to string schemas.
18031852
*
18041853
* @return array[] The collection of lines which make up the schema annotation object.
18051854
*/
1806-
public function buildSchemaObjectArrays(array $typesMap, string $default = '', string $example = ''): array
1855+
public function buildSchemaObjectArrays(array $typesMap, string $default = '', string $example = '', array $enum = []): array
18071856
{
18081857
$schemas = [];
18091858
foreach ($typesMap as $type => $subType) {
1810-
$schemas[] = $this->buildSchemaObjectArray($type, $subType ?? '', $default, $example);
1859+
$schemaEnum = $type === 'string' ? $enum : [];
1860+
$schemas[] = $this->buildSchemaObjectArray($type, $subType ?? '', $default, $example, $schemaEnum);
18111861
}
18121862

18131863
if (count($schemas) === 1) {
@@ -1867,7 +1917,12 @@ public function compileOperationLines(string $path, string $opId, string $plugin
18671917
// Escape quotes differently for the annotation examples
18681918
$exampleString = str_replace('\"', '""', $exampleString);
18691919
}
1870-
$paramMap[] = $this->buildSchemaObjectArrays($param['types'], strval($param['default']), strval($exampleString));
1920+
$paramMap[] = $this->buildSchemaObjectArrays(
1921+
$param['types'],
1922+
strval($param['default']),
1923+
strval($exampleString),
1924+
$param['enum'] ?? []
1925+
);
18711926
$operationValuesMap[] = ['@OA\Parameter' => $paramMap];
18721927
}
18731928
foreach ($responses as $response) {

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Changelog
22

3+
5.0.2-b1 - 2026-02-16
4+
- Added support for string literal union types
35

46
5.0.1-b1 - 2026-02-16
57
- Added class and function level docs

plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "OpenApiDocs",
33
"description": "Generate API documentation for Matomo in the OpenAPI format.",
4-
"version": "5.0.1-b1",
4+
"version": "5.0.2-b1",
55
"theme": false,
66
"keywords": [
77
"API",
@@ -28,4 +28,4 @@
2828
"source": "https:\/\/github.com\/matomo-org\/plugin-OpenApiDocs"
2929
},
3030
"category": "development"
31-
}
31+
}

tests/Unit/AnnotationGeneratorTest.php

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,38 @@ public function getTestDataForBuildParameterAnnotationData(): iterable
596596
'default' => 'Piwik\API\NoDefaultValue',
597597
'example' => '',
598598
]];
599+
yield 'should extract enum values when docInfo is a union of string literals' => ['period', [], [
600+
'type' => "'day'|'week'|'month'",
601+
], [
602+
'name' => 'period',
603+
'types' => ['string' => null],
604+
'description' => '',
605+
'required' => 'true',
606+
'default' => 'Piwik\API\NoDefaultValue',
607+
'example' => '',
608+
'enum' => ['day', 'week', 'month'],
609+
]];
610+
yield 'should extract enum values when docInfo uses double-quoted string literals' => ['format', [], [
611+
'type' => '"json"|"xml"',
612+
], [
613+
'name' => 'format',
614+
'types' => ['string' => null],
615+
'description' => '',
616+
'required' => 'true',
617+
'default' => 'Piwik\API\NoDefaultValue',
618+
'example' => '',
619+
'enum' => ['json', 'xml'],
620+
]];
621+
yield 'should not add enum when union mixes string literal and non-literal type' => ['period', [], [
622+
'type' => "'day'|int",
623+
], [
624+
'name' => 'period',
625+
'types' => ['string' => null, 'integer' => null],
626+
'description' => '',
627+
'required' => 'true',
628+
'default' => 'Piwik\API\NoDefaultValue',
629+
'example' => '',
630+
]];
599631
yield 'should allow multiple types when metadata type is string' => ['someParam', [
600632
'type' => 'string',
601633
], [
@@ -1081,10 +1113,27 @@ public function testBuildLinesForAnnotationObject(): void
10811113
$this->expectNotToPerformAssertions();
10821114
}
10831115

1084-
public function testBuildSchemaObjectArray(): void
1116+
public function testBuildSchemaObjectArrayWithStringEnum(): void
10851117
{
1086-
// TODO - buildSchemaObjectArray method
1087-
$this->expectNotToPerformAssertions();
1118+
$expectedWithEnum = [
1119+
'@OA\Schema' => [
1120+
'type="string"',
1121+
'enum={"day","week"}',
1122+
'example="day"',
1123+
],
1124+
];
1125+
$this->assertEquals($expectedWithEnum, $this->annotationGenerator->buildSchemaObjectArray('string', '', NoDefaultValue::class, 'day', ['day', 'week']));
1126+
}
1127+
1128+
public function testBuildSchemaObjectArrayIgnoresEnumForNonStringTypes(): void
1129+
{
1130+
$expectedWithoutEnum = [
1131+
'@OA\Schema' => [
1132+
'type="integer"',
1133+
'example=1',
1134+
],
1135+
];
1136+
$this->assertEquals($expectedWithoutEnum, $this->annotationGenerator->buildSchemaObjectArray('integer', '', NoDefaultValue::class, '1', ['1', '2']));
10881137
}
10891138

10901139
/**

0 commit comments

Comments
 (0)