diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php index 84c85c5..3024a45 100644 --- a/Annotations/AnnotationGenerator.php +++ b/Annotations/AnnotationGenerator.php @@ -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; @@ -474,7 +484,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa $default = json_encode($default); } - return [ + $paramData = [ 'name' => $paramName, 'types' => $typesMap, 'description' => $description, @@ -482,6 +492,37 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa '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; } /** @@ -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); } @@ -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) { @@ -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) { diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8b2a2..895de6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/plugin.json b/plugin.json index 71c1c66..76e6c4a 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.1-b1", + "version": "5.0.2-b1", "theme": false, "keywords": [ "API", @@ -28,4 +28,4 @@ "source": "https:\/\/github.com\/matomo-org\/plugin-OpenApiDocs" }, "category": "development" -} \ No newline at end of file +} diff --git a/tests/Unit/AnnotationGeneratorTest.php b/tests/Unit/AnnotationGeneratorTest.php index 5f34842..9790de7 100644 --- a/tests/Unit/AnnotationGeneratorTest.php +++ b/tests/Unit/AnnotationGeneratorTest.php @@ -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', ], [ @@ -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'])); } /**