Skip to content

Commit 5cad729

Browse files
committed
Added basic parameter examples, not currently showing complex types, fixed logic around basic array shapes
1 parent 41aa12f commit 5cad729

4 files changed

Lines changed: 882 additions & 10 deletions

File tree

Annotations/AnnotationGenerator.php

Lines changed: 170 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ class AnnotationGenerator
9999
*/
100100
protected $allowLocalRequests;
101101

102+
/**
103+
* @var array<string, mixed>|null
104+
*/
105+
protected $parameterExamples;
106+
102107
public function __construct(
103108
DocumentationGenerator $generator,
104109
?PathResolver $pathResolver = null,
@@ -110,6 +115,7 @@ public function __construct(
110115
$this->artifactWriter = $artifactWriter ?? new ArtifactWriter();
111116
$this->missingImportantDataWarnings = [];
112117
$this->allowLocalRequests = $allowLocalRequests;
118+
$this->parameterExamples = null;
113119
$this->currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');
114120
}
115121

@@ -455,7 +461,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
455461
// Sometimes, doc-block can wrap type hinting with parenthesis. Remove them.
456462
$type = trim($type, '()');
457463
// If the signature type is array, but the type hinting provides more, use that instead
458-
if ($type === 'array' && strpos($docType, '[]') !== false && strpos($docType, '|') === false) {
464+
if ($type === 'array' && $this->hasSpecificArrayShape($docType) && strpos($docType, '|') === false) {
459465
$type = $docType;
460466
}
461467
$typesMap = [];
@@ -468,6 +474,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
468474
$typeHints = array_diff($typeHints, ['bool']);
469475
}
470476

477+
$isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue;
471478
$allTypeHintsAreStringLiterals = $this->areAllTypeHintsStringLiterals($typeHints);
472479
$enumValues = [];
473480
if ($allTypeHintsAreStringLiterals) {
@@ -478,17 +485,13 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
478485
} else {
479486
foreach ($typeHints as $typePart) {
480487
$typePart = trim($typePart, ' ()');
481-
$normalisedType = $this->getOpenApiTypeFromPhpType($typePart);
488+
$normalisedType = $this->hasSpecificArrayShape($typePart) ? 'array' : $this->getOpenApiTypeFromPhpType($typePart);
482489
// If the type is array, check if there's a subType
483-
$subType = null;
484-
if ($normalisedType === 'array' && $typePart !== 'array' && strpos($typePart, '[]') !== false) {
485-
$subType = substr($typePart, 0, strpos($typePart, '[]'));
486-
}
490+
$subType = $this->getArraySubTypeFromPhpType($typePart, $normalisedType);
487491
$typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType;
488492
}
489493
}
490494

491-
$isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue;
492495
$description = $paramDocInfo['description'] ?? '';
493496
if (empty($description)) {
494497
$this->addMissingImportantDataWarning($methodName, $paramName, 'Description is not specified in comment block.');
@@ -506,6 +509,13 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa
506509
$example = trim($example, '"');
507510
}
508511

512+
if ($isRequired && $example === '') {
513+
$configExample = $this->getParameterExampleFromConfig($paramName, $type, $typesMap);
514+
if ($configExample !== null) {
515+
$example = $configExample;
516+
}
517+
}
518+
509519
// Clean up the descriptions a little more like removing linebreaks and escaping double-quotes
510520
$description = $this->normaliseDescriptionText($description);
511521

@@ -555,6 +565,138 @@ protected function areAllTypeHintsStringLiterals(array $typeHints): bool
555565
return true;
556566
}
557567

568+
protected function hasSpecificArrayShape(string $type): bool
569+
{
570+
return strpos($type, '[]') !== false || preg_match('/^(array|list)<.+>$/', trim($type)) === 1;
571+
}
572+
573+
protected function getArraySubTypeFromPhpType(string $typePart, string $normalisedType): ?string
574+
{
575+
if ($normalisedType !== 'array' || $typePart === 'array') {
576+
return null;
577+
}
578+
579+
if (strpos($typePart, '[]') !== false) {
580+
return substr($typePart, 0, strpos($typePart, '[]'));
581+
}
582+
583+
if (preg_match('/^(array|list)<(.+)>$/', trim($typePart), $matches) !== 1) {
584+
return null;
585+
}
586+
587+
$genericParts = array_map('trim', explode(',', $matches[2], 2));
588+
if (count($genericParts) === 1) {
589+
return $genericParts[0];
590+
}
591+
592+
return $genericParts[1];
593+
}
594+
595+
/**
596+
* Load and return the configured parameter examples.
597+
*
598+
* @return array<string, mixed>
599+
*/
600+
protected function getParameterExamplesConfig(): array
601+
{
602+
if ($this->parameterExamples !== null) {
603+
return $this->parameterExamples;
604+
}
605+
606+
$configPath = $this->currentPluginDir . '/config/ParameterExamples.php';
607+
if (!is_file($configPath)) {
608+
$this->parameterExamples = [];
609+
return $this->parameterExamples;
610+
}
611+
612+
$config = require $configPath;
613+
$this->parameterExamples = is_array($config) ? $config : [];
614+
615+
return $this->parameterExamples;
616+
}
617+
618+
/**
619+
* Return a config-backed example string if the configured example is intentionally simple enough to support.
620+
*/
621+
protected function getParameterExampleFromConfig(string $paramName, string $type, array $typesMap): ?string
622+
{
623+
$config = $this->getParameterExamplesConfig();
624+
$keysToTry = [
625+
$paramName . ':' . $type,
626+
$paramName . ':' . preg_replace('/\s+/', '', $type),
627+
];
628+
629+
$configValue = null;
630+
$foundConfigValue = false;
631+
foreach (array_unique($keysToTry) as $key) {
632+
if (!array_key_exists($key, $config)) {
633+
continue;
634+
}
635+
636+
$configValue = $config[$key];
637+
$foundConfigValue = true;
638+
break;
639+
}
640+
641+
if (!$foundConfigValue) {
642+
return null;
643+
}
644+
645+
return $this->normaliseConfiguredParameterExample($configValue, $typesMap);
646+
}
647+
648+
/**
649+
* Convert supported scalar/basic-array config values into the string form used by schema generation.
650+
*/
651+
protected function normaliseConfiguredParameterExample($example, array $typesMap = []): ?string
652+
{
653+
if (is_bool($example)) {
654+
return $example ? 'true' : 'false';
655+
}
656+
657+
if (is_int($example) || is_float($example) || is_string($example)) {
658+
return strval($example);
659+
}
660+
661+
if (
662+
!is_array($example)
663+
|| !$this->isBasicExampleArray($example)
664+
|| !$this->supportsBasicArrayExample($typesMap)
665+
) {
666+
return null;
667+
}
668+
669+
$encoded = json_encode(array_values($example));
670+
671+
return is_string($encoded) ? $encoded : null;
672+
}
673+
674+
/**
675+
* Only use array config examples when the emitted schema includes an array shape.
676+
*/
677+
protected function supportsBasicArrayExample(array $typesMap): bool
678+
{
679+
return array_key_exists('array', $typesMap);
680+
}
681+
682+
/**
683+
* Only support flat indexed arrays of scalar values for now.
684+
*/
685+
protected function isBasicExampleArray(array $example): bool
686+
{
687+
if (array_values($example) !== $example) {
688+
return false;
689+
}
690+
691+
foreach ($example as $item) {
692+
if (!is_bool($item) && !is_int($item) && !is_float($item) && !is_string($item)) {
693+
return false;
694+
}
695+
}
696+
697+
return true;
698+
}
699+
558700
/**
559701
* Take description text and normalise it. This includes trimming surrounding whitespace, removing newlines and
560702
* escaping double-quote characters.
@@ -1863,7 +2005,7 @@ public function buildSchemaObjectArray(string $type, string $subType = '', strin
18632005
*/
18642006
public function wrapStringWithQuotes(string $string, string $type, string $quoteCharacter = '"'): string
18652007
{
1866-
if (in_array($type, ['integer', 'boolean', 'array'])) {
2008+
if (in_array($type, ['integer', 'number', 'boolean', 'array'])) {
18672009
return $string;
18682010
}
18692011

@@ -1972,12 +2114,17 @@ public function compileOperationLines(string $path, string $opId, string $plugin
19722114
$paramMap[] = 'description="' . $param['description'] . '"';
19732115
}
19742116
$exampleString = $param['example'];
1975-
if (in_array('array', array_keys($param['types']))) {
2117+
$useParameterLevelExample = $this->shouldUseParameterLevelExample($param['types'], $exampleString);
2118+
if (in_array('array', array_keys($param['types'])) && !$useParameterLevelExample) {
19762119
// The annotation expects example objects and not arrays, so replace [] with {}
19772120
$exampleString = str_replace(['[', ']'], ['{', '}'], $exampleString);
19782121
// Escape quotes differently for the annotation examples
19792122
$exampleString = str_replace('\"', '""', $exampleString);
19802123
}
2124+
if ($useParameterLevelExample) {
2125+
$paramMap[] = 'example="' . $this->normaliseDescriptionText($exampleString) . '"';
2126+
$exampleString = '';
2127+
}
19812128
$paramMap[] = $this->buildSchemaObjectArrays(
19822129
$param['types'],
19832130
strval($param['default']),
@@ -2019,4 +2166,18 @@ public function compileOperationLines(string $path, string $opId, string $plugin
20192166
$this->removeTrailingCommaFromLastLine($lines);
20202167
return $lines;
20212168
}
2169+
2170+
/**
2171+
* Use a parameter-level string example for scalar/array unions so Swagger UI can show a concrete query value.
2172+
*
2173+
* @param array<string, string|null> $typesMap
2174+
*/
2175+
protected function shouldUseParameterLevelExample(array $typesMap, string $example): bool
2176+
{
2177+
if (count($typesMap) <= 1 || !array_key_exists('array', $typesMap) || $example === '') {
2178+
return false;
2179+
}
2180+
2181+
return is_array(json_decode($example, true));
2182+
}
20222183
}

0 commit comments

Comments
 (0)