Skip to content

Commit eec3922

Browse files
Merge pull request #233 from nextcloud/feat/generate-spec/sub-dir-controllers
2 parents 051d3e5 + 0e8d6d3 commit eec3922

File tree

5 files changed

+167
-8
lines changed

5 files changed

+167
-8
lines changed

generate-spec.php

Lines changed: 19 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);
@@ -84,9 +88,12 @@
8488
Logger::panic('appinfo', 'info.xml file at ' . $infoXMLPath . ' is not parsable');
8589
}
8690

91+
$rawNamespace = $xml->namespace ?? ucfirst($xml->id);
92+
8793
$appIsCore = false;
94+
$appNamespace = 'OCA\\' . $rawNamespace;
8895
$appID = (string)$xml->id;
89-
$readableAppID = $xml->namespace ? (string)$xml->namespace : Helpers::generateReadableAppID($appID);
96+
$readableAppID = (string)$rawNamespace;
9097
$appSummary = (string)$xml->summary;
9198
$appVersion = (string)$xml->version;
9299
$appLicence = (string)$xml->licence;
@@ -104,6 +111,7 @@
104111
}
105112

106113
$appIsCore = true;
114+
$appNamespace = 'OC\\Core';
107115
$appID = 'core';
108116
$readableAppID = 'Core';
109117
$appSummary = 'Core functionality of Nextcloud';
@@ -242,9 +250,9 @@
242250
$controllers = [];
243251
$controllersDir = $sourceDir . '/Controller';
244252
if (file_exists($controllersDir)) {
245-
$dir = new DirectoryIterator($controllersDir);
253+
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($controllersDir));
246254
$controllerFiles = [];
247-
foreach ($dir as $file) {
255+
foreach ($iterator as $file) {
248256
$filePath = $file->getPathname();
249257
if (!str_ends_with($filePath, 'Controller.php')) {
250258
continue;
@@ -254,7 +262,10 @@
254262
sort($controllerFiles);
255263

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

@@ -263,7 +274,7 @@
263274
$controllerClass = null;
264275
/** @var Class_ $class */
265276
foreach ($nodeFinder->findInstanceOf($stmts, Class_::class) as $class) {
266-
if ($class->name->name === $controllerName . 'Controller') {
277+
if ($class->namespacedName->name === $appNamespace . '\\Controller\\' . $controllerName . 'Controller') {
267278
$controllerClass = $class;
268279
break;
269280
}
@@ -369,7 +380,7 @@
369380
$controllerClass = null;
370381
/** @var Class_ $class */
371382
foreach ($nodeFinder->findInstanceOf($controllers[$controllerName] ?? [], Class_::class) as $class) {
372-
if ($class->name == $controllerName . 'Controller') {
383+
if ($class->namespacedName->name === $appNamespace . '\\Controller\\' . $controllerName . 'Controller') {
373384
$controllerClass = $class;
374385
break;
375386
}
@@ -410,7 +421,7 @@
410421
Logger::panic($routeName, "Controller '" . $controllerName . "' is marked as ignore but also has other scopes");
411422
}
412423

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

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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\DataResponse;
15+
use OCP\AppFramework\OCSController;
16+
17+
class SubDirController extends OCSController {
18+
/**
19+
* A route in a controller in a subdir.
20+
*
21+
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
22+
*
23+
* 200: Personal settings updated
24+
*/
25+
#[NoAdminRequired]
26+
public function subDirRoute(): DataResponse {
27+
return new DataResponse();
28+
}
29+
}

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)