fix(api-docs): deduplicate explicit OpenAPI parameters

Explicit ApiParameter metadata now replaces matching auto-generated path parameters instead of producing duplicates, matching the precedence used by the Go OpenAPI builder.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 22:32:23 +00:00
parent a89a70851f
commit 929b6b97ca
2 changed files with 57 additions and 4 deletions

View file

@ -523,16 +523,36 @@ class OpenApiBuilder
protected function buildParameters(Route $route, ?object $controller, string $action, array $config): array
{
$parameters = [];
$parameterIndex = [];
$addParameter = function (array $parameter) use (&$parameters, &$parameterIndex): void {
$name = $parameter['name'] ?? null;
$in = $parameter['in'] ?? null;
if (! is_string($name) || $name === '' || ! is_string($in) || $in === '') {
return;
}
$key = $in.':'.$name;
if (isset($parameterIndex[$key])) {
$parameters[$parameterIndex[$key]] = $parameter;
return;
}
$parameterIndex[$key] = count($parameters);
$parameters[] = $parameter;
};
// Add path parameters
preg_match_all('/\{([^}?]+)\??}/', $route->uri(), $matches);
foreach ($matches[1] as $param) {
$parameters[] = [
$addParameter([
'name' => $param,
'in' => 'path',
'required' => true,
'schema' => ['type' => 'string'],
];
]);
}
// Add parameters from ApiParameter attributes
@ -544,12 +564,12 @@ class OpenApiBuilder
foreach ($paramAttrs as $attr) {
$param = $attr->newInstance();
$parameters[] = $param->toOpenApi();
$addParameter($param->toOpenApi());
}
}
}
return $parameters;
return array_values($parameters);
}
/**

View file

@ -245,6 +245,29 @@ describe('Application Endpoint Parameter Docs', function () {
expect($serverIncludeContent['schema']['type'])->toBe('boolean');
});
it('lets explicit path parameter metadata override the generated entry', function () {
RouteFacade::prefix('api')
->middleware('api')
->group(function () {
RouteFacade::get('/test-scan/items/{id}/explicit', [TestExplicitPathParameterController::class, 'show']);
});
$builder = new OpenApiBuilder;
$spec = $builder->build();
$operation = $spec['paths']['/api/test-scan/items/{id}/explicit']['get'];
$parameters = $operation['parameters'] ?? [];
expect($parameters)->toHaveCount(1);
$idParam = collect($parameters)->firstWhere('name', 'id');
expect($idParam)->not->toBeNull();
expect($idParam['in'])->toBe('path');
expect($idParam['required'])->toBeTrue();
expect($idParam['description'])->toBe('Explicit item identifier');
});
it('documents the MCP tool call request body shape', function () {
$builder = new OpenApiBuilder;
$spec = $builder->build();
@ -1178,6 +1201,16 @@ class TestPartialHiddenController
public function hiddenMethod(): void {}
}
/**
* Test controller with an explicit path parameter override.
*/
class TestExplicitPathParameterController
{
#[ApiParameter('id', 'path', 'string', 'Explicit item identifier')]
#[ApiResponse(200, TestJsonResource::class, 'Item details')]
public function show(string $id): void {}
}
/**
* Test tagged controller.
*/