Skip to content

Commit

Permalink
Merge pull request #73 from nextcloud/feat/routing/route-attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
provokateurin authored Feb 21, 2024
2 parents d26a182 + 4771319 commit 7e39e24
Show file tree
Hide file tree
Showing 5 changed files with 547 additions and 9 deletions.
68 changes: 68 additions & 0 deletions generate-spec
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ foreach ([__DIR__ . "/../../autoload.php", __DIR__ . "/vendor/autoload.php"] as

use Ahc\Cli\Input\Command;
use DirectoryIterator;
use PhpParser\Node\AttributeGroup;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
Expand Down Expand Up @@ -249,6 +251,72 @@ if (file_exists($controllersDir)) {
}

$routes = [];
foreach ($controllers as $controllerName => $stmts) {
$controllerClass = null;
/** @var Class_ $class */
foreach ($nodeFinder->findInstanceOf($stmts, Class_::class) as $class) {
if ($class->name->name === $controllerName . 'Controller') {
$controllerClass = $class;
break;
}
}
if ($controllerClass === null) {
Logger::error($controllerName, "Controller '$controllerName' not found");
continue;
}

/** @var ClassMethod $classMethod */
foreach ($nodeFinder->findInstanceOf($controllerClass->stmts, ClassMethod::class) as $classMethod) {
$name = substr($class->name->name, 0, -strlen('Controller')) . '#' . $classMethod->name->name;

/** @var AttributeGroup $attrGroup */
foreach ($classMethod->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->getLast() !== 'Route' && $attr->name->getLast() !== 'ApiRoute' && $attr->name->getLast() !== 'FrontpageRoute') {
continue;
}

$key = match ($attr->name->getLast()) {
'Route' => null,
'ApiRoute' => 'ocs',
'FrontpageRoute' => 'routes',
};
$args = [
'name' => $name,
];
for ($i = 0, $iMax = count($attr->args); $i < $iMax; $i++) {
$arg = $attr->args[$i];

if ($arg->name !== null) {
$argName = $arg->name->name;
} else {
$argNames = ['verb', 'url', 'requirements', 'defaults', 'root', 'postfix'];
if ($attr->name->getLast() === 'Route') {
array_unshift($argNames, 'type');
}
$argName = $argNames[$i];
}

if ($argName === 'type' && $arg->value instanceof ClassConstFetch) {
$type = $arg->value->name->name;
$key = match ($type) {
'TYPE_API' => 'ocs',
'TYPE_FRONTPAGE' => 'routes',
default => Logger::panic($name, 'Unknown Route type: ' . $type),
};
continue;
}

$args[$argName] = Helpers::exprToValue($name, $arg->value);
}

$parsedRoutes[$key] ??= [];
$parsedRoutes[$key][] = $args;
}
}
}
}

foreach ($parsedRoutes as $key => $value) {
$isOCS = $key === "ocs";
$isIndex = $key === "routes";
Expand Down
19 changes: 10 additions & 9 deletions src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ public function __construct(

public static function parseRoutes(string $path): array {
$content = file_get_contents($path);
if (str_contains($content, "return ")) {
if (str_contains($content, "\$this")) {
preg_match("/return ([^;]*);/", $content, $matches);
return self::includeRoutes("<?php\nreturn " . $matches[1] . ";");
if (str_contains($content, 'return ')) {
if (str_contains($content, '$this')) {
preg_match('/return ([^;]*);/', $content, $matches);
return self::includeRoutes("<?php\nreturn ${matches[1]};");
}
return include($path);
} elseif (str_contains($content, "registerRoutes")) {
preg_match_all("/registerRoutes\(.*?\\\$this,.*?(\[[^;]*)\);/s", $content, $matches);
return array_merge(...array_map(fn (string $match) => self::includeRoutes("<?php\nreturn " . $match . ";"), $matches[1]));
} else {
Logger::panic("Routes", "Unknown routes.php format");
} elseif (str_contains($content, 'registerRoutes')) {
preg_match_all('/registerRoutes\(.*?\$this,.*?(\[[^;]*)\);/s', $content, $matches);
return array_merge(...array_map(fn (string $match) => self::includeRoutes("<?php\nreturn $match;"), $matches[1]));
}

Logger::warning('Routes', 'Unknown routes.php format');
return [];
}

private static function includeRoutes(string $code): array {
Expand Down
37 changes: 37 additions & 0 deletions tests/lib/Controller/RoutingController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace OCA\Notifications\Controller;

use OCP\AppFramework\Http\Attribute\Route;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;

class RoutingController extends OCSController {
/**
* OCS Route with attribute
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
*
* 200: Success
*/
#[Route(Route::TYPE_API, verb: 'GET', url: '/attribute-ocs/{param}', requirements: ['param' => '[a-z]+'], defaults: ['param' => 'abc'], root: '/tests', postfix: 'Route')]
#[ApiRoute(verb: 'POST', url: '/attribute-ocs/{param}', requirements: ['param' => '[a-z]+'], defaults: ['param' => 'abc'], root: '/tests', postfix: 'ApiRoute')]
public function attributeOCS() {
return DataResponse();
}

/**
* @NoCSRFRequired
*
* Index Route with attribute
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
*
* 200: Success
*/
#[Route(Route::TYPE_FRONTPAGE, verb: 'GET', url: '/attribute-index/{param}', requirements: ['param' => '[a-z]+'], defaults: ['param' => 'abc'], root: '/tests', postfix: 'Route')]
#[FrontpageRoute(verb: 'POST', url: '/attribute-index/{param}', requirements: ['param' => '[a-z]+'], defaults: ['param' => 'abc'], root: '/tests', postfix: 'FrontpageRoute')]
public function attributeIndex() {
return DataResponse();
}
}
216 changes: 216 additions & 0 deletions tests/openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -2327,6 +2327,222 @@
}
}
}
},
"/ocs/v2.php/tests/attribute-ocs/{param}": {
"get": {
"operationId": "routing-attributeocs-route",
"summary": "OCS Route with attribute",
"description": "This endpoint requires admin access",
"tags": [
"routing"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "param",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z]+$",
"default": "abc"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
},
"post": {
"operationId": "routing-attributeocs-apiroute",
"summary": "OCS Route with attribute",
"description": "This endpoint requires admin access",
"tags": [
"routing"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "param",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z]+$",
"default": "abc"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/index.php/tests/attribute-index/{param}": {
"get": {
"operationId": "routing-attribute-index-route",
"summary": "Index Route with attribute",
"description": "This endpoint requires admin access",
"tags": [
"routing"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "param",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z]+$",
"default": "abc"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
}
}
},
"post": {
"operationId": "routing-attribute-index-frontpageroute",
"summary": "Index Route with attribute",
"description": "This endpoint requires admin access",
"tags": [
"routing"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "param",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z]+$",
"default": "abc"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
},
"tags": []
Expand Down
Loading

0 comments on commit 7e39e24

Please sign in to comment.