Skip to content

Commit 7d512fd

Browse files
committed
feat(ControllerMethod): Support RequestHeader attribute
Signed-off-by: provokateurin <[email protected]>
1 parent bb21dad commit 7d512fd

File tree

5 files changed

+134
-15
lines changed

5 files changed

+134
-15
lines changed

generate-spec.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -835,17 +835,21 @@
835835
$parameters[] = $parameter;
836836
}
837837

838-
foreach ($route->controllerMethod->requestHeaders as $requestHeader) {
839-
$parameters[] = [
838+
foreach ($route->controllerMethod->requestHeaders as $requestHeader => $requestHeaderDescription) {
839+
$parameter = [
840840
'name' => $requestHeader,
841841
'in' => 'header',
842842
// Not required, because getHeader() will return an empty string by default.
843843
// It might still mean that a header is always required, but the controller method has to check the
844844
// value manually anyway.
845-
'schema' => [
846-
'type' => 'string',
847-
],
848845
];
846+
if ($requestHeaderDescription !== null) {
847+
$parameter['description'] = $requestHeaderDescription;
848+
}
849+
$parameter['schema'] = [
850+
'type' => 'string',
851+
];
852+
$parameters[] = $parameter;
849853
}
850854

851855
if ($route->isOCS || !$route->isNoCSRFRequired) {

src/ControllerMethod.php

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace OpenAPIExtractor;
99

10+
use PhpParser\Node\AttributeGroup;
1011
use PhpParser\Node\Expr\MethodCall;
1112
use PhpParser\Node\Expr\PropertyFetch;
1213
use PhpParser\Node\Expr\Variable;
@@ -20,13 +21,14 @@
2021
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
2122
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
2223
use PHPStan\PhpDocParser\Parser\TokenIterator;
24+
use RuntimeException;
2325

2426
class ControllerMethod {
2527
private const STATUS_CODE_DESCRIPTION_PATTERN = '/^(\d{3}): (.+)$/';
2628

2729
/**
2830
* @param ControllerMethodParameter[] $parameters
29-
* @param list<string> $requestHeaders
31+
* @param array<string, string> $requestHeaders
3032
* @param list<ControllerMethodResponse|null> $responses
3133
* @param OpenApiType[] $returns
3234
* @param array<int, string> $responseDescription
@@ -192,7 +194,7 @@ public static function parse(string $context,
192194
// Only keep lines that don't match the status code pattern in the description
193195
$description = Helpers::cleanDocComment(implode("\n", array_filter(array_filter(explode("\n", $description), static fn (string $line): bool => trim($line) !== ''), static fn (string $line): bool => in_array(preg_match(self::STATUS_CODE_DESCRIPTION_PATTERN, $line), [0, false], true))));
194196

195-
if ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode && $psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
197+
if ($paramTag instanceof ParamTagValueNode && $psalmParamTag instanceof ParamTagValueNode) {
196198
try {
197199
$type = OpenApiType::resolve(
198200
$context . ': @param: ' . $psalmParamTag->parameterName,
@@ -221,9 +223,9 @@ public static function parse(string $context,
221223
);
222224
}
223225

224-
} elseif ($psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
226+
} elseif ($psalmParamTag instanceof ParamTagValueNode) {
225227
$type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $psalmParamTag);
226-
} elseif ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
228+
} elseif ($paramTag instanceof ParamTagValueNode) {
227229
$type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $paramTag);
228230
} elseif ($allowMissingDocs) {
229231
$type = OpenApiType::resolve($context . ': $' . $methodParameterName . ': ' . $methodParameterName, $definitions, $methodParameter->type);
@@ -286,14 +288,14 @@ public static function parse(string $context,
286288
Logger::warning($context, 'Summary ends with a punctuation mark');
287289
}
288290

289-
$headers = [];
291+
$codeRequestHeaders = [];
290292
foreach ($nodeFinder->findInstanceOf($method->getStmts(), MethodCall::class) as $methodCall) {
291293
if ($methodCall->var instanceof PropertyFetch &&
292294
$methodCall->var->var instanceof Variable &&
293295
$methodCall->var->var->name === 'this' &&
294296
$methodCall->var->name->name === 'request') {
295297
if ($methodCall->name->name === 'getHeader') {
296-
$headers[] = $methodCall->args[0]->value->value;
298+
$codeRequestHeaders[] = $methodCall->args[0]->value->value;
297299
}
298300
if ($methodCall->name->name === 'getParam') {
299301
$name = $methodCall->args[0]->value->value;
@@ -326,7 +328,47 @@ public static function parse(string $context,
326328
}
327329
}
328330

329-
return new ControllerMethod($parameters, array_unique($headers), $responses, $responseDescriptions, $methodDescription, $methodSummary, $isDeprecated);
331+
$attributeRequestHeaders = [];
332+
/** @var AttributeGroup $attrGroup */
333+
foreach ($method->attrGroups as $attrGroup) {
334+
foreach ($attrGroup->attrs as $attr) {
335+
if ($attr->name->getLast() === 'RequestHeader') {
336+
$args = [];
337+
foreach ($attr->args as $key => $arg) {
338+
$attrName = $arg->name?->name;
339+
if ($attrName === null) {
340+
$attrName = match ($key) {
341+
0 => 'name',
342+
1 => 'description',
343+
default => throw new RuntimeException('Should not happen.'),
344+
};
345+
}
346+
$args[$attrName] = $arg->value->value;
347+
}
348+
349+
if (array_key_exists($args['name'], $attributeRequestHeaders)) {
350+
Logger::error($context, 'Request header "' . $args['name'] . '" already documented.');
351+
}
352+
353+
$attributeRequestHeaders[$args['name']] = $args['description'];
354+
}
355+
}
356+
}
357+
358+
$undocumentedRequestHeaders = array_diff($codeRequestHeaders, array_keys($attributeRequestHeaders));
359+
if ($undocumentedRequestHeaders !== []) {
360+
Logger::warning($context, 'Undocumented request headers (use the RequestHeader attribute): ' . implode(', ', $undocumentedRequestHeaders));
361+
foreach ($undocumentedRequestHeaders as $header) {
362+
$attributeRequestHeaders[$header] = null;
363+
}
364+
}
365+
366+
$unusedRequestHeaders = array_diff(array_keys($attributeRequestHeaders), $codeRequestHeaders);
367+
if ($unusedRequestHeaders !== []) {
368+
Logger::error($context, 'Unused request header descriptions: ' . implode(', ', $unusedRequestHeaders));
369+
}
370+
371+
return new ControllerMethod($parameters, $attributeRequestHeaders, $responses, $responseDescriptions, $methodDescription, $methodSummary, $isDeprecated);
330372
}
331373

332374
}

tests/lib/Controller/SettingsController.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCP\AppFramework\Http\Attribute\IgnoreOpenAPI;
1616
use OCP\AppFramework\Http\Attribute\OpenAPI;
1717
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
18+
use OCP\AppFramework\Http\Attribute\RequestHeader;
1819
use OCP\AppFramework\Http\DataResponse;
1920
use OCP\AppFramework\OCS\OCSNotFoundException;
2021
use OCP\AppFramework\OCSController;
@@ -759,8 +760,16 @@ public function samePathPost(): DataResponse {
759760
*
760761
* 200: Admin settings updated
761762
*/
763+
#[RequestHeader('X-Custom-Header-1', 'A custom header 1')]
764+
#[RequestHeader('X-Custom-Header-2', description: 'A custom header 2')]
765+
#[RequestHeader(name: 'X-Custom-Header-3', description: 'A custom header 3')]
766+
#[RequestHeader(description: 'A custom header 4', name: 'X-Custom-Header-4')]
762767
public function requestHeader(): DataResponse {
763-
$this->request->getHeader('X-Custom-Header');
768+
$this->request->getHeader('X-Custom-Header-1');
769+
$this->request->getHeader('X-Custom-Header-2');
770+
$this->request->getHeader('X-Custom-Header-3');
771+
$this->request->getHeader('X-Custom-Header-4');
772+
$this->request->getHeader('X-Custom-Header-5');
764773

765774
return new DataResponse();
766775
}

tests/openapi-administration.json

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5472,7 +5472,39 @@
54725472
}
54735473
},
54745474
{
5475-
"name": "X-Custom-Header",
5475+
"name": "X-Custom-Header-1",
5476+
"in": "header",
5477+
"description": "A custom header 1",
5478+
"schema": {
5479+
"type": "string"
5480+
}
5481+
},
5482+
{
5483+
"name": "X-Custom-Header-2",
5484+
"in": "header",
5485+
"description": "A custom header 2",
5486+
"schema": {
5487+
"type": "string"
5488+
}
5489+
},
5490+
{
5491+
"name": "X-Custom-Header-3",
5492+
"in": "header",
5493+
"description": "A custom header 3",
5494+
"schema": {
5495+
"type": "string"
5496+
}
5497+
},
5498+
{
5499+
"name": "X-Custom-Header-4",
5500+
"in": "header",
5501+
"description": "A custom header 4",
5502+
"schema": {
5503+
"type": "string"
5504+
}
5505+
},
5506+
{
5507+
"name": "X-Custom-Header-5",
54765508
"in": "header",
54775509
"schema": {
54785510
"type": "string"

tests/openapi-full.json

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5629,7 +5629,39 @@
56295629
}
56305630
},
56315631
{
5632-
"name": "X-Custom-Header",
5632+
"name": "X-Custom-Header-1",
5633+
"in": "header",
5634+
"description": "A custom header 1",
5635+
"schema": {
5636+
"type": "string"
5637+
}
5638+
},
5639+
{
5640+
"name": "X-Custom-Header-2",
5641+
"in": "header",
5642+
"description": "A custom header 2",
5643+
"schema": {
5644+
"type": "string"
5645+
}
5646+
},
5647+
{
5648+
"name": "X-Custom-Header-3",
5649+
"in": "header",
5650+
"description": "A custom header 3",
5651+
"schema": {
5652+
"type": "string"
5653+
}
5654+
},
5655+
{
5656+
"name": "X-Custom-Header-4",
5657+
"in": "header",
5658+
"description": "A custom header 4",
5659+
"schema": {
5660+
"type": "string"
5661+
}
5662+
},
5663+
{
5664+
"name": "X-Custom-Header-5",
56335665
"in": "header",
56345666
"schema": {
56355667
"type": "string"

0 commit comments

Comments
 (0)