Skip to content

Commit e5e9d6f

Browse files
committed
feat(generate-spec): Support controllers in sub-directories
Signed-off-by: provokateurin <[email protected]>
1 parent 051d3e5 commit e5e9d6f

File tree

6 files changed

+170
-8
lines changed

6 files changed

+170
-8
lines changed

generate-spec.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
}
1717

1818
use Ahc\Cli\Input\Command;
19-
use DirectoryIterator;
2019
use PhpParser\Node\AttributeGroup;
2120
use PhpParser\Node\Expr\ClassConstFetch;
2221
use PhpParser\Node\Expr\New_;
@@ -25,6 +24,8 @@
2524
use PhpParser\Node\Stmt\ClassMethod;
2625
use PhpParser\Node\Stmt\Throw_;
2726
use PhpParser\NodeFinder;
27+
use PhpParser\NodeTraverser;
28+
use PhpParser\NodeVisitor\NameResolver;
2829
use PhpParser\ParserFactory;
2930
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
3031
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
@@ -69,6 +70,9 @@
6970

7071
$astParser = (new ParserFactory())->createForNewestSupportedVersion();
7172
$nodeFinder = new NodeFinder;
73+
$nameResolver = new NameResolver;
74+
$nodeTraverser = new NodeTraverser;
75+
$nodeTraverser->addVisitor($nameResolver);
7276

7377
$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true, 'comments' => true]);
7478
$lexer = new Lexer($config);
@@ -83,10 +87,14 @@
8387
if ($xml === false) {
8488
Logger::panic('appinfo', 'info.xml file at ' . $infoXMLPath . ' is not parsable');
8589
}
90+
if (!$xml->namespace) {
91+
Logger::panic('appinfo', 'A <namespace/> must be set.');
92+
}
8693

8794
$appIsCore = false;
95+
$appNamespace = 'OCA\\' . $xml->namespace;
8896
$appID = (string)$xml->id;
89-
$readableAppID = $xml->namespace ? (string)$xml->namespace : Helpers::generateReadableAppID($appID);
97+
$readableAppID = (string)$xml->namespace;
9098
$appSummary = (string)$xml->summary;
9199
$appVersion = (string)$xml->version;
92100
$appLicence = (string)$xml->licence;
@@ -104,6 +112,7 @@
104112
}
105113

106114
$appIsCore = true;
115+
$appNamespace = 'OC\\Core';
107116
$appID = 'core';
108117
$readableAppID = 'Core';
109118
$appSummary = 'Core functionality of Nextcloud';
@@ -242,9 +251,9 @@
242251
$controllers = [];
243252
$controllersDir = $sourceDir . '/Controller';
244253
if (file_exists($controllersDir)) {
245-
$dir = new DirectoryIterator($controllersDir);
254+
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($controllersDir));
246255
$controllerFiles = [];
247-
foreach ($dir as $file) {
256+
foreach ($iterator as $file) {
248257
$filePath = $file->getPathname();
249258
if (!str_ends_with($filePath, 'Controller.php')) {
250259
continue;
@@ -254,7 +263,10 @@
254263
sort($controllerFiles);
255264

256265
foreach ($controllerFiles as $filePath) {
257-
$controllers[basename($filePath, 'Controller.php')] = $astParser->parse(file_get_contents($filePath));
266+
$offset = strlen($controllersDir . '/');
267+
$name = substr($filePath, $offset, strlen($filePath) - $offset - strlen('Controller.php'));
268+
$name = str_replace('/', '\\', $name);
269+
$controllers[$name] = $nodeTraverser->traverse($astParser->parse(file_get_contents($filePath)));
258270
}
259271
}
260272

@@ -263,7 +275,7 @@
263275
$controllerClass = null;
264276
/** @var Class_ $class */
265277
foreach ($nodeFinder->findInstanceOf($stmts, Class_::class) as $class) {
266-
if ($class->name->name === $controllerName . 'Controller') {
278+
if ($class->namespacedName->name === $appNamespace . '\\Controller\\' . $controllerName . 'Controller') {
267279
$controllerClass = $class;
268280
break;
269281
}
@@ -369,7 +381,7 @@
369381
$controllerClass = null;
370382
/** @var Class_ $class */
371383
foreach ($nodeFinder->findInstanceOf($controllers[$controllerName] ?? [], Class_::class) as $class) {
372-
if ($class->name == $controllerName . 'Controller') {
384+
if ($class->namespacedName->name === $appNamespace . '\\Controller\\' . $controllerName . 'Controller') {
373385
$controllerClass = $class;
374386
break;
375387
}
@@ -410,7 +422,7 @@
410422
Logger::panic($routeName, "Controller '" . $controllerName . "' is marked as ignore but also has other scopes");
411423
}
412424

413-
$tagName = implode('_', array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase($controllerName)));
425+
$tagName = implode('_', array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase(str_replace('\\', '', $controllerName))));
414426
$doc = $controllerClass->getDocComment()?->getText();
415427
if ($doc != null && count(array_filter($tags, fn (array $tag): bool => $tag['name'] === $tagName)) == 0) {
416428
$classDescription = [];

tests/appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
88
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
99
<id>notifications</id>
10+
<namespace>Notifications</namespace>
1011
<name>Notifications</name>
1112
<summary><![CDATA[This app provides a backend and frontend for the notification API available in Nextcloud.]]></summary>
1213
<description><![CDATA[This app provides a backend and frontend for the notification API available in Nextcloud.

tests/appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,6 @@
8585
['name' => 'Settings#deprecatedRouteAndParameterGet', 'url' => '/api/{apiVersion}/deprecated-route-parameter-get', 'verb' => 'GET', 'requirements' => ['apiVersion' => '(v2)']],
8686
['name' => 'Settings#samePathGet', 'url' => '/api/{apiVersion}/same-path', 'verb' => 'GET', 'requirements' => ['apiVersion' => '(v2)']],
8787
['name' => 'Settings#samePathPost', 'url' => '/api/{apiVersion}/same-path', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
88+
['name' => 'V1\SubDir#subDirRoute', 'url' => '/sub-dir', 'verb' => 'GET'],
8889
],
8990
];
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Notifications\Controller\V1;
11+
12+
use OCP\AppFramework\Http;
13+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
14+
use OCP\AppFramework\Http\Attribute\OpenAPI;
15+
use OCP\AppFramework\Http\DataResponse;
16+
use OCP\AppFramework\OCSController;
17+
18+
class SubDirController extends OCSController {
19+
/**
20+
* A route in a controller in a subdir.
21+
*
22+
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
23+
*
24+
* 200: Personal settings updated
25+
*/
26+
#[NoAdminRequired]
27+
public function subDirRoute(): DataResponse {
28+
return new DataResponse();
29+
}
30+
}

tests/openapi-full.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6533,6 +6533,65 @@
65336533
}
65346534
}
65356535
},
6536+
"/ocs/v2.php/apps/notifications/sub-dir": {
6537+
"get": {
6538+
"operationId": "v1_sub_dir-sub-dir-route",
6539+
"summary": "A route in a controller in a subdir.",
6540+
"tags": [
6541+
"v1_sub_dir"
6542+
],
6543+
"security": [
6544+
{
6545+
"bearer_auth": []
6546+
},
6547+
{
6548+
"basic_auth": []
6549+
}
6550+
],
6551+
"parameters": [
6552+
{
6553+
"name": "OCS-APIRequest",
6554+
"in": "header",
6555+
"description": "Required to be true for the API request to pass",
6556+
"required": true,
6557+
"schema": {
6558+
"type": "boolean",
6559+
"default": true
6560+
}
6561+
}
6562+
],
6563+
"responses": {
6564+
"200": {
6565+
"description": "Personal settings updated",
6566+
"content": {
6567+
"application/json": {
6568+
"schema": {
6569+
"type": "object",
6570+
"required": [
6571+
"ocs"
6572+
],
6573+
"properties": {
6574+
"ocs": {
6575+
"type": "object",
6576+
"required": [
6577+
"meta",
6578+
"data"
6579+
],
6580+
"properties": {
6581+
"meta": {
6582+
"$ref": "#/components/schemas/OCSMeta"
6583+
},
6584+
"data": {}
6585+
}
6586+
}
6587+
}
6588+
}
6589+
}
6590+
}
6591+
}
6592+
}
6593+
}
6594+
},
65366595
"/index.php/apps/notifications/plain/with-scope": {
65376596
"get": {
65386597
"operationId": "plain-with-scope",

tests/openapi.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,65 @@
994994
}
995995
}
996996
},
997+
"/ocs/v2.php/apps/notifications/sub-dir": {
998+
"get": {
999+
"operationId": "v1_sub_dir-sub-dir-route",
1000+
"summary": "A route in a controller in a subdir.",
1001+
"tags": [
1002+
"v1_sub_dir"
1003+
],
1004+
"security": [
1005+
{
1006+
"bearer_auth": []
1007+
},
1008+
{
1009+
"basic_auth": []
1010+
}
1011+
],
1012+
"parameters": [
1013+
{
1014+
"name": "OCS-APIRequest",
1015+
"in": "header",
1016+
"description": "Required to be true for the API request to pass",
1017+
"required": true,
1018+
"schema": {
1019+
"type": "boolean",
1020+
"default": true
1021+
}
1022+
}
1023+
],
1024+
"responses": {
1025+
"200": {
1026+
"description": "Personal settings updated",
1027+
"content": {
1028+
"application/json": {
1029+
"schema": {
1030+
"type": "object",
1031+
"required": [
1032+
"ocs"
1033+
],
1034+
"properties": {
1035+
"ocs": {
1036+
"type": "object",
1037+
"required": [
1038+
"meta",
1039+
"data"
1040+
],
1041+
"properties": {
1042+
"meta": {
1043+
"$ref": "#/components/schemas/OCSMeta"
1044+
},
1045+
"data": {}
1046+
}
1047+
}
1048+
}
1049+
}
1050+
}
1051+
}
1052+
}
1053+
}
1054+
}
1055+
},
9971056
"/index.php/apps/notifications/plain/with-scope": {
9981057
"get": {
9991058
"operationId": "plain-with-scope",

0 commit comments

Comments
 (0)