Skip to content

Commit bd06476

Browse files
authored
Add examples for try it out, #PG-5178 (#38)
* Added basic parameter examples, not currently showing complex types, fixed logic around basic array shapes * generated more meaningful examples + removed not required * phpcs * Fix tests * Added missing required examples and fixed default logic
1 parent f290b8f commit bd06476

4 files changed

Lines changed: 541 additions & 11 deletions

File tree

Annotations/AnnotationGenerator.php

Lines changed: 173 additions & 10 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,11 +509,20 @@ 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

512522
$default = $paramMetadata['default'] ?? null;
513-
if (!is_string($default)) {
523+
if ($default === null) {
524+
$default = NoDefaultValue::class;
525+
} elseif (!is_string($default)) {
514526
$default = json_encode($default);
515527
}
516528

@@ -555,6 +567,138 @@ protected function areAllTypeHintsStringLiterals(array $typeHints): bool
555567
return true;
556568
}
557569

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

@@ -1972,12 +2116,17 @@ public function compileOperationLines(string $path, string $opId, string $plugin
19722116
$paramMap[] = 'description="' . $param['description'] . '"';
19732117
}
19742118
$exampleString = $param['example'];
1975-
if (in_array('array', array_keys($param['types']))) {
2119+
$useParameterLevelExample = $this->shouldUseParameterLevelExample($param['types'], $exampleString);
2120+
if (in_array('array', array_keys($param['types'])) && !$useParameterLevelExample) {
19762121
// The annotation expects example objects and not arrays, so replace [] with {}
19772122
$exampleString = str_replace(['[', ']'], ['{', '}'], $exampleString);
19782123
// Escape quotes differently for the annotation examples
19792124
$exampleString = str_replace('\"', '""', $exampleString);
19802125
}
2126+
if ($useParameterLevelExample) {
2127+
$paramMap[] = 'example="' . $this->normaliseDescriptionText($exampleString) . '"';
2128+
$exampleString = '';
2129+
}
19812130
$paramMap[] = $this->buildSchemaObjectArrays(
19822131
$param['types'],
19832132
strval($param['default']),
@@ -2019,4 +2168,18 @@ public function compileOperationLines(string $path, string $opId, string $plugin
20192168
$this->removeTrailingCommaFromLastLine($lines);
20202169
return $lines;
20212170
}
2171+
2172+
/**
2173+
* Use a parameter-level string example for scalar/array unions so Swagger UI can show a concrete query value.
2174+
*
2175+
* @param array<string, string|null> $typesMap
2176+
*/
2177+
protected function shouldUseParameterLevelExample(array $typesMap, string $example): bool
2178+
{
2179+
if (count($typesMap) <= 1 || !array_key_exists('array', $typesMap) || $example === '') {
2180+
return false;
2181+
}
2182+
2183+
return is_array(json_decode($example, true));
2184+
}
20222185
}

0 commit comments

Comments
 (0)