From fb82c9e35416921613a6ae633d9ef53d35f595b7 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Wed, 10 Jan 2024 16:27:55 +0100 Subject: [PATCH 1/2] refactor(routing): Allow unknown routes.php formats Signed-off-by: provokateurin --- src/Route.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Route.php b/src/Route.php index 6da01a0..2df871a 100644 --- a/src/Route.php +++ b/src/Route.php @@ -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(" self::includeRoutes(" self::includeRoutes(" Date: Mon, 15 Jan 2024 13:15:30 +0100 Subject: [PATCH 2/2] feat(routing): Support new Route attribute Signed-off-by: provokateurin --- generate-spec | 68 +++++++ tests/lib/Controller/RoutingController.php | 37 ++++ tests/openapi-administration.json | 216 +++++++++++++++++++++ tests/openapi-full.json | 216 +++++++++++++++++++++ 4 files changed, 537 insertions(+) create mode 100644 tests/lib/Controller/RoutingController.php diff --git a/generate-spec b/generate-spec index cd51c57..4354ab9 100755 --- a/generate-spec +++ b/generate-spec @@ -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_; @@ -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"; diff --git a/tests/lib/Controller/RoutingController.php b/tests/lib/Controller/RoutingController.php new file mode 100644 index 0000000..2e86511 --- /dev/null +++ b/tests/lib/Controller/RoutingController.php @@ -0,0 +1,37 @@ +, 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, 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(); + } +} diff --git a/tests/openapi-administration.json b/tests/openapi-administration.json index 61dc919..7aa37bc 100644 --- a/tests/openapi-administration.json +++ b/tests/openapi-administration.json @@ -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": [] diff --git a/tests/openapi-full.json b/tests/openapi-full.json index 175cdff..9230ef2 100644 --- a/tests/openapi-full.json +++ b/tests/openapi-full.json @@ -2455,6 +2455,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": {} + } + } + } + } + } + }, "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-admin-overwritten": { "post": { "operationId": "admin_settings-moved-to-default-scope",