From 09935651ae08ae251b9dd484c0e343d951457e30 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 15:47:54 +0100 Subject: [PATCH 01/29] feat(scopes): Allow apps to define different API scopes for different target clients Signed-off-by: Joas Schilling --- generate-spec | 436 +++++++++++++++++++++++++++--------------------- src/Helpers.php | 36 ++++ 2 files changed, 279 insertions(+), 193 deletions(-) diff --git a/generate-spec b/generate-spec index a66d8a4..d43902e 100755 --- a/generate-spec +++ b/generate-spec @@ -302,6 +302,12 @@ foreach ($parsedRoutes as $key => $value) { continue; } + $controllerScope = Helpers::getAttributeScope($controllerClass, 'OpenAPI', $routeName); + if ($controllerScope === 'ignore') { + Logger::info($routeName, "Controller '" . $controllerName . "' ignored because of OpenAPI attribute"); + continue; + } + $tagName = implode("_", array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase($controllerName))); $doc = $controllerClass->getDocComment()?->getText(); if ($doc != null && count(array_filter($tags, fn (array $tag) => $tag["name"] == $tagName)) == 0) { @@ -355,6 +361,20 @@ foreach ($parsedRoutes as $key => $value) { continue; } + $scope = Helpers::getAttributeScope($classMethod, 'OpenAPI', $routeName); + if ($scope === 'ignore') { + Logger::info($routeName, "Route ignored because of OpenAPI attribute"); + continue; + } + + if ($scope === null) { + if ($controllerScope !== null) { + $scope = $controllerScope; + } else { + $scope = 'default'; + } + } + if ($isOCS && !array_key_exists("OCSMeta", $schemas)) { $schemas["OCSMeta"] = [ "type" => "object", @@ -402,7 +422,8 @@ foreach ($parsedRoutes as $key => $value) { continue; } - $routes[] = new Route( + $routes[$scope] ??= []; + $routes[$scope][] = new Route( $routeName, $tagName, $methodName, @@ -424,228 +445,238 @@ foreach ($parsedRoutes as $key => $value) { $tagNames = []; if ($useTags) { - foreach ($routes as $route) { - if (!in_array($route->tag, $tagNames)) { - $tagNames[] = $route->tag; + foreach ($routes as $scope => $scopeRoutes) { + foreach ($scopeRoutes as $route) { + if (!in_array($route->tag, $tagNames)) { + $tagNames[] = $route->tag; + } } } } -foreach ($routes as $route) { - $pathParameters = []; - $urlParameters = []; - - preg_match_all("/{[^}]*}/", $route->url, $urlParameters); - $urlParameters = array_map(fn (string $name) => substr($name, 1, -1), $urlParameters[0]); - - foreach ($urlParameters as $urlParameter) { - $matchingParameters = array_filter($route->controllerMethod->parameters, function (ControllerMethodParameter $param) use ($urlParameter) { - return $param->name == $urlParameter; - }); - $requirement = array_key_exists($urlParameter, $route->requirements) ? $route->requirements[$urlParameter] : null; - if (count($matchingParameters) == 1) { - $parameter = $matchingParameters[array_keys($matchingParameters)[0]]; - if ($parameter?->methodParameter == null && ($route->requirements == null || !array_key_exists($urlParameter, $route->requirements))) { - Logger::error($route->name, "Unable to find parameter for '" . $urlParameter . "'"); - continue; - } - - $schema = $parameter->type->toArray($openapiVersion, true); - $description = $parameter?->docType != null && $parameter->docType->description != "" ? Helpers::cleanDocComment($parameter->docType->description) : null; - } else { - $schema = [ - "type" => "string", - ]; - $description = null; - } +$scopePaths = []; + +foreach ($routes as $scope => $scopeRoutes) { + foreach ($scopeRoutes as $route) { + $pathParameters = []; + $urlParameters = []; + + preg_match_all("/{[^}]*}/", $route->url, $urlParameters); + $urlParameters = array_map(fn (string $name) => substr($name, 1, -1), $urlParameters[0]); + + foreach ($urlParameters as $urlParameter) { + $matchingParameters = array_filter($route->controllerMethod->parameters, function (ControllerMethodParameter $param) use ($urlParameter) { + return $param->name == $urlParameter; + }); + $requirement = array_key_exists($urlParameter, $route->requirements) ? $route->requirements[$urlParameter] : null; + if (count($matchingParameters) == 1) { + $parameter = $matchingParameters[array_keys($matchingParameters)[0]]; + if ($parameter?->methodParameter == null && ($route->requirements == null || !array_key_exists($urlParameter, $route->requirements))) { + Logger::error($route->name, "Unable to find parameter for '" . $urlParameter . "'"); + continue; + } - if ($requirement != null) { - if (!str_starts_with($requirement, "^")) { - $requirement = "^" . $requirement; - } - if (!str_ends_with($requirement, "$")) { - $requirement = $requirement . "$"; + $schema = $parameter->type->toArray($openapiVersion, true); + $description = $parameter?->docType != null && $parameter->docType->description != "" ? Helpers::cleanDocComment($parameter->docType->description) : null; + } else { + $schema = [ + "type" => "string", + ]; + $description = null; } - } - if ($schema["type"] == "string") { - if ($urlParameter == "apiVersion") { - if ($requirement == null) { - Logger::error($route->name, "Missing requirement for apiVersion"); - continue; + if ($requirement != null) { + if (!str_starts_with($requirement, "^")) { + $requirement = "^" . $requirement; } - preg_match("/^\^\(([v0-9-.|]*)\)\\$$/m", $requirement, $matches); - if (count($matches) == 2) { - $enum = explode("|", $matches[1]); - } else { - Logger::error($route->name, "Invalid requirement for apiVersion"); - continue; + if (!str_ends_with($requirement, "$")) { + $requirement = $requirement . "$"; } - $schema["enum"] = $enum; - $schema["default"] = end($enum); - } elseif ($requirement != null) { - $schema["pattern"] = $requirement; } - } - if (array_key_exists($urlParameter, $route->defaults)) { - $schema["default"] = $route->defaults[$urlParameter]; - } - - $pathParameters[] = array_merge( - [ - "name" => $urlParameter, - "in" => "path", - ], - $description != null ? ["description" => $description] : [], - [ - "required" => true, - "schema" => $schema, - ], - ); - } + if ($schema["type"] == "string") { + if ($urlParameter == "apiVersion") { + if ($requirement == null) { + Logger::error($route->name, "Missing requirement for apiVersion"); + continue; + } + preg_match("/^\^\(([v0-9-.|]*)\)\\$$/m", $requirement, $matches); + if (count($matches) == 2) { + $enum = explode("|", $matches[1]); + } else { + Logger::error($route->name, "Invalid requirement for apiVersion"); + continue; + } + $schema["enum"] = $enum; + $schema["default"] = end($enum); + } elseif ($requirement != null) { + $schema["pattern"] = $requirement; + } + } - $queryParameters = []; - foreach ($route->controllerMethod->parameters as $parameter) { - $alreadyInPath = false; - foreach ($pathParameters as $pathParameter) { - if ($pathParameter["name"] == $parameter->name) { - $alreadyInPath = true; - break; + if (array_key_exists($urlParameter, $route->defaults)) { + $schema["default"] = $route->defaults[$urlParameter]; } - } - if (!$alreadyInPath) { - $queryParameters[] = $parameter; - } - } - $mergedResponses = []; - foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->statusCode, array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response) => $response != null))) as $statusCode) { - if ($firstStatusCode && count($mergedResponses) > 0) { - break; + $pathParameters[] = array_merge( + [ + "name" => $urlParameter, + "in" => "path", + ], + $description != null ? ["description" => $description] : [], + [ + "required" => true, + "schema" => $schema, + ], + ); } - $statusCodeResponses = array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response) => $response != null && $response->statusCode == $statusCode); - $headers = []; - foreach ($statusCodeResponses as $response) { - if ($response->headers !== null) { - $headers = array_merge($headers, $response->headers); + $queryParameters = []; + foreach ($route->controllerMethod->parameters as $parameter) { + $alreadyInPath = false; + foreach ($pathParameters as $pathParameter) { + if ($pathParameter["name"] == $parameter->name) { + $alreadyInPath = true; + break; + } + } + if (!$alreadyInPath) { + $queryParameters[] = $parameter; } } - $mergedContentTypeResponses = []; - foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->contentType, array_filter($statusCodeResponses, fn (ControllerMethodResponse $response) => $response->contentType != null))) as $contentType) { - if ($firstContentType && count($mergedContentTypeResponses) > 0) { + $mergedResponses = []; + foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->statusCode, array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response) => $response != null))) as $statusCode) { + if ($firstStatusCode && count($mergedResponses) > 0) { break; } - /** @var ControllerMethodResponse[] $contentTypeResponses */ - $contentTypeResponses = array_values(array_filter($statusCodeResponses, fn (ControllerMethodResponse $response) => $response->contentType == $contentType)); + $statusCodeResponses = array_filter($route->controllerMethod->responses, fn (?ControllerMethodResponse $response) => $response != null && $response->statusCode == $statusCode); + $headers = []; + foreach ($statusCodeResponses as $response) { + if ($response->headers !== null) { + $headers = array_merge($headers, $response->headers); + } + } + + $mergedContentTypeResponses = []; + foreach (array_unique(array_map(fn (ControllerMethodResponse $response) => $response->contentType, array_filter($statusCodeResponses, fn (ControllerMethodResponse $response) => $response->contentType != null))) as $contentType) { + if ($firstContentType && count($mergedContentTypeResponses) > 0) { + break; + } - $hasEmpty = count(array_filter($contentTypeResponses, fn (ControllerMethodResponse $response) => $response->type == null)) > 0; - $uniqueResponses = array_values(array_intersect_key($contentTypeResponses, array_unique(array_map(fn (ControllerMethodResponse $response) => $response->type->toArray($openapiVersion), array_filter($contentTypeResponses, fn (ControllerMethodResponse $response) => $response->type != null)), SORT_REGULAR))); - if (count($uniqueResponses) == 1) { - if ($hasEmpty) { - $mergedContentTypeResponses[$contentType] = []; + /** @var ControllerMethodResponse[] $contentTypeResponses */ + $contentTypeResponses = array_values(array_filter($statusCodeResponses, fn (ControllerMethodResponse $response) => $response->contentType == $contentType)); + + $hasEmpty = count(array_filter($contentTypeResponses, fn (ControllerMethodResponse $response) => $response->type == null)) > 0; + $uniqueResponses = array_values(array_intersect_key($contentTypeResponses, array_unique(array_map(fn (ControllerMethodResponse $response) => $response->type->toArray($openapiVersion), array_filter($contentTypeResponses, fn (ControllerMethodResponse $response) => $response->type != null)), SORT_REGULAR))); + if (count($uniqueResponses) == 1) { + if ($hasEmpty) { + $mergedContentTypeResponses[$contentType] = []; + } else { + $schema = Helpers::cleanEmptyResponseArray($contentTypeResponses[0]->type->toArray($openapiVersion)); + $mergedContentTypeResponses[$contentType] = ["schema" => Helpers::wrapOCSResponse($route, $contentTypeResponses[0], $schema)]; + } } else { - $schema = Helpers::cleanEmptyResponseArray($contentTypeResponses[0]->type->toArray($openapiVersion)); - $mergedContentTypeResponses[$contentType] = ["schema" => Helpers::wrapOCSResponse($route, $contentTypeResponses[0], $schema)]; + $mergedContentTypeResponses[$contentType] = [ + "schema" => [ + [$hasEmpty ? "anyOf" : "oneOf" => array_map(function (ControllerMethodResponse $response) use ($route, $openapiVersion) { + $schema = Helpers::cleanEmptyResponseArray($response->type->toArray($openapiVersion)); + return Helpers::wrapOCSResponse($route, $response, $schema); + }, $uniqueResponses)], + ], + ]; } - } else { - $mergedContentTypeResponses[$contentType] = [ - "schema" => [ - [$hasEmpty ? "anyOf" : "oneOf" => array_map(function (ControllerMethodResponse $response) use ($route, $openapiVersion) { - $schema = Helpers::cleanEmptyResponseArray($response->type->toArray($openapiVersion)); - return Helpers::wrapOCSResponse($route, $response, $schema); - }, $uniqueResponses)], - ], - ]; } - } - $mergedResponses[$statusCode] = array_merge( - [ - "description" => array_key_exists($statusCode, $route->controllerMethod->responseDescription) ? $route->controllerMethod->responseDescription[$statusCode] : "", - ], - count($headers) > 0 ? [ - "headers" => array_combine( - array_keys($headers), - array_map( - fn (OpenApiType $type) => [ - "schema" => $type->toArray($openapiVersion), - ], - array_values($headers), + $mergedResponses[$statusCode] = array_merge( + [ + "description" => array_key_exists($statusCode, $route->controllerMethod->responseDescription) ? $route->controllerMethod->responseDescription[$statusCode] : "", + ], + count($headers) > 0 ? [ + "headers" => array_combine( + array_keys($headers), + array_map( + fn (OpenApiType $type) => [ + "schema" => $type->toArray($openapiVersion), + ], + array_values($headers), + ), ), + ] : [], + count($mergedContentTypeResponses) > 0 ? [ + "content" => $mergedContentTypeResponses, + ] : [], + ); + } + + $operationId = [$route->tag, ...Helpers::splitOnUppercaseFollowedByNonUppercase($route->methodName)]; + if ($route->postfix != null) { + $operationId[] = $route->postfix; + } + + $security = []; + if ($route->isPublic) { + // Add empty authentication, meaning that it's optional. We can't know if there is a difference in behaviour for authenticated vs. unauthenticated access on public pages (e.g. capabilities) + $security[] = new stdClass(); + } + if (!$route->isCORS) { + // Bearer auth is not allowed on CORS routes + $security[] = ["bearer_auth" => []]; + } + if (!$route->isCSRFRequired || $route->isOCS) { + // Add basic auth last, so it's only fallback if bearer is available + $security[] = ["basic_auth" => []]; + } + + $operation = array_merge( + ["operationId" => strtolower(implode("-", $operationId))], + $route->controllerMethod->summary != null ? ["summary" => $route->controllerMethod->summary] : [], + count($route->controllerMethod->description) > 0 ? ["description" => implode("\n", $route->controllerMethod->description)] : [], + $route->controllerMethod->isDeprecated ? ["deprecated" => true] : [], + $useTags ? ["tags" => [$route->tag]] : [], + count($security) > 0 ? ["security" => $security] : [], + count($queryParameters) > 0 || count($pathParameters) > 0 || $route->isOCS ? [ + "parameters" => array_merge( + array_map(fn (ControllerMethodParameter $parameter) => array_merge( + [ + "name" => $parameter->name . ($parameter->type->type == "array" ? "[]" : ""), + "in" => "query", + ], + $parameter->docType != null && $parameter->docType->description != "" ? ["description" => Helpers::cleanDocComment($parameter->docType->description)] : [], + !$parameter->type->nullable && !$parameter->type->hasDefaultValue ? ["required" => true] : [], + ["schema" => $parameter->type->toArray($openapiVersion, true),], + ), $queryParameters), + $pathParameters, + $route->isOCS ? [[ + "name" => "OCS-APIRequest", + "in" => "header", + "description" => "Required to be true for the API request to pass", + "required" => true, + "schema" => [ + "type" => "boolean", + "default" => true, + ], + ]] : [], ), ] : [], - count($mergedContentTypeResponses) > 0 ? [ - "content" => $mergedContentTypeResponses, - ] : [], + ["responses" => $mergedResponses], ); - } - $operationId = [$route->tag, ...Helpers::splitOnUppercaseFollowedByNonUppercase($route->methodName)]; - if ($route->postfix != null) { - $operationId[] = $route->postfix; - } + $scopePaths[$scope] ??= []; + $scopePaths[$scope][$route->url] ??= []; - $security = []; - if ($route->isPublic) { - // Add empty authentication, meaning that it's optional. We can't know if there is a difference in behaviour for authenticated vs. unauthenticated access on public pages (e.g. capabilities) - $security[] = new stdClass(); - } - if (!$route->isCORS) { - // Bearer auth is not allowed on CORS routes - $security[] = ["bearer_auth" => []]; - } - if (!$route->isCSRFRequired || $route->isOCS) { - // Add basic auth last, so it's only fallback if bearer is available - $security[] = ["basic_auth" => []]; - } + if (!array_key_exists($route->url, $openapi["paths"])) { + $openapi["paths"][$route->url] = []; + } - $operation = array_merge( - ["operationId" => strtolower(implode("-", $operationId))], - $route->controllerMethod->summary != null ? ["summary" => $route->controllerMethod->summary] : [], - count($route->controllerMethod->description) > 0 ? ["description" => implode("\n", $route->controllerMethod->description)] : [], - $route->controllerMethod->isDeprecated ? ["deprecated" => true] : [], - $useTags ? ["tags" => [$route->tag]] : [], - count($security) > 0 ? ["security" => $security] : [], - count($queryParameters) > 0 || count($pathParameters) > 0 || $route->isOCS ? [ - "parameters" => array_merge( - array_map(fn (ControllerMethodParameter $parameter) => array_merge( - [ - "name" => $parameter->name . ($parameter->type->type == "array" ? "[]" : ""), - "in" => "query", - ], - $parameter->docType != null && $parameter->docType->description != "" ? ["description" => Helpers::cleanDocComment($parameter->docType->description)] : [], - !$parameter->type->nullable && !$parameter->type->hasDefaultValue ? ["required" => true] : [], - ["schema" => $parameter->type->toArray($openapiVersion, true),], - ), $queryParameters), - $pathParameters, - $route->isOCS ? [[ - "name" => "OCS-APIRequest", - "in" => "header", - "description" => "Required to be true for the API request to pass", - "required" => true, - "schema" => [ - "type" => "boolean", - "default" => true, - ], - ]] : [], - ), - ] : [], - ["responses" => $mergedResponses], - ); - if (!array_key_exists($route->url, $openapi["paths"])) { - $openapi["paths"][$route->url] = []; - } + $verb = strtolower($route->verb); + if (array_key_exists($verb, $scopePaths[$scope][$route->url])) { + Logger::error($route->name, "Operation '" . $route->verb . "' already set for path '" . $route->url . "'"); + } - $verb = strtolower($route->verb); - if (array_key_exists($verb, $openapi["paths"][$route->url])) { - Logger::error($route->name, "Operation '" . $route->verb . "' already set for path '" . $route->url . "'"); + $scopePaths[$scope][$route->url][$verb] = $operation; } - - $openapi["paths"][$route->url][$verb] = $operation; } if ($appIsCore) { @@ -688,7 +719,7 @@ if ($appIsCore) { ], ], ]; - $openapi["paths"]["/status.php"] = [ + $scopePaths['default']['/status.php'] = [ "get" => [ "operationId" => "get-status", "responses" => [ @@ -711,10 +742,6 @@ if (count($schemas) == 0 && count($routes) == 0) { Logger::error("app", "No spec generated"); } -if (count($openapi["paths"]) == 0) { - $openapi["paths"] = new stdClass(); -} - ksort($schemas); $openapi["components"]["schemas"] = count($schemas) == 0 ? new stdClass() : $schemas; @@ -722,6 +749,29 @@ if ($useTags) { $openapi["tags"] = $tags; } -file_put_contents($out, json_encode($openapi, Helpers::jsonFlags())); +foreach ($scopePaths as $scope => $paths) { + $openapiScope = $openapi; + + if (count($paths) == 0) { + $paths = new stdClass(); + } + + $scopeSuffix = $scope === 'default' ? '' : '-' . $scope; + $openapiScope['info']['title'] .= $scopeSuffix; + $openapiScope['paths'] = $paths; + + $startExtension = strrpos($out, '.'); + if ($startExtension !== false) { + // Path + filename (without extension) + $path = substr($out, 0, $startExtension); + // Extension + $extension = substr($out, $startExtension); + $scopeOut = $path . $scopeSuffix . $extension; + } else { + $scopeOut = $out . $scopeSuffix; + } + + file_put_contents($scopeOut, json_encode($openapiScope, Helpers::jsonFlags())); +} Logger::info("app", "Generated ". count($routes). " routes!"); diff --git a/src/Helpers.php b/src/Helpers.php index f4076d1..0d5a31b 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -4,6 +4,8 @@ use Exception; use PhpParser\Node; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use stdClass; @@ -149,4 +151,38 @@ public static function cleanSchemaName(string $name): string { global $readableAppID; return substr($name, strlen($readableAppID)); } + static function getAttributeScope(ClassMethod|Class_|Node $node, string $annotation, string $routeName): ?string { + /** @var Node\AttributeGroup $attrGroup */ + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->getLast() === $annotation) { + if (empty($attr->args)) { + return 'default'; + } + + foreach ($attr->args as $arg) { + if ($arg->name->name === 'scope') { + if ($arg->value instanceof ClassConstFetch) { + if ($arg->value->class->getLast() === 'OpenAPI') { + return match ($arg->value->name->name) { + 'SCOPE_DEFAULT' => 'default', + 'SCOPE_ADMINISTRATION' => 'administration', + 'SCOPE_FEDERATION' => 'federation', + 'SCOPE_IGNORE' => 'ignore', + // Fall back for future scopes assuming we follow the pattern (cut of 'SCOPE_' and lower case) + default => strtolower(substr($arg->value->name->name, 6)), + }; + } + } elseif ($arg->value instanceof String_) { + return $arg->value->value; + } + Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); + } + } + } + } + } + + return null; + } } From 36ab954369460b7aec03052bd2e2142cf58c67ad Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 15:52:02 +0100 Subject: [PATCH 02/29] feat(scopes): Allow multiple scopes Signed-off-by: Joas Schilling --- generate-spec | 50 +++++++++++++++++++++++++------------------------ src/Helpers.php | 16 ++++++++++------ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/generate-spec b/generate-spec index d43902e..ad58d17 100755 --- a/generate-spec +++ b/generate-spec @@ -302,8 +302,8 @@ foreach ($parsedRoutes as $key => $value) { continue; } - $controllerScope = Helpers::getAttributeScope($controllerClass, 'OpenAPI', $routeName); - if ($controllerScope === 'ignore') { + $controllerScopes = Helpers::getAttributeScopes($controllerClass, 'OpenAPI', $routeName); + if (in_array('ignore', $controllerScopes, true)) { Logger::info($routeName, "Controller '" . $controllerName . "' ignored because of OpenAPI attribute"); continue; } @@ -361,17 +361,17 @@ foreach ($parsedRoutes as $key => $value) { continue; } - $scope = Helpers::getAttributeScope($classMethod, 'OpenAPI', $routeName); - if ($scope === 'ignore') { + $scopes = Helpers::getAttributeScopes($classMethod, 'OpenAPI', $routeName); + if (in_array('ignore', $scopes, true)) { Logger::info($routeName, "Route ignored because of OpenAPI attribute"); continue; } - if ($scope === null) { - if ($controllerScope !== null) { - $scope = $controllerScope; + if (empty($scopes)) { + if (!empty($controllerScopes)) { + $scopes = $controllerScopes; } else { - $scope = 'default'; + $scopes = ['default']; } } @@ -422,22 +422,24 @@ foreach ($parsedRoutes as $key => $value) { continue; } - $routes[$scope] ??= []; - $routes[$scope][] = new Route( - $routeName, - $tagName, - $methodName, - $postfix, - $verb, - $url, - $requirements, - $defaults, - $classMethodInfo, - $isOCS, - $isCORS, - $isCSRFRequired, - $isPublic, - ); + foreach ($scopes as $scope) { + $routes[$scope] ??= []; + $routes[$scope][] = new Route( + $routeName, + $tagName, + $methodName, + $postfix, + $verb, + $url, + $requirements, + $defaults, + $classMethodInfo, + $isOCS, + $isCORS, + $isCSRFRequired, + $isPublic, + ); + } Logger::debug($routeName, "Route generated"); } diff --git a/src/Helpers.php b/src/Helpers.php index 0d5a31b..6881db6 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -151,20 +151,23 @@ public static function cleanSchemaName(string $name): string { global $readableAppID; return substr($name, strlen($readableAppID)); } - static function getAttributeScope(ClassMethod|Class_|Node $node, string $annotation, string $routeName): ?string { + + static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annotation, string $routeName): array { + $scopes = []; + /** @var Node\AttributeGroup $attrGroup */ foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->getLast() === $annotation) { if (empty($attr->args)) { - return 'default'; + $scopes[] = 'default'; } foreach ($attr->args as $arg) { if ($arg->name->name === 'scope') { if ($arg->value instanceof ClassConstFetch) { if ($arg->value->class->getLast() === 'OpenAPI') { - return match ($arg->value->name->name) { + $scopes[] = match ($arg->value->name->name) { 'SCOPE_DEFAULT' => 'default', 'SCOPE_ADMINISTRATION' => 'administration', 'SCOPE_FEDERATION' => 'federation', @@ -174,15 +177,16 @@ static function getAttributeScope(ClassMethod|Class_|Node $node, string $annotat }; } } elseif ($arg->value instanceof String_) { - return $arg->value->value; + $scopes[] = $arg->value->value; + } else { + Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); } - Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); } } } } } - return null; + return $scopes; } } From fff1bd9e635235618e532985a71a2dc4eecb79df Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 15:53:22 +0100 Subject: [PATCH 03/29] fix(scopes): Only list schemas that are used in this Scope Signed-off-by: Joas Schilling --- generate-spec | 34 +++++++++++++++++++++++++++++++++- src/Helpers.php | 15 +++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/generate-spec b/generate-spec index ad58d17..7ddefdf 100755 --- a/generate-spec +++ b/generate-spec @@ -745,7 +745,6 @@ if (count($schemas) == 0 && count($routes) == 0) { } ksort($schemas); -$openapi["components"]["schemas"] = count($schemas) == 0 ? new stdClass() : $schemas; if ($useTags) { $openapi["tags"] = $tags; @@ -762,6 +761,39 @@ foreach ($scopePaths as $scope => $paths) { $openapiScope['info']['title'] .= $scopeSuffix; $openapiScope['paths'] = $paths; + $usedSchemas = []; + foreach ($paths as $url => $urlRoutes) { + foreach ($urlRoutes as $httpMethod => $routeData) { + foreach ($routeData['responses'] as $statusCode => $responseData) { + $usedSchemas = array_merge($usedSchemas, Helpers::collectUsedRefs($responseData['content']['application/json']['schema'])); + } + } + } + + $scopedSchemas = []; + foreach ($usedSchemas as $usedSchema) { + if (!str_starts_with($usedSchema, '#/components/schemas/')) { + continue; + } + + $schemaName = substr($usedSchema, strlen('#/components/schemas/')); + + if (!isset($schemas[$schemaName])) { + Logger::error("app", "Schema $schemaName used by scope $scope is not defined"); + } + + $scopedSchemas[$schemaName] = $schemas[$schemaName]; + } + + if (isset($schemas['Capabilities'])) { + $scopedSchemas['Capabilities'] = $schemas['Capabilities']; + } + if (isset($schemas['PublicCapabilities'])) { + $scopedSchemas['PublicCapabilities'] = $schemas['PublicCapabilities']; + } + + $openapiScope['components']['schemas'] = $scopedSchemas; + $startExtension = strrpos($out, '.'); if ($startExtension !== false) { // Path + filename (without extension) diff --git a/src/Helpers.php b/src/Helpers.php index 6881db6..5120c16 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -189,4 +189,19 @@ static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annota return $scopes; } + + public static function collectUsedRefs(array $data): array { + $refs = []; + if (isset($data['$ref'])) { + $refs[] = [$data['$ref']]; + } + if (isset($data['properties'])) { + foreach ($data['properties'] as $property) { + if (is_array($property)) { + $refs[] = self::collectUsedRefs($property); + } + } + } + return array_merge(...$refs); + } } From 288914d384f9e8903739f392ac21c8a6b4250ea2 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 15:54:06 +0100 Subject: [PATCH 04/29] feat(scopes): Move admin-only routes to administration scope when default only Signed-off-by: Joas Schilling --- generate-spec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/generate-spec b/generate-spec index 7ddefdf..aae9be6 100755 --- a/generate-spec +++ b/generate-spec @@ -370,6 +370,8 @@ foreach ($parsedRoutes as $key => $value) { if (empty($scopes)) { if (!empty($controllerScopes)) { $scopes = $controllerScopes; + } elseif ($isAdmin) { + $scopes = ['administration']; } else { $scopes = ['default']; } From e065a7cb81c58a810749805b17eac9dbea0a719e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 15:56:59 +0100 Subject: [PATCH 05/29] feat(scopes): Handle tags defined by the scopes Signed-off-by: Joas Schilling --- generate-spec | 14 +++++++----- src/Helpers.php | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Route.php | 2 +- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/generate-spec b/generate-spec index aae9be6..134dcad 100755 --- a/generate-spec +++ b/generate-spec @@ -377,6 +377,8 @@ foreach ($parsedRoutes as $key => $value) { } } + $routeTags = Helpers::getAttributeTagsByScope($classMethod, 'OpenAPI', $routeName, $tagName, reset($scopes)); + if ($isOCS && !array_key_exists("OCSMeta", $schemas)) { $schemas["OCSMeta"] = [ "type" => "object", @@ -428,7 +430,7 @@ foreach ($parsedRoutes as $key => $value) { $routes[$scope] ??= []; $routes[$scope][] = new Route( $routeName, - $tagName, + $routeTags[$scope] ?? [$tagName], $methodName, $postfix, $verb, @@ -451,8 +453,10 @@ $tagNames = []; if ($useTags) { foreach ($routes as $scope => $scopeRoutes) { foreach ($scopeRoutes as $route) { - if (!in_array($route->tag, $tagNames)) { - $tagNames[] = $route->tag; + foreach ($route->tags as $tag) { + if (!in_array($tag, $tagNames)) { + $tagNames[] = $tag; + } } } } @@ -614,7 +618,7 @@ foreach ($routes as $scope => $scopeRoutes) { ); } - $operationId = [$route->tag, ...Helpers::splitOnUppercaseFollowedByNonUppercase($route->methodName)]; + $operationId = [...$route->tags, ...Helpers::splitOnUppercaseFollowedByNonUppercase($route->methodName)]; if ($route->postfix != null) { $operationId[] = $route->postfix; } @@ -638,7 +642,7 @@ foreach ($routes as $scope => $scopeRoutes) { $route->controllerMethod->summary != null ? ["summary" => $route->controllerMethod->summary] : [], count($route->controllerMethod->description) > 0 ? ["description" => implode("\n", $route->controllerMethod->description)] : [], $route->controllerMethod->isDeprecated ? ["deprecated" => true] : [], - $useTags ? ["tags" => [$route->tag]] : [], + $useTags ? ["tags" => $route->tags] : [], count($security) > 0 ? ["security" => $security] : [], count($queryParameters) > 0 || count($pathParameters) > 0 || $route->isOCS ? [ "parameters" => array_merge( diff --git a/src/Helpers.php b/src/Helpers.php index 5120c16..c54cb6d 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -4,6 +4,8 @@ use Exception; use PhpParser\Node; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Class_; @@ -190,6 +192,63 @@ static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annota return $scopes; } + public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, string $annotation, string $routeName, string $defaultTag, string $defaultScope): array { + $tags = []; + + /** @var Node\AttributeGroup $attrGroup */ + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->getLast() === $annotation) { + if (empty($attr->args)) { + $tags[$defaultScope] = [$defaultTag]; + continue; + } + + $foundsTags = []; + $foundScopeName = null; + foreach ($attr->args as $arg) { + if ($arg->name->name === 'scope') { + if ($arg->value instanceof ClassConstFetch) { + if ($arg->value->class->getLast() === 'OpenAPI') { + $foundScopeName = match ($arg->value->name->name) { + 'SCOPE_DEFAULT' => 'default', + 'SCOPE_ADMINISTRATION' => 'administration', + 'SCOPE_FEDERATION' => 'federation', + 'SCOPE_IGNORE' => 'ignore', + // Fall back for future scopes assuming we follow the pattern (cut of 'SCOPE_' and lower case) + default => strtolower(substr($arg->value->name->name, 6)), + }; + } + } elseif ($arg->value instanceof String_) { + $foundScopeName = $arg->value->value; + } else { + Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); + } + } + + if ($arg->name->name === 'tags') { + if ($arg->value instanceof Array_) { + foreach ($arg->value->items as $item) { + if ($item instanceof ArrayItem) { + if ($item->value instanceof String_) { + $foundsTags[] = $item->value->value; + } + } + } + } + } + } + + if (!empty($foundsTags)) { + $tags[$foundScopeName ?: $defaultScope] = $foundsTags; + } + } + } + } + + return $tags; + } + public static function collectUsedRefs(array $data): array { $refs = []; if (isset($data['$ref'])) { diff --git a/src/Route.php b/src/Route.php index f53c72b..d0e064c 100644 --- a/src/Route.php +++ b/src/Route.php @@ -5,7 +5,7 @@ class Route { public function __construct( public string $name, - public string $tag, + public array $tags, public string $methodName, public ?string $postfix, public string $verb, From 481ca1c7d9a9e9cbcd4495113de18811882a5446 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 15:57:49 +0100 Subject: [PATCH 06/29] fix(scopes): Don't break on files responses Signed-off-by: Joas Schilling --- generate-spec | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/generate-spec b/generate-spec index 134dcad..50635fe 100755 --- a/generate-spec +++ b/generate-spec @@ -771,7 +771,11 @@ foreach ($scopePaths as $scope => $paths) { foreach ($paths as $url => $urlRoutes) { foreach ($urlRoutes as $httpMethod => $routeData) { foreach ($routeData['responses'] as $statusCode => $responseData) { - $usedSchemas = array_merge($usedSchemas, Helpers::collectUsedRefs($responseData['content']['application/json']['schema'])); + if (isset($responseData['content']['application/json'])) { + $usedSchemas = array_merge($usedSchemas, Helpers::collectUsedRefs($responseData['content']['application/json']['schema'])); + } else { + Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); + } } } } From a369adbd4e9ab0519465a54051ae575693fa9a05 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:00:07 +0100 Subject: [PATCH 07/29] fix(scopes): Deduplicate logic Signed-off-by: Joas Schilling --- src/Helpers.php | 77 ++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/src/Helpers.php b/src/Helpers.php index c54cb6d..dcc5d54 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -4,6 +4,8 @@ use Exception; use PhpParser\Node; +use PhpParser\Node\Arg; +use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\ClassConstFetch; @@ -137,7 +139,7 @@ public static function classMethodHasAnnotationOrAttribute(ClassMethod|Class_|No return true; } - /** @var Node\AttributeGroup $attrGroup */ + /** @var AttributeGroup $attrGroup */ foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->getLast() == $annotation) { @@ -154,35 +156,49 @@ public static function cleanSchemaName(string $name): string { return substr($name, strlen($readableAppID)); } - static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annotation, string $routeName): array { + protected static function getScopeNameFromAttributeArgument(Arg $arg, string $routeName): ?string { + if ($arg->name->name === 'scope') { + if ($arg->value instanceof ClassConstFetch) { + if ($arg->value->class->getLast() === 'OpenAPI') { + return self::getScopeNameFromConst($arg->value); + } + } elseif ($arg->value instanceof String_) { + return $arg->value->value; + } else { + Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); + } + } + + return null; + } + + protected static function getScopeNameFromConst(ClassConstFetch $scope): string { + return match ($scope->name->name) { + 'SCOPE_DEFAULT' => 'default', + 'SCOPE_ADMINISTRATION' => 'administration', + 'SCOPE_FEDERATION' => 'federation', + 'SCOPE_IGNORE' => 'ignore', + // Fall back for future scopes assuming we follow the pattern (cut of 'SCOPE_' and lower case) + default => strtolower(substr($scope->name->name, 6)), + }; + } + + public static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annotation, string $routeName): array { $scopes = []; - /** @var Node\AttributeGroup $attrGroup */ + /** @var AttributeGroup $attrGroup */ foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->getLast() === $annotation) { if (empty($attr->args)) { $scopes[] = 'default'; + continue; } foreach ($attr->args as $arg) { - if ($arg->name->name === 'scope') { - if ($arg->value instanceof ClassConstFetch) { - if ($arg->value->class->getLast() === 'OpenAPI') { - $scopes[] = match ($arg->value->name->name) { - 'SCOPE_DEFAULT' => 'default', - 'SCOPE_ADMINISTRATION' => 'administration', - 'SCOPE_FEDERATION' => 'federation', - 'SCOPE_IGNORE' => 'ignore', - // Fall back for future scopes assuming we follow the pattern (cut of 'SCOPE_' and lower case) - default => strtolower(substr($arg->value->name->name, 6)), - }; - } - } elseif ($arg->value instanceof String_) { - $scopes[] = $arg->value->value; - } else { - Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); - } + $scope = self::getScopeNameFromAttributeArgument($arg, $routeName); + if ($scope !== null) { + $scopes[] = $scope; } } } @@ -195,7 +211,7 @@ static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annota public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, string $annotation, string $routeName, string $defaultTag, string $defaultScope): array { $tags = []; - /** @var Node\AttributeGroup $attrGroup */ + /** @var AttributeGroup $attrGroup */ foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { if ($attr->name->getLast() === $annotation) { @@ -207,24 +223,7 @@ public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, st $foundsTags = []; $foundScopeName = null; foreach ($attr->args as $arg) { - if ($arg->name->name === 'scope') { - if ($arg->value instanceof ClassConstFetch) { - if ($arg->value->class->getLast() === 'OpenAPI') { - $foundScopeName = match ($arg->value->name->name) { - 'SCOPE_DEFAULT' => 'default', - 'SCOPE_ADMINISTRATION' => 'administration', - 'SCOPE_FEDERATION' => 'federation', - 'SCOPE_IGNORE' => 'ignore', - // Fall back for future scopes assuming we follow the pattern (cut of 'SCOPE_' and lower case) - default => strtolower(substr($arg->value->name->name, 6)), - }; - } - } elseif ($arg->value instanceof String_) { - $foundScopeName = $arg->value->value; - } else { - Logger::panic($routeName, 'Can not interpret value of scope provided in OpenAPI(scope: …) attribute. Please use string or OpenAPI::SCOPE_* constants'); - } - } + $foundScopeName = self::getScopeNameFromAttributeArgument($arg, $routeName); if ($arg->name->name === 'tags') { if ($arg->value instanceof Array_) { From f6ad18f3217319426692b475ec6ca516f23a63da Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:01:10 +0100 Subject: [PATCH 08/29] fix(scopes): Also export schemas that are only referenced by other schemas Signed-off-by: Joas Schilling --- generate-spec | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/generate-spec b/generate-spec index 50635fe..98c2dd8 100755 --- a/generate-spec +++ b/generate-spec @@ -781,7 +781,7 @@ foreach ($scopePaths as $scope => $paths) { } $scopedSchemas = []; - foreach ($usedSchemas as $usedSchema) { + while ($usedSchema = array_shift($usedSchemas)) { if (!str_starts_with($usedSchema, '#/components/schemas/')) { continue; } @@ -792,6 +792,18 @@ foreach ($scopePaths as $scope => $paths) { Logger::error("app", "Schema $schemaName used by scope $scope is not defined"); } + // Queue potential sub-refs for exporting as well + if (isset($schemas[$schemaName]['allOf'])) { + foreach ($schemas[$schemaName]['allOf'] as $subType) { + $newRefs = Helpers::collectUsedRefs($subType); + foreach ($newRefs as $newRef) { + if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { + $usedSchemas[] = $newRef; + } + } + } + } + $scopedSchemas[$schemaName] = $schemas[$schemaName]; } From 6ef6a4b9124f28c24880866cabc0dac7a71eafea Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:02:11 +0100 Subject: [PATCH 09/29] fix(scopes): Don't warn when we deal with binary return types Signed-off-by: Joas Schilling --- generate-spec | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/generate-spec b/generate-spec index 98c2dd8..35c72a3 100755 --- a/generate-spec +++ b/generate-spec @@ -773,6 +773,10 @@ foreach ($scopePaths as $scope => $paths) { foreach ($routeData['responses'] as $statusCode => $responseData) { if (isset($responseData['content']['application/json'])) { $usedSchemas = array_merge($usedSchemas, Helpers::collectUsedRefs($responseData['content']['application/json']['schema'])); + } elseif (isset($responseData['content']['*/*']['schema']['type'], $responseData['content']['*/*']['schema']['format']) + && $responseData['content']['*/*']['schema']['type'] === 'string' + && $responseData['content']['*/*']['schema']['format'] === 'binary') { + Logger::info("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); } else { Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); } From 7aa0bac6544952994ee8b21ed75f86ac992c3345 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:04:05 +0100 Subject: [PATCH 10/29] fix(scopes): Correctly add schemas references by nested schemas Signed-off-by: Joas Schilling --- generate-spec | 14 ++++++++------ src/Helpers.php | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/generate-spec b/generate-spec index 35c72a3..e8ffa89 100755 --- a/generate-spec +++ b/generate-spec @@ -797,12 +797,14 @@ foreach ($scopePaths as $scope => $paths) { } // Queue potential sub-refs for exporting as well - if (isset($schemas[$schemaName]['allOf'])) { - foreach ($schemas[$schemaName]['allOf'] as $subType) { - $newRefs = Helpers::collectUsedRefs($subType); - foreach ($newRefs as $newRef) { - if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { - $usedSchemas[] = $newRef; + foreach (['allOf', 'oneOf', 'anyOf', 'properties'] as $group) { + if (isset($schemas[$schemaName][$group])) { + foreach ($schemas[$schemaName][$group] as $subType) { + $newRefs = Helpers::collectUsedRefs($subType); + foreach ($newRefs as $newRef) { + if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { + $usedSchemas[] = $newRef; + } } } } diff --git a/src/Helpers.php b/src/Helpers.php index dcc5d54..01126a7 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -260,6 +260,9 @@ public static function collectUsedRefs(array $data): array { } } } + if (isset($data['items'])) { + $refs[] = self::collectUsedRefs($data['items']); + } return array_merge(...$refs); } } From 886b55f3ff13858df78a9e72b428b252437d5e38 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:04:53 +0100 Subject: [PATCH 11/29] fix(scopes): Don't break with \stdClass returns Signed-off-by: Joas Schilling --- generate-spec | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/generate-spec b/generate-spec index e8ffa89..1021180 100755 --- a/generate-spec +++ b/generate-spec @@ -772,13 +772,16 @@ foreach ($scopePaths as $scope => $paths) { foreach ($urlRoutes as $httpMethod => $routeData) { foreach ($routeData['responses'] as $statusCode => $responseData) { if (isset($responseData['content']['application/json'])) { - $usedSchemas = array_merge($usedSchemas, Helpers::collectUsedRefs($responseData['content']['application/json']['schema'])); + if (is_array($responseData['content']['application/json']['schema'])) { + $newSchemas = Helpers::collectUsedRefs($responseData['content']['application/json']['schema']); + $usedSchemas = array_merge($usedSchemas, $newSchemas); + } } elseif (isset($responseData['content']['*/*']['schema']['type'], $responseData['content']['*/*']['schema']['format']) && $responseData['content']['*/*']['schema']['type'] === 'string' && $responseData['content']['*/*']['schema']['format'] === 'binary') { Logger::info("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); } else { - Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); + Logger::info("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); } } } From 59826d74cf547859c846e3110a055bcccbb1265c Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:06:04 +0100 Subject: [PATCH 12/29] fix(scopes): Handle all possible cases of other Schemas when importing used schemas Signed-off-by: Joas Schilling --- generate-spec | 11 ++++++++++- src/Helpers.php | 12 ++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/generate-spec b/generate-spec index 1021180..ada2912 100755 --- a/generate-spec +++ b/generate-spec @@ -800,7 +800,7 @@ foreach ($scopePaths as $scope => $paths) { } // Queue potential sub-refs for exporting as well - foreach (['allOf', 'oneOf', 'anyOf', 'properties'] as $group) { + foreach (['allOf', 'oneOf', 'anyOf', 'properties', 'additionalProperties'] as $group) { if (isset($schemas[$schemaName][$group])) { foreach ($schemas[$schemaName][$group] as $subType) { $newRefs = Helpers::collectUsedRefs($subType); @@ -813,6 +813,15 @@ foreach ($scopePaths as $scope => $paths) { } } + if (isset($schemas[$schemaName]['items'])) { + $newRefs = Helpers::collectUsedRefs($schemas[$schemaName]['items']); + foreach ($newRefs as $newRef) { + if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { + $usedSchemas[] = $newRef; + } + } + } + $scopedSchemas[$schemaName] = $schemas[$schemaName]; } diff --git a/src/Helpers.php b/src/Helpers.php index 01126a7..e2691b5 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -253,13 +253,17 @@ public static function collectUsedRefs(array $data): array { if (isset($data['$ref'])) { $refs[] = [$data['$ref']]; } - if (isset($data['properties'])) { - foreach ($data['properties'] as $property) { - if (is_array($property)) { - $refs[] = self::collectUsedRefs($property); + + foreach (['allOf', 'oneOf', 'anyOf', 'properties', 'additionalProperties'] as $group) { + if (isset($data[$group]) && is_array($data[$group])) { + foreach ($data[$group] as $property) { + if (is_array($property)) { + $refs[] = self::collectUsedRefs($property); + } } } } + if (isset($data['items'])) { $refs[] = self::collectUsedRefs($data['items']); } From 2cf5e137ed190f017fbfcec96674294009e91bb5 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 Jan 2024 16:23:15 +0100 Subject: [PATCH 13/29] fix(scopes): Add tests for the scope and tag features Signed-off-by: Joas Schilling --- tests/appinfo/routes.php | 5 +- tests/lib/Controller/FederationController.php | 69 + tests/lib/Controller/SettingsController.php | 29 - tests/openapi-administration.json | 1762 ++++++++++++++++ tests/openapi-federation.json | 123 ++ tests/openapi.json | 1878 +---------------- 6 files changed, 2006 insertions(+), 1860 deletions(-) create mode 100644 tests/lib/Controller/FederationController.php create mode 100644 tests/openapi-administration.json create mode 100644 tests/openapi-federation.json diff --git a/tests/appinfo/routes.php b/tests/appinfo/routes.php index e17e0e6..5899300 100644 --- a/tests/appinfo/routes.php +++ b/tests/appinfo/routes.php @@ -30,10 +30,11 @@ ['name' => 'AdminSettings#movedToDefaultScope', 'url' => '/api/{apiVersion}/default-admin-overwritten', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'AdminSettings#movedToSettingsTag', 'url' => '/api/{apiVersion}/moved-with-tag', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], - ['name' => 'Settings#federationByController', 'url' => '/api/{apiVersion}/controller-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], + ['name' => 'Federation#federationByController', 'url' => '/api/{apiVersion}/controller-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], + ['name' => 'Federation#movedToDefaultScope', 'url' => '/api/{apiVersion}/default-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], + ['name' => 'Settings#ignoreByDeprecatedAttributeOnMethod', 'url' => '/api/{apiVersion}/ignore-openapi-attribute', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#ignoreByScopeOnMethod', 'url' => '/api/{apiVersion}/ignore-method-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], - ['name' => 'Settings#movedToDefaultScope', 'url' => '/api/{apiVersion}/default-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#movedToAdminScope', 'url' => '/api/{apiVersion}/admin-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#defaultAndAdminScope', 'url' => '/api/{apiVersion}/default-and-admin-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#nestedSchemas', 'url' => '/api/{apiVersion}/nested-schemas', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], diff --git a/tests/lib/Controller/FederationController.php b/tests/lib/Controller/FederationController.php new file mode 100644 index 0000000..df57737 --- /dev/null +++ b/tests/lib/Controller/FederationController.php @@ -0,0 +1,69 @@ + + * + * @author Julien Barnoin + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Notifications\Controller; + +use OCA\Notifications\ResponseDefinitions; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; + +/** + * @psalm-import-type NotificationsPushDevice from ResponseDefinitions + * @psalm-import-type NotificationsNotification from ResponseDefinitions + * @psalm-import-type NotificationsCollection from ResponseDefinitions + */ +#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] +class FederationController extends OCSController { + + /** + * @NoAdminRequired + * + * Route is ignored because of scope on the controller + * + * @return DataResponse, array{}> + * + * 200: OK + */ + public function federationByController(): DataResponse { + return new DataResponse(); + } + + /** + * @NoAdminRequired + * + * Route is only in the default scope + * + * @return DataResponse, array{}> + * + * 200: Personal settings updated + */ + #[OpenAPI] + public function movedToDefaultScope(): DataResponse { + return new DataResponse(); + } +} diff --git a/tests/lib/Controller/SettingsController.php b/tests/lib/Controller/SettingsController.php index 71f9878..703d587 100644 --- a/tests/lib/Controller/SettingsController.php +++ b/tests/lib/Controller/SettingsController.php @@ -38,22 +38,7 @@ * @psalm-import-type NotificationsNotification from ResponseDefinitions * @psalm-import-type NotificationsCollection from ResponseDefinitions */ -#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] class SettingsController extends OCSController { - - /** - * @NoAdminRequired - * - * Route is ignored because of scope on the controller - * - * @return DataResponse, array{}> - * - * 200: OK - */ - public function federationByController(): DataResponse { - return new DataResponse(); - } - /** * @NoAdminRequired * @@ -82,20 +67,6 @@ public function ignoreByScopeOnMethod(): DataResponse { return new DataResponse(); } - /** - * @NoAdminRequired - * - * Route is only in the default scope - * - * @return DataResponse, array{}> - * - * 200: Personal settings updated - */ - #[OpenAPI] - public function movedToDefaultScope(): DataResponse { - return new DataResponse(); - } - /** * @NoAdminRequired * diff --git a/tests/openapi-administration.json b/tests/openapi-administration.json new file mode 100644 index 0000000..4c64552 --- /dev/null +++ b/tests/openapi-administration.json @@ -0,0 +1,1762 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "notifications-administration", + "version": "0.0.1", + "description": "This app provides a backend and frontend for the notification API available in Nextcloud.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + }, + "PushDevice": { + "allOf": [ + { + "$ref": "#/components/schemas/PushDeviceBase" + }, + { + "type": "object", + "required": [ + "publicKey", + "signature" + ], + "properties": { + "publicKey": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + } + ] + }, + "PushDeviceBase": { + "type": "object", + "required": [ + "deviceIdentifier" + ], + "properties": { + "deviceIdentifier": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-admin": { + "post": { + "operationId": "admin_settings-admin-scope-implicit-from-admin-required", + "summary": "Route is only in the admin scope because there is no \"NoAdminRequired\" annotation or attribute", + "description": "This endpoint requires admin access", + "tags": [ + "admin_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/moved-with-tag": { + "post": { + "operationId": "settings-admin-settings-moved-to-settings-tag", + "summary": "Route in default scope with tags", + "description": "This endpoint requires admin access", + "tags": [ + "settings", + "admin-settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/admin-scope": { + "post": { + "operationId": "settings-moved-to-admin-scope", + "summary": "Route is only in the admin scope due to defined scope", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/PushDevice" + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-and-admin-scope": { + "post": { + "operationId": "settings-default-and-admin-scope", + "summary": "Route is in admin and default scope", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-of-int": { + "post": { + "operationId": "settings-list-of-int-parameters", + "summary": "A route with a limited set of possible integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of objects", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/min-max": { + "post": { + "operationId": "settings-int-parameter-with-min-and-max", + "summary": "A route with a min and max integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Between 5 and 10", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 5, + "maximum": 10 + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/min": { + "post": { + "operationId": "settings-int-parameter-with-min", + "summary": "A route with a min integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "At least 5", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 5 + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/max": { + "post": { + "operationId": "settings-int-parameter-with-max", + "summary": "A route with a max integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "At most 10", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "maximum": 10 + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/mixed-list-one": { + "post": { + "operationId": "settings-list-of-int-string-and-one-bool", + "summary": "A route with a list of 2 integers, 2 strings and 1 boolean", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "weird", + "in": "query", + "description": "Weird list", + "required": true, + "schema": { + "oneOf": [ + { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + { + "type": "string", + "enum": [ + "yes", + "no" + ] + }, + { + "type": "boolean", + "enum": [ + true + ] + } + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/mixed-list-all": { + "post": { + "operationId": "settings-list-of-int-string-and-all-bools", + "summary": "A route with a list of 2 integers, 2 strings and 1 boolean", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "weird", + "in": "query", + "description": "Weird list", + "required": true, + "schema": { + "oneOf": [ + { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + { + "type": "string", + "enum": [ + "yes", + "no" + ] + }, + { + "type": "boolean", + "enum": [ + true, + false + ] + } + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean": { + "post": { + "operationId": "settings-boolean-parameter-required", + "summary": "A route with required boolean", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "Boolean required", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-default-false": { + "post": { + "operationId": "settings-boolean-parameter-default-false", + "summary": "A route with boolean defaulting to false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "Boolean defaulting to false", + "schema": { + "type": "integer", + "default": 0, + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-default-true": { + "post": { + "operationId": "settings-boolean-parameter-default-true", + "summary": "A route with boolean defaulting to true", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "Boolean defaulting to true", + "schema": { + "type": "integer", + "default": 1, + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-true": { + "post": { + "operationId": "settings-boolean-true-parameter", + "summary": "A route with boolean or true", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "boolean or true", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-false": { + "post": { + "operationId": "settings-boolean-false-parameter", + "summary": "A route with boolean or false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "boolean or false", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-true-false": { + "post": { + "operationId": "settings-boolean-true-false-parameter", + "summary": "A route with boolean or true or false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "boolean or true or false", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/true-false": { + "post": { + "operationId": "settings-true-false-parameter", + "summary": "A route with true or false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "true or false", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/string-value": { + "post": { + "operationId": "settings-string-value-parameter", + "summary": "A route with string or 'test'", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "value", + "in": "query", + "description": "string or 'test'", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/int-value": { + "post": { + "operationId": "settings-int-value-parameter", + "summary": "A route with int or 0", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "value", + "in": "query", + "description": "int or 0", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/numeric": { + "post": { + "operationId": "settings-numeric-parameter", + "summary": "A route with numeric", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "value", + "in": "query", + "description": "Some numeric value", + "required": true, + "schema": { + "type": "number" + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/tests/openapi-federation.json b/tests/openapi-federation.json new file mode 100644 index 0000000..bd686c9 --- /dev/null +++ b/tests/openapi-federation.json @@ -0,0 +1,123 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "notifications-federation", + "version": "0.0.1", + "description": "This app provides a backend and frontend for the notification API available in Nextcloud.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/controller-scope": { + "post": { + "operationId": "federation-federation-by-controller", + "summary": "Route is ignored because of scope on the controller", + "tags": [ + "federation" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/tests/openapi.json b/tests/openapi.json index 501197f..ce4f56d 100644 --- a/tests/openapi.json +++ b/tests/openapi.json @@ -20,32 +20,27 @@ } }, "schemas": { - "Collection": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Item" - } - }, - "Item": { + "OCSMeta": { "type": "object", "required": [ - "label", - "link", - "type", - "primary" + "status", + "statuscode" ], "properties": { - "label": { + "status": { "type": "string" }, - "link": { + "statuscode": { + "type": "integer" + }, + "message": { "type": "string" }, - "type": { + "totalitems": { "type": "string" }, - "primary": { - "type": "boolean" + "itemsperpage": { + "type": "string" } } }, @@ -124,6 +119,12 @@ } } }, + "Collection": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + } + }, "NotificationAction": { "type": "object", "required": [ @@ -147,138 +148,32 @@ } } }, - "OCSMeta": { + "Item": { "type": "object", "required": [ - "status", - "statuscode" + "label", + "link", + "type", + "primary" ], "properties": { - "status": { - "type": "string" - }, - "statuscode": { - "type": "integer" - }, - "message": { + "label": { "type": "string" }, - "totalitems": { + "link": { "type": "string" }, - "itemsperpage": { + "type": { "type": "string" - } - } - }, - "PushDevice": { - "allOf": [ - { - "$ref": "#/components/schemas/PushDeviceBase" }, - { - "type": "object", - "required": [ - "publicKey", - "signature" - ], - "properties": { - "publicKey": { - "type": "string" - }, - "signature": { - "type": "string" - } - } - } - ] - }, - "PushDeviceBase": { - "type": "object", - "required": [ - "deviceIdentifier" - ], - "properties": { - "deviceIdentifier": { - "type": "string" + "primary": { + "type": "boolean" } } } } }, "paths": { - "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-admin": { - "post": { - "operationId": "admin_settings-admin-scope-implicit-from-admin-required", - "summary": "Route is only in the admin scope because there is no \"NoAdminRequired\" annotation or attribute", - "description": "This endpoint requires admin access", - "tags": [ - "admin_settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Personal settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-admin-overwritten": { "post": { "operationId": "admin_settings-moved-to-default-scope", @@ -351,13 +246,12 @@ } } }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/moved-with-tag": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-scope": { "post": { - "operationId": "admin_settings-moved-to-settings-tag", - "summary": "Route in default scope with tags", - "description": "This endpoint requires admin access", + "operationId": "federation-moved-to-default-scope", + "summary": "Route is only in the default scope", "tags": [ - "admin_settings" + "federation" ], "security": [ { @@ -423,10 +317,10 @@ } } }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/controller-scope": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-and-admin-scope": { "post": { - "operationId": "settings-federation-by-controller", - "summary": "Route is ignored because of scope on the controller", + "operationId": "settings-default-and-admin-scope", + "summary": "Route is in admin and default scope", "tags": [ "settings" ], @@ -464,7 +358,7 @@ ], "responses": { "200": { - "description": "OK", + "description": "Admin settings updated", "content": { "application/json": { "schema": { @@ -494,10 +388,10 @@ } } }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/ignore-method-scope": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/nested-schemas": { "post": { - "operationId": "settings-ignore-by-scope-on-method", - "summary": "Route is ignored because of scope on the method", + "operationId": "settings-nested-schemas", + "summary": "Route is ignored because of scope on the controller", "tags": [ "settings" ], @@ -554,78 +448,12 @@ "meta": { "$ref": "#/components/schemas/OCSMeta" }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-scope": { - "post": { - "operationId": "settings-moved-to-default-scope", - "summary": "Route is only in the default scope", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Personal settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + } + } } } } @@ -636,10 +464,10 @@ } } }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/admin-scope": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-schemas": { "post": { - "operationId": "settings-moved-to-admin-scope", - "summary": "Route is only in the admin scope due to defined scope", + "operationId": "settings-list-schemas", + "summary": "Route is ignored because of scope on the controller", "tags": [ "settings" ], @@ -677,7 +505,7 @@ ], "responses": { "200": { - "description": "Admin settings updated", + "description": "OK", "content": { "application/json": { "schema": { @@ -697,7 +525,7 @@ "$ref": "#/components/schemas/OCSMeta" }, "data": { - "$ref": "#/components/schemas/PushDevice" + "$ref": "#/components/schemas/Collection" } } } @@ -708,1614 +536,6 @@ } } } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-and-admin-scope": { - "post": { - "operationId": "settings-default-and-admin-scope", - "summary": "Route is in admin and default scope", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/nested-schemas": { - "post": { - "operationId": "settings-nested-schemas", - "summary": "Route is ignored because of scope on the controller", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Notification" - } - } - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-schemas": { - "post": { - "operationId": "settings-list-schemas", - "summary": "Route is ignored because of scope on the controller", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "$ref": "#/components/schemas/Collection" - } - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-of-int": { - "post": { - "operationId": "settings-list-of-int-parameters", - "summary": "A route with a limited set of possible integers", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Maximum number of objects", - "required": true, - "schema": { - "type": "integer", - "format": "int64", - "enum": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/min-max": { - "post": { - "operationId": "settings-int-parameter-with-min-and-max", - "summary": "A route with a min and max integers", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Between 5 and 10", - "required": true, - "schema": { - "type": "integer", - "format": "int64", - "minimum": 5, - "maximum": 10 - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/min": { - "post": { - "operationId": "settings-int-parameter-with-min", - "summary": "A route with a min integers", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "At least 5", - "required": true, - "schema": { - "type": "integer", - "format": "int64", - "minimum": 5 - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/max": { - "post": { - "operationId": "settings-int-parameter-with-max", - "summary": "A route with a max integers", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "At most 10", - "required": true, - "schema": { - "type": "integer", - "format": "int64", - "maximum": 10 - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/mixed-list-one": { - "post": { - "operationId": "settings-list-of-int-string-and-one-bool", - "summary": "A route with a list of 2 integers, 2 strings and 1 boolean", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "weird", - "in": "query", - "description": "Weird list", - "required": true, - "schema": { - "oneOf": [ - { - "type": "integer", - "enum": [ - 0, - 1 - ] - }, - { - "type": "string", - "enum": [ - "yes", - "no" - ] - }, - { - "type": "boolean", - "enum": [ - true - ] - } - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/mixed-list-all": { - "post": { - "operationId": "settings-list-of-int-string-and-all-bools", - "summary": "A route with a list of 2 integers, 2 strings and 1 boolean", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "weird", - "in": "query", - "description": "Weird list", - "required": true, - "schema": { - "oneOf": [ - { - "type": "integer", - "enum": [ - 0, - 1 - ] - }, - { - "type": "string", - "enum": [ - "yes", - "no" - ] - }, - { - "type": "boolean", - "enum": [ - true, - false - ] - } - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean": { - "post": { - "operationId": "settings-boolean-parameter-required", - "summary": "A route with required boolean", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "Boolean required", - "required": true, - "schema": { - "type": "integer", - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-default-false": { - "post": { - "operationId": "settings-boolean-parameter-default-false", - "summary": "A route with boolean defaulting to false", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "Boolean defaulting to false", - "schema": { - "type": "integer", - "default": 0, - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-default-true": { - "post": { - "operationId": "settings-boolean-parameter-default-true", - "summary": "A route with boolean defaulting to true", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "Boolean defaulting to true", - "schema": { - "type": "integer", - "default": 1, - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-true": { - "post": { - "operationId": "settings-boolean-true-parameter", - "summary": "A route with boolean or true", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "boolean or true", - "required": true, - "schema": { - "type": "integer", - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-false": { - "post": { - "operationId": "settings-boolean-false-parameter", - "summary": "A route with boolean or false", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "boolean or false", - "required": true, - "schema": { - "type": "integer", - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-true-false": { - "post": { - "operationId": "settings-boolean-true-false-parameter", - "summary": "A route with boolean or true or false", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "boolean or true or false", - "required": true, - "schema": { - "type": "integer", - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/true-false": { - "post": { - "operationId": "settings-true-false-parameter", - "summary": "A route with true or false", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "yesOrNo", - "in": "query", - "description": "true or false", - "required": true, - "schema": { - "type": "integer", - "enum": [ - 0, - 1 - ] - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/string-value": { - "post": { - "operationId": "settings-string-value-parameter", - "summary": "A route with string or 'test'", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "value", - "in": "query", - "description": "string or 'test'", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/int-value": { - "post": { - "operationId": "settings-int-value-parameter", - "summary": "A route with int or 0", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "value", - "in": "query", - "description": "int or 0", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/notifications/api/{apiVersion}/numeric": { - "post": { - "operationId": "settings-numeric-parameter", - "summary": "A route with numeric", - "description": "This endpoint requires admin access", - "tags": [ - "settings" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "value", - "in": "query", - "description": "Some numeric value", - "required": true, - "schema": { - "type": "number" - } - }, - { - "name": "apiVersion", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": [ - "v2" - ], - "default": "v2" - } - }, - { - "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": "Admin settings updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } } }, "tags": [] From 2cd80cffa04696a0b5c8f09acd69d6ecaaace366 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 11:20:46 +0100 Subject: [PATCH 14/29] fix(scopes): Fix tag variable name Signed-off-by: Joas Schilling --- src/Helpers.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Helpers.php b/src/Helpers.php index e2691b5..d93f579 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -220,7 +220,7 @@ public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, st continue; } - $foundsTags = []; + $foundTags = []; $foundScopeName = null; foreach ($attr->args as $arg) { $foundScopeName = self::getScopeNameFromAttributeArgument($arg, $routeName); @@ -230,7 +230,7 @@ public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, st foreach ($arg->value->items as $item) { if ($item instanceof ArrayItem) { if ($item->value instanceof String_) { - $foundsTags[] = $item->value->value; + $foundTags[] = $item->value->value; } } } @@ -238,8 +238,8 @@ public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, st } } - if (!empty($foundsTags)) { - $tags[$foundScopeName ?: $defaultScope] = $foundsTags; + if (!empty($foundTags)) { + $tags[$foundScopeName ?: $defaultScope] = $foundTags; } } } From f76700f1609f1c8dc415750c2a73dec43f9b6ef7 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 11:34:30 +0100 Subject: [PATCH 15/29] fix(scopes): Remove attribute name from parameter as it's always the same Signed-off-by: Joas Schilling --- generate-spec | 6 +++--- src/Helpers.php | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/generate-spec b/generate-spec index ada2912..fb8e6b2 100755 --- a/generate-spec +++ b/generate-spec @@ -302,7 +302,7 @@ foreach ($parsedRoutes as $key => $value) { continue; } - $controllerScopes = Helpers::getAttributeScopes($controllerClass, 'OpenAPI', $routeName); + $controllerScopes = Helpers::getOpenAPIAttributeScopes($controllerClass, $routeName); if (in_array('ignore', $controllerScopes, true)) { Logger::info($routeName, "Controller '" . $controllerName . "' ignored because of OpenAPI attribute"); continue; @@ -361,7 +361,7 @@ foreach ($parsedRoutes as $key => $value) { continue; } - $scopes = Helpers::getAttributeScopes($classMethod, 'OpenAPI', $routeName); + $scopes = Helpers::getOpenAPIAttributeScopes($classMethod, $routeName); if (in_array('ignore', $scopes, true)) { Logger::info($routeName, "Route ignored because of OpenAPI attribute"); continue; @@ -377,7 +377,7 @@ foreach ($parsedRoutes as $key => $value) { } } - $routeTags = Helpers::getAttributeTagsByScope($classMethod, 'OpenAPI', $routeName, $tagName, reset($scopes)); + $routeTags = Helpers::getOpenAPIAttributeTagsByScope($classMethod, $routeName, $tagName, reset($scopes)); if ($isOCS && !array_key_exists("OCSMeta", $schemas)) { $schemas["OCSMeta"] = [ diff --git a/src/Helpers.php b/src/Helpers.php index d93f579..95b9fa2 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -15,6 +15,8 @@ use stdClass; class Helpers { + public const OPENAPI_ATTRIBUTE_CLASSNAME = 'OpenAPI'; + public static function generateReadableAppID(string $appID): string { return implode("", array_map(fn (string $s) => ucfirst($s), explode("_", $appID))); } @@ -159,7 +161,7 @@ public static function cleanSchemaName(string $name): string { protected static function getScopeNameFromAttributeArgument(Arg $arg, string $routeName): ?string { if ($arg->name->name === 'scope') { if ($arg->value instanceof ClassConstFetch) { - if ($arg->value->class->getLast() === 'OpenAPI') { + if ($arg->value->class->getLast() === self::OPENAPI_ATTRIBUTE_CLASSNAME) { return self::getScopeNameFromConst($arg->value); } } elseif ($arg->value instanceof String_) { @@ -183,13 +185,13 @@ protected static function getScopeNameFromConst(ClassConstFetch $scope): string }; } - public static function getAttributeScopes(ClassMethod|Class_|Node $node, string $annotation, string $routeName): array { + public static function getOpenAPIAttributeScopes(ClassMethod|Class_|Node $node, string $routeName): array { $scopes = []; /** @var AttributeGroup $attrGroup */ foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->getLast() === $annotation) { + if ($attr->name->getLast() === self::OPENAPI_ATTRIBUTE_CLASSNAME) { if (empty($attr->args)) { $scopes[] = 'default'; continue; @@ -208,13 +210,13 @@ public static function getAttributeScopes(ClassMethod|Class_|Node $node, string return $scopes; } - public static function getAttributeTagsByScope(ClassMethod|Class_|Node $node, string $annotation, string $routeName, string $defaultTag, string $defaultScope): array { + public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $node, string $routeName, string $defaultTag, string $defaultScope): array { $tags = []; /** @var AttributeGroup $attrGroup */ foreach ($node->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { - if ($attr->name->getLast() === $annotation) { + if ($attr->name->getLast() === self::OPENAPI_ATTRIBUTE_CLASSNAME) { if (empty($attr->args)) { $tags[$defaultScope] = [$defaultTag]; continue; From 42f15fb9b33bd14f63e93a6686a579288da48e72 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 11:46:54 +0100 Subject: [PATCH 16/29] fix(scopes): Panic when a controller/route is ignored but has other scopes Signed-off-by: Joas Schilling --- generate-spec | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/generate-spec b/generate-spec index fb8e6b2..49e79f5 100755 --- a/generate-spec +++ b/generate-spec @@ -297,15 +297,24 @@ foreach ($parsedRoutes as $key => $value) { Logger::error($routeName, "Controller '" . $controllerName . "' not found"); continue; } + + $controllerScopes = Helpers::getOpenAPIAttributeScopes($controllerClass, $routeName); if (Helpers::classMethodHasAnnotationOrAttribute($controllerClass, "IgnoreOpenAPI")) { - Logger::debug($routeName, "Controller '" . $controllerName . "' ignored because of IgnoreOpenAPI attribute"); - continue; + if (count($controllerScopes) === 0 || (in_array('ignore', $controllerScopes, true) && count($controllerScopes) === 1)) { + Logger::info($routeName, "Controller '" . $controllerName . "' ignored because of IgnoreOpenAPI attribute"); + continue; + } + + Logger::panic($routeName, "Controller '" . $controllerName . "' is marked as ignore but also has other scopes"); } - $controllerScopes = Helpers::getOpenAPIAttributeScopes($controllerClass, $routeName); if (in_array('ignore', $controllerScopes, true)) { - Logger::info($routeName, "Controller '" . $controllerName . "' ignored because of OpenAPI attribute"); - continue; + if (count($controllerScopes) === 1) { + Logger::info($routeName, "Controller '" . $controllerName . "' ignored because of OpenAPI attribute"); + continue; + } + + Logger::panic($routeName, "Controller '" . $controllerName . "' is marked as ignore but also has other scopes"); } $tagName = implode("_", array_map(fn (string $s) => strtolower($s), Helpers::splitOnUppercaseFollowedByNonUppercase($controllerName))); @@ -355,16 +364,24 @@ foreach ($parsedRoutes as $key => $value) { $isAdmin = !Helpers::classMethodHasAnnotationOrAttribute($methodFunction, "NoAdminRequired") && !$isPublic; $isDeprecated = Helpers::classMethodHasAnnotationOrAttribute($methodFunction, "deprecated"); $isIgnored = Helpers::classMethodHasAnnotationOrAttribute($methodFunction, "IgnoreOpenAPI"); + $scopes = Helpers::getOpenAPIAttributeScopes($classMethod, $routeName); if ($isIgnored) { - Logger::debug($routeName, "Route ignored because of IgnoreOpenAPI attribute"); - continue; + if (count($scopes) === 0 || (in_array('ignore', $scopes, true) && count($scopes) === 1)) { + Logger::debug($routeName, "Route ignored because of IgnoreOpenAPI attribute"); + continue; + } + + Logger::panic($routeName, "Route is marked as ignore but also has other scopes"); } - $scopes = Helpers::getOpenAPIAttributeScopes($classMethod, $routeName); if (in_array('ignore', $scopes, true)) { - Logger::info($routeName, "Route ignored because of OpenAPI attribute"); - continue; + if (count($scopes) === 1) { + Logger::info($routeName, "Route ignored because of OpenAPI attribute"); + continue; + } + + Logger::panic($routeName, "Route is marked as ignore but also has other scopes"); } if (empty($scopes)) { From 03ccc5688959b8e7f38a8ae8dad790bd7937c23e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 11:51:24 +0100 Subject: [PATCH 17/29] fix(scopes): Better log levels for schema detection Signed-off-by: Joas Schilling --- generate-spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generate-spec b/generate-spec index 49e79f5..8c1eafc 100755 --- a/generate-spec +++ b/generate-spec @@ -796,9 +796,9 @@ foreach ($scopePaths as $scope => $paths) { } elseif (isset($responseData['content']['*/*']['schema']['type'], $responseData['content']['*/*']['schema']['format']) && $responseData['content']['*/*']['schema']['type'] === 'string' && $responseData['content']['*/*']['schema']['format'] === 'binary') { - Logger::info("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); + Logger::debug("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); } else { - Logger::info("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); + Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); } } } From 3729e5a5be5e6aed6febd98a8b9fd08b53b3fb77 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 11:54:41 +0100 Subject: [PATCH 18/29] fix(scopes): Reduce nesting of the scope loops Signed-off-by: Joas Schilling --- src/Helpers.php | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Helpers.php b/src/Helpers.php index 95b9fa2..e87073c 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -227,15 +227,16 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n foreach ($attr->args as $arg) { $foundScopeName = self::getScopeNameFromAttributeArgument($arg, $routeName); - if ($arg->name->name === 'tags') { - if ($arg->value instanceof Array_) { - foreach ($arg->value->items as $item) { - if ($item instanceof ArrayItem) { - if ($item->value instanceof String_) { - $foundTags[] = $item->value->value; - } - } - } + if ($arg->name->name !== 'tags') { + continue; + } + if (!$arg->value instanceof Array_) { + continue; + } + + foreach ($arg->value->items as $item) { + if ($item instanceof ArrayItem && $item->value instanceof String_) { + $foundTags[] = $item->value->value; } } } @@ -257,11 +258,13 @@ public static function collectUsedRefs(array $data): array { } foreach (['allOf', 'oneOf', 'anyOf', 'properties', 'additionalProperties'] as $group) { - if (isset($data[$group]) && is_array($data[$group])) { - foreach ($data[$group] as $property) { - if (is_array($property)) { - $refs[] = self::collectUsedRefs($property); - } + if (!isset($data[$group]) || !is_array($data[$group])) { + continue; + } + + foreach ($data[$group] as $property) { + if (is_array($property)) { + $refs[] = self::collectUsedRefs($property); } } } From 9833a7544b83981922c6cf72667daa6518756a90 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 12:05:39 +0100 Subject: [PATCH 19/29] fix(scopes): Deduplicate finding used schemas Signed-off-by: Joas Schilling --- generate-spec | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/generate-spec b/generate-spec index 8c1eafc..27e5b07 100755 --- a/generate-spec +++ b/generate-spec @@ -816,26 +816,10 @@ foreach ($scopePaths as $scope => $paths) { Logger::error("app", "Schema $schemaName used by scope $scope is not defined"); } - // Queue potential sub-refs for exporting as well - foreach (['allOf', 'oneOf', 'anyOf', 'properties', 'additionalProperties'] as $group) { - if (isset($schemas[$schemaName][$group])) { - foreach ($schemas[$schemaName][$group] as $subType) { - $newRefs = Helpers::collectUsedRefs($subType); - foreach ($newRefs as $newRef) { - if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { - $usedSchemas[] = $newRef; - } - } - } - } - } - - if (isset($schemas[$schemaName]['items'])) { - $newRefs = Helpers::collectUsedRefs($schemas[$schemaName]['items']); - foreach ($newRefs as $newRef) { - if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { - $usedSchemas[] = $newRef; - } + $newRefs = Helpers::collectUsedRefs($schemas[$schemaName]); + foreach ($newRefs as $newRef) { + if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { + $usedSchemas[] = $newRef; } } From a69832907086306075587cf61d10f2b31cf4f486 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 12:35:48 +0100 Subject: [PATCH 20/29] fix(scopes): Allow scopes and tags without parameter names Signed-off-by: Joas Schilling --- src/Helpers.php | 18 +++-- tests/appinfo/routes.php | 2 + .../Controller/AdminSettingsController.php | 12 +++ tests/lib/Controller/SettingsController.php | 14 ++++ tests/openapi-administration.json | 73 +++++++++++++++++++ 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/Helpers.php b/src/Helpers.php index e87073c..d37c9c1 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -158,8 +158,8 @@ public static function cleanSchemaName(string $name): string { return substr($name, strlen($readableAppID)); } - protected static function getScopeNameFromAttributeArgument(Arg $arg, string $routeName): ?string { - if ($arg->name->name === 'scope') { + protected static function getScopeNameFromAttributeArgument(Arg $arg, int $key, string $routeName): ?string { + if ($arg->name?->name === 'scope' || ($arg->name === null && $key === 0)) { if ($arg->value instanceof ClassConstFetch) { if ($arg->value->class->getLast() === self::OPENAPI_ATTRIBUTE_CLASSNAME) { return self::getScopeNameFromConst($arg->value); @@ -197,8 +197,8 @@ public static function getOpenAPIAttributeScopes(ClassMethod|Class_|Node $node, continue; } - foreach ($attr->args as $arg) { - $scope = self::getScopeNameFromAttributeArgument($arg, $routeName); + foreach ($attr->args as $key => $arg) { + $scope = self::getScopeNameFromAttributeArgument($arg, (int) $key, $routeName); if ($scope !== null) { $scopes[] = $scope; } @@ -224,13 +224,15 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n $foundTags = []; $foundScopeName = null; - foreach ($attr->args as $arg) { - $foundScopeName = self::getScopeNameFromAttributeArgument($arg, $routeName); + foreach ($attr->args as $key => $arg) { + $foundScopeName = self::getScopeNameFromAttributeArgument($arg, (int) $key, $routeName); - if ($arg->name->name !== 'tags') { + if ($arg->name?->name !== 'tags' && ($arg->name !== null || $key !== 1)) { continue; } + if (!$arg->value instanceof Array_) { + continue; } @@ -244,6 +246,8 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n if (!empty($foundTags)) { $tags[$foundScopeName ?: $defaultScope] = $foundTags; } + + } } } diff --git a/tests/appinfo/routes.php b/tests/appinfo/routes.php index 5899300..7806088 100644 --- a/tests/appinfo/routes.php +++ b/tests/appinfo/routes.php @@ -29,12 +29,14 @@ ['name' => 'AdminSettings#adminScopeImplicitFromAdminRequired', 'url' => '/api/{apiVersion}/default-admin', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'AdminSettings#movedToDefaultScope', 'url' => '/api/{apiVersion}/default-admin-overwritten', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'AdminSettings#movedToSettingsTag', 'url' => '/api/{apiVersion}/moved-with-tag', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], + ['name' => 'AdminSettings#movedToSettingsTagUnnamed', 'url' => '/api/{apiVersion}/moved-with-unnamed-tag', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Federation#federationByController', 'url' => '/api/{apiVersion}/controller-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Federation#movedToDefaultScope', 'url' => '/api/{apiVersion}/default-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#ignoreByDeprecatedAttributeOnMethod', 'url' => '/api/{apiVersion}/ignore-openapi-attribute', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#ignoreByScopeOnMethod', 'url' => '/api/{apiVersion}/ignore-method-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], + ['name' => 'Settings#ignoreByUnnamedScopeOnMethod', 'url' => '/api/{apiVersion}/ignore-method-scope-unnamed', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#movedToAdminScope', 'url' => '/api/{apiVersion}/admin-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#defaultAndAdminScope', 'url' => '/api/{apiVersion}/default-and-admin-scope', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], ['name' => 'Settings#nestedSchemas', 'url' => '/api/{apiVersion}/nested-schemas', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']], diff --git a/tests/lib/Controller/AdminSettingsController.php b/tests/lib/Controller/AdminSettingsController.php index f3fbd72..dcd461a 100644 --- a/tests/lib/Controller/AdminSettingsController.php +++ b/tests/lib/Controller/AdminSettingsController.php @@ -66,4 +66,16 @@ public function movedToDefaultScope(): DataResponse { public function movedToSettingsTag(): DataResponse { return new DataResponse(); } + + /** + * Route in default scope with tags + * + * @return DataResponse, array{}> + * + * 200: Personal settings updated + */ + #[OpenAPI(OpenAPI::SCOPE_ADMINISTRATION, ['settings', 'admin-settings'])] + public function movedToSettingsTagUnnamed(): DataResponse { + return new DataResponse(); + } } diff --git a/tests/lib/Controller/SettingsController.php b/tests/lib/Controller/SettingsController.php index 703d587..09b7726 100644 --- a/tests/lib/Controller/SettingsController.php +++ b/tests/lib/Controller/SettingsController.php @@ -67,6 +67,20 @@ public function ignoreByScopeOnMethod(): DataResponse { return new DataResponse(); } + /** + * @NoAdminRequired + * + * Route is ignored because of scope on the method but without `scope: ` name + * + * @return DataResponse, array{}> + * + * 200: OK + */ + #[OpenAPI(OpenAPI::SCOPE_IGNORE)] + public function ignoreByUnnamedScopeOnMethod(): DataResponse { + return new DataResponse(); + } + /** * @NoAdminRequired * diff --git a/tests/openapi-administration.json b/tests/openapi-administration.json index 4c64552..eea0e65 100644 --- a/tests/openapi-administration.json +++ b/tests/openapi-administration.json @@ -225,6 +225,79 @@ } } }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/moved-with-unnamed-tag": { + "post": { + "operationId": "settings-admin-settings-moved-to-settings-tag-unnamed", + "summary": "Route in default scope with tags", + "description": "This endpoint requires admin access", + "tags": [ + "settings", + "admin-settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/notifications/api/{apiVersion}/admin-scope": { "post": { "operationId": "settings-moved-to-admin-scope", From 2125e853de2ba00479e7ba1c8f80a8a403c84c51 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 13:20:28 +0100 Subject: [PATCH 21/29] feat(scopes): Add a "full" scope when more than 1 is used Signed-off-by: Joas Schilling --- generate-spec | 92 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/generate-spec b/generate-spec index 27e5b07..6ff8041 100755 --- a/generate-spec +++ b/generate-spec @@ -773,6 +773,13 @@ if ($useTags) { $openapi["tags"] = $tags; } +$hasSingleScope = count($scopePaths) === 1; +$fullScopePathArrays = []; + +if (!$hasSingleScope) { + $scopePaths['full'] = []; +} + foreach ($scopePaths as $scope => $paths) { $openapiScope = $openapi; @@ -780,60 +787,69 @@ foreach ($scopePaths as $scope => $paths) { $paths = new stdClass(); } - $scopeSuffix = $scope === 'default' ? '' : '-' . $scope; + $scopeSuffix = ($hasSingleScope || $scope === 'default') ? '' : '-' . $scope; $openapiScope['info']['title'] .= $scopeSuffix; $openapiScope['paths'] = $paths; - $usedSchemas = []; - foreach ($paths as $url => $urlRoutes) { - foreach ($urlRoutes as $httpMethod => $routeData) { - foreach ($routeData['responses'] as $statusCode => $responseData) { - if (isset($responseData['content']['application/json'])) { - if (is_array($responseData['content']['application/json']['schema'])) { - $newSchemas = Helpers::collectUsedRefs($responseData['content']['application/json']['schema']); - $usedSchemas = array_merge($usedSchemas, $newSchemas); + if ($scope !== 'full' && !$hasSingleScope) { + $fullScopePathArrays[] = $paths; + } + + if ($scope === 'full') { + $openapiScope['paths'] = array_merge(...$fullScopePathArrays); + $openapiScope['components']['schemas'] = $schemas; + } else { + $usedSchemas = []; + foreach ($paths as $url => $urlRoutes) { + foreach ($urlRoutes as $httpMethod => $routeData) { + foreach ($routeData['responses'] as $statusCode => $responseData) { + if (isset($responseData['content']['application/json'])) { + if (is_array($responseData['content']['application/json']['schema'])) { + $newSchemas = Helpers::collectUsedRefs($responseData['content']['application/json']['schema']); + $usedSchemas = array_merge($usedSchemas, $newSchemas); + } + } elseif (isset($responseData['content']['*/*']['schema']['type'], $responseData['content']['*/*']['schema']['format']) + && $responseData['content']['*/*']['schema']['type'] === 'string' + && $responseData['content']['*/*']['schema']['format'] === 'binary') { + Logger::debug("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); + } else { + Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); } - } elseif (isset($responseData['content']['*/*']['schema']['type'], $responseData['content']['*/*']['schema']['format']) - && $responseData['content']['*/*']['schema']['type'] === 'string' - && $responseData['content']['*/*']['schema']['format'] === 'binary') { - Logger::debug("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); - } else { - Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); } } } - } - $scopedSchemas = []; - while ($usedSchema = array_shift($usedSchemas)) { - if (!str_starts_with($usedSchema, '#/components/schemas/')) { - continue; - } + $scopedSchemas = []; + while ($usedSchema = array_shift($usedSchemas)) { + if (!str_starts_with($usedSchema, '#/components/schemas/')) { + continue; + } - $schemaName = substr($usedSchema, strlen('#/components/schemas/')); + $schemaName = substr($usedSchema, strlen('#/components/schemas/')); - if (!isset($schemas[$schemaName])) { - Logger::error("app", "Schema $schemaName used by scope $scope is not defined"); - } + if (!isset($schemas[$schemaName])) { + Logger::error("app", "Schema $schemaName used by scope $scope is not defined"); + } - $newRefs = Helpers::collectUsedRefs($schemas[$schemaName]); - foreach ($newRefs as $newRef) { - if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { - $usedSchemas[] = $newRef; + $newRefs = Helpers::collectUsedRefs($schemas[$schemaName]); + foreach ($newRefs as $newRef) { + if (!isset($scopedSchemas[substr($newRef, strlen('#/components/schemas/'))])) { + $usedSchemas[] = $newRef; + } } + + $scopedSchemas[$schemaName] = $schemas[$schemaName]; } - $scopedSchemas[$schemaName] = $schemas[$schemaName]; - } + if (isset($schemas['Capabilities'])) { + $scopedSchemas['Capabilities'] = $schemas['Capabilities']; + } + if (isset($schemas['PublicCapabilities'])) { + $scopedSchemas['PublicCapabilities'] = $schemas['PublicCapabilities']; + } - if (isset($schemas['Capabilities'])) { - $scopedSchemas['Capabilities'] = $schemas['Capabilities']; + $openapiScope['components']['schemas'] = $scopedSchemas; } - if (isset($schemas['PublicCapabilities'])) { - $scopedSchemas['PublicCapabilities'] = $schemas['PublicCapabilities']; - } - - $openapiScope['components']['schemas'] = $scopedSchemas; $startExtension = strrpos($out, '.'); if ($startExtension !== false) { From 84c976bd6c2c87f5df2e720675b0d269d42b997f Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 13:20:59 +0100 Subject: [PATCH 22/29] fix(tests): Adjust some copy-paste description and add openapi-full.json Signed-off-by: Joas Schilling --- .../Controller/AdminSettingsController.php | 2 +- tests/lib/Controller/FederationController.php | 4 +- tests/lib/Controller/SettingsController.php | 4 +- tests/openapi-administration.json | 2 +- tests/openapi-federation.json | 2 +- tests/openapi-full.json | 2325 +++++++++++++++++ tests/openapi.json | 6 +- 7 files changed, 2335 insertions(+), 10 deletions(-) create mode 100644 tests/openapi-full.json diff --git a/tests/lib/Controller/AdminSettingsController.php b/tests/lib/Controller/AdminSettingsController.php index dcd461a..b16a9cf 100644 --- a/tests/lib/Controller/AdminSettingsController.php +++ b/tests/lib/Controller/AdminSettingsController.php @@ -68,7 +68,7 @@ public function movedToSettingsTag(): DataResponse { } /** - * Route in default scope with tags + * Route in default scope with tags but without named parameters on the attribute * * @return DataResponse, array{}> * diff --git a/tests/lib/Controller/FederationController.php b/tests/lib/Controller/FederationController.php index df57737..a656530 100644 --- a/tests/lib/Controller/FederationController.php +++ b/tests/lib/Controller/FederationController.php @@ -43,7 +43,7 @@ class FederationController extends OCSController { /** * @NoAdminRequired * - * Route is ignored because of scope on the controller + * Route is in federation scope as per controller scope * * @return DataResponse, array{}> * @@ -56,7 +56,7 @@ public function federationByController(): DataResponse { /** * @NoAdminRequired * - * Route is only in the default scope + * Route is only in the default scope (moved from federation) * * @return DataResponse, array{}> * diff --git a/tests/lib/Controller/SettingsController.php b/tests/lib/Controller/SettingsController.php index 09b7726..28a76cd 100644 --- a/tests/lib/Controller/SettingsController.php +++ b/tests/lib/Controller/SettingsController.php @@ -124,7 +124,7 @@ public function defaultAndAdminScope(): DataResponse { /** * @NoAdminRequired * - * Route is ignored because of scope on the controller + * Route is referencing nested schemas * * @return DataResponse, array{}> * @@ -137,7 +137,7 @@ public function nestedSchemas(): DataResponse { /** * @NoAdminRequired * - * Route is ignored because of scope on the controller + * Route is referencing a schema which is a list of schemas * * @return DataResponse * diff --git a/tests/openapi-administration.json b/tests/openapi-administration.json index eea0e65..31d5a58 100644 --- a/tests/openapi-administration.json +++ b/tests/openapi-administration.json @@ -228,7 +228,7 @@ "/ocs/v2.php/apps/notifications/api/{apiVersion}/moved-with-unnamed-tag": { "post": { "operationId": "settings-admin-settings-moved-to-settings-tag-unnamed", - "summary": "Route in default scope with tags", + "summary": "Route in default scope with tags but without named parameters on the attribute", "description": "This endpoint requires admin access", "tags": [ "settings", diff --git a/tests/openapi-federation.json b/tests/openapi-federation.json index bd686c9..1df8ce0 100644 --- a/tests/openapi-federation.json +++ b/tests/openapi-federation.json @@ -50,7 +50,7 @@ "/ocs/v2.php/apps/notifications/api/{apiVersion}/controller-scope": { "post": { "operationId": "federation-federation-by-controller", - "summary": "Route is ignored because of scope on the controller", + "summary": "Route is in federation scope as per controller scope", "tags": [ "federation" ], diff --git a/tests/openapi-full.json b/tests/openapi-full.json new file mode 100644 index 0000000..5315901 --- /dev/null +++ b/tests/openapi-full.json @@ -0,0 +1,2325 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "notifications-full", + "version": "0.0.1", + "description": "This app provides a backend and frontend for the notification API available in Nextcloud.", + "license": { + "name": "agpl" + } + }, + "components": { + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + }, + "bearer_auth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Collection": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Item" + } + }, + "Item": { + "type": "object", + "required": [ + "label", + "link", + "type", + "primary" + ], + "properties": { + "label": { + "type": "string" + }, + "link": { + "type": "string" + }, + "type": { + "type": "string" + }, + "primary": { + "type": "boolean" + } + } + }, + "Notification": { + "type": "object", + "required": [ + "notification_id", + "app", + "user", + "datetime", + "object_type", + "object_id", + "subject", + "message", + "link", + "actions" + ], + "properties": { + "notification_id": { + "type": "integer", + "format": "int64" + }, + "app": { + "type": "string" + }, + "user": { + "type": "string" + }, + "datetime": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "message": { + "type": "string" + }, + "link": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationAction" + } + }, + "subjectRich": { + "type": "string" + }, + "subjectRichParameters": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "messageRich": { + "type": "string" + }, + "messageRichParameters": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "icon": { + "type": "string" + }, + "shouldNotify": { + "type": "boolean" + } + } + }, + "NotificationAction": { + "type": "object", + "required": [ + "label", + "link", + "type", + "primary" + ], + "properties": { + "label": { + "type": "string" + }, + "link": { + "type": "string" + }, + "type": { + "type": "string" + }, + "primary": { + "type": "boolean" + } + } + }, + "OCSMeta": { + "type": "object", + "required": [ + "status", + "statuscode" + ], + "properties": { + "status": { + "type": "string" + }, + "statuscode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "totalitems": { + "type": "string" + }, + "itemsperpage": { + "type": "string" + } + } + }, + "PushDevice": { + "allOf": [ + { + "$ref": "#/components/schemas/PushDeviceBase" + }, + { + "type": "object", + "required": [ + "publicKey", + "signature" + ], + "properties": { + "publicKey": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + } + ] + }, + "PushDeviceBase": { + "type": "object", + "required": [ + "deviceIdentifier" + ], + "properties": { + "deviceIdentifier": { + "type": "string" + } + } + } + } + }, + "paths": { + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-admin": { + "post": { + "operationId": "admin_settings-admin-scope-implicit-from-admin-required", + "summary": "Route is only in the admin scope because there is no \"NoAdminRequired\" annotation or attribute", + "description": "This endpoint requires admin access", + "tags": [ + "admin_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/moved-with-tag": { + "post": { + "operationId": "settings-admin-settings-moved-to-settings-tag", + "summary": "Route in default scope with tags", + "description": "This endpoint requires admin access", + "tags": [ + "settings", + "admin-settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/moved-with-unnamed-tag": { + "post": { + "operationId": "settings-admin-settings-moved-to-settings-tag-unnamed", + "summary": "Route in default scope with tags but without named parameters on the attribute", + "description": "This endpoint requires admin access", + "tags": [ + "settings", + "admin-settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/admin-scope": { + "post": { + "operationId": "settings-moved-to-admin-scope", + "summary": "Route is only in the admin scope due to defined scope", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/PushDevice" + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-and-admin-scope": { + "post": { + "operationId": "settings-default-and-admin-scope", + "summary": "Route is in admin and default scope", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-of-int": { + "post": { + "operationId": "settings-list-of-int-parameters", + "summary": "A route with a limited set of possible integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of objects", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/min-max": { + "post": { + "operationId": "settings-int-parameter-with-min-and-max", + "summary": "A route with a min and max integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Between 5 and 10", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 5, + "maximum": 10 + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/min": { + "post": { + "operationId": "settings-int-parameter-with-min", + "summary": "A route with a min integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "At least 5", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 5 + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/max": { + "post": { + "operationId": "settings-int-parameter-with-max", + "summary": "A route with a max integers", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "At most 10", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "maximum": 10 + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/mixed-list-one": { + "post": { + "operationId": "settings-list-of-int-string-and-one-bool", + "summary": "A route with a list of 2 integers, 2 strings and 1 boolean", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "weird", + "in": "query", + "description": "Weird list", + "required": true, + "schema": { + "oneOf": [ + { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + { + "type": "string", + "enum": [ + "yes", + "no" + ] + }, + { + "type": "boolean", + "enum": [ + true + ] + } + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/mixed-list-all": { + "post": { + "operationId": "settings-list-of-int-string-and-all-bools", + "summary": "A route with a list of 2 integers, 2 strings and 1 boolean", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "weird", + "in": "query", + "description": "Weird list", + "required": true, + "schema": { + "oneOf": [ + { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + { + "type": "string", + "enum": [ + "yes", + "no" + ] + }, + { + "type": "boolean", + "enum": [ + true, + false + ] + } + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean": { + "post": { + "operationId": "settings-boolean-parameter-required", + "summary": "A route with required boolean", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "Boolean required", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-default-false": { + "post": { + "operationId": "settings-boolean-parameter-default-false", + "summary": "A route with boolean defaulting to false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "Boolean defaulting to false", + "schema": { + "type": "integer", + "default": 0, + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-default-true": { + "post": { + "operationId": "settings-boolean-parameter-default-true", + "summary": "A route with boolean defaulting to true", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "Boolean defaulting to true", + "schema": { + "type": "integer", + "default": 1, + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-true": { + "post": { + "operationId": "settings-boolean-true-parameter", + "summary": "A route with boolean or true", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "boolean or true", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-false": { + "post": { + "operationId": "settings-boolean-false-parameter", + "summary": "A route with boolean or false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "boolean or false", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/boolean-true-false": { + "post": { + "operationId": "settings-boolean-true-false-parameter", + "summary": "A route with boolean or true or false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "boolean or true or false", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/true-false": { + "post": { + "operationId": "settings-true-false-parameter", + "summary": "A route with true or false", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "yesOrNo", + "in": "query", + "description": "true or false", + "required": true, + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/string-value": { + "post": { + "operationId": "settings-string-value-parameter", + "summary": "A route with string or 'test'", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "value", + "in": "query", + "description": "string or 'test'", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/int-value": { + "post": { + "operationId": "settings-int-value-parameter", + "summary": "A route with int or 0", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "value", + "in": "query", + "description": "int or 0", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/numeric": { + "post": { + "operationId": "settings-numeric-parameter", + "summary": "A route with numeric", + "description": "This endpoint requires admin access", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "value", + "in": "query", + "description": "Some numeric value", + "required": true, + "schema": { + "type": "number" + } + }, + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Admin settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-admin-overwritten": { + "post": { + "operationId": "admin_settings-moved-to-default-scope", + "summary": "Route is in the default scope because the method overwrites with the Attribute", + "description": "This endpoint requires admin access", + "tags": [ + "admin_settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-scope": { + "post": { + "operationId": "federation-moved-to-default-scope", + "summary": "Route is only in the default scope (moved from federation)", + "tags": [ + "federation" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "Personal settings updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/nested-schemas": { + "post": { + "operationId": "settings-nested-schemas", + "summary": "Route is referencing nested schemas", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Notification" + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-schemas": { + "post": { + "operationId": "settings-list-schemas", + "summary": "Route is referencing a schema which is a list of schemas", + "tags": [ + "settings" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Collection" + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/notifications/api/{apiVersion}/controller-scope": { + "post": { + "operationId": "federation-federation-by-controller", + "summary": "Route is in federation scope as per controller scope", + "tags": [ + "federation" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v2" + ], + "default": "v2" + } + }, + { + "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": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/tests/openapi.json b/tests/openapi.json index ce4f56d..b67de7c 100644 --- a/tests/openapi.json +++ b/tests/openapi.json @@ -249,7 +249,7 @@ "/ocs/v2.php/apps/notifications/api/{apiVersion}/default-scope": { "post": { "operationId": "federation-moved-to-default-scope", - "summary": "Route is only in the default scope", + "summary": "Route is only in the default scope (moved from federation)", "tags": [ "federation" ], @@ -391,7 +391,7 @@ "/ocs/v2.php/apps/notifications/api/{apiVersion}/nested-schemas": { "post": { "operationId": "settings-nested-schemas", - "summary": "Route is ignored because of scope on the controller", + "summary": "Route is referencing nested schemas", "tags": [ "settings" ], @@ -467,7 +467,7 @@ "/ocs/v2.php/apps/notifications/api/{apiVersion}/list-schemas": { "post": { "operationId": "settings-list-schemas", - "summary": "Route is ignored because of scope on the controller", + "summary": "Route is referencing a schema which is a list of schemas", "tags": [ "settings" ], From 0d8fb4b6b6e166411b19f4cced5a18fb0af4ba7d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 12 Jan 2024 14:43:44 +0100 Subject: [PATCH 23/29] fix(scopes): Improve help when not reading schemas Signed-off-by: Joas Schilling --- generate-spec | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/generate-spec b/generate-spec index 6ff8041..ca11032 100755 --- a/generate-spec +++ b/generate-spec @@ -803,17 +803,28 @@ foreach ($scopePaths as $scope => $paths) { foreach ($paths as $url => $urlRoutes) { foreach ($urlRoutes as $httpMethod => $routeData) { foreach ($routeData['responses'] as $statusCode => $responseData) { - if (isset($responseData['content']['application/json'])) { - if (is_array($responseData['content']['application/json']['schema'])) { - $newSchemas = Helpers::collectUsedRefs($responseData['content']['application/json']['schema']); - $usedSchemas = array_merge($usedSchemas, $newSchemas); + if (empty($responseData['content'])) { + continue; + } + + foreach ($responseData['content'] as $contentType => $contentData) { + if ($contentType === 'application/json') { + if (isset($contentData['schema']) && is_array($contentData['schema'])) { + $newSchemas = Helpers::collectUsedRefs($contentData['schema']); + $usedSchemas = array_merge($usedSchemas, $newSchemas); + } + } elseif ( + ( + in_array($contentType, ['*/*', 'text/css', 'application/octet-stream'], true) + || str_starts_with($contentType, 'image/') + ) + && isset($contentData['schema']['type'], $contentData['schema']['format']) + && $contentData['schema']['type'] === 'string' + && $contentData['schema']['format'] === 'binary') { + Logger::debug("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); + } elseif (isset($contentData['schema'])) { + Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode with content type $contentType"); } - } elseif (isset($responseData['content']['*/*']['schema']['type'], $responseData['content']['*/*']['schema']['format']) - && $responseData['content']['*/*']['schema']['type'] === 'string' - && $responseData['content']['*/*']['schema']['format'] === 'binary') { - Logger::debug("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); - } else { - Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode"); } } } From 5d78669d873abb73ce55bf01219c60dfbc822cc9 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Jan 2024 13:49:15 +0100 Subject: [PATCH 24/29] fix(tags): Panic when the tags are not an array list Signed-off-by: Joas Schilling --- src/Helpers.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Helpers.php b/src/Helpers.php index d37c9c1..2dd7496 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -7,7 +7,6 @@ use PhpParser\Node\Arg; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Class_; @@ -232,12 +231,11 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n } if (!$arg->value instanceof Array_) { - - continue; + Logger::panic($routeName, 'Can not read value of tags provided in OpenAPI attribute for route ' . $routeName); } foreach ($arg->value->items as $item) { - if ($item instanceof ArrayItem && $item->value instanceof String_) { + if ($item?->value instanceof String_) { $foundTags[] = $item->value->value; } } @@ -246,8 +244,6 @@ public static function getOpenAPIAttributeTagsByScope(ClassMethod|Class_|Node $n if (!empty($foundTags)) { $tags[$foundScopeName ?: $defaultScope] = $foundTags; } - - } } } From 4d20003eb06809bd1f5187bfe91bdcc53bf0c902 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Jan 2024 13:52:35 +0100 Subject: [PATCH 25/29] fix(scopes): Inspect all responses for schemas Signed-off-by: Joas Schilling --- generate-spec | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/generate-spec b/generate-spec index ca11032..0f3d5d6 100755 --- a/generate-spec +++ b/generate-spec @@ -655,7 +655,7 @@ foreach ($routes as $scope => $scopeRoutes) { } $operation = array_merge( - ["operationId" => strtolower(implode("-", $operationId))], + ["operationId" => strtolower(implode("-", $operationId))], $route->controllerMethod->summary != null ? ["summary" => $route->controllerMethod->summary] : [], count($route->controllerMethod->description) > 0 ? ["description" => implode("\n", $route->controllerMethod->description)] : [], $route->controllerMethod->isDeprecated ? ["deprecated" => true] : [], @@ -808,22 +808,9 @@ foreach ($scopePaths as $scope => $paths) { } foreach ($responseData['content'] as $contentType => $contentData) { - if ($contentType === 'application/json') { - if (isset($contentData['schema']) && is_array($contentData['schema'])) { - $newSchemas = Helpers::collectUsedRefs($contentData['schema']); - $usedSchemas = array_merge($usedSchemas, $newSchemas); - } - } elseif ( - ( - in_array($contentType, ['*/*', 'text/css', 'application/octet-stream'], true) - || str_starts_with($contentType, 'image/') - ) - && isset($contentData['schema']['type'], $contentData['schema']['format']) - && $contentData['schema']['type'] === 'string' - && $contentData['schema']['format'] === 'binary') { - Logger::debug("app", "Binary response from '$httpMethod $url' - Skipping schema reading"); - } elseif (isset($contentData['schema'])) { - Logger::warning("app", "Could not read used schemas for response to '$httpMethod $url' with status code $statusCode with content type $contentType"); + if (isset($contentData['schema']) && is_array($contentData['schema'])) { + $newSchemas = Helpers::collectUsedRefs($contentData['schema']); + $usedSchemas = array_merge($usedSchemas, $newSchemas); } } } From ed40c152c2f84b0468868be65cfa29c4afa58ff0 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Jan 2024 14:01:20 +0100 Subject: [PATCH 26/29] fix(scopes): Fix counting the routes Signed-off-by: Joas Schilling --- generate-spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generate-spec b/generate-spec index 0f3d5d6..f889e96 100755 --- a/generate-spec +++ b/generate-spec @@ -861,6 +861,6 @@ foreach ($scopePaths as $scope => $paths) { } file_put_contents($scopeOut, json_encode($openapiScope, Helpers::jsonFlags())); -} -Logger::info("app", "Generated ". count($routes). " routes!"); + Logger::info('app', 'Generated scope ' . $scope . ' with ' . count($openapiScope['paths']) . ' routes!'); +} From 79517db3ce0f4b8cb8f434b492152fcec2edab3d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Jan 2024 16:05:08 +0100 Subject: [PATCH 27/29] fix(scopes): Fix empty path arrays Signed-off-by: Joas Schilling --- generate-spec | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/generate-spec b/generate-spec index f889e96..b6ba981 100755 --- a/generate-spec +++ b/generate-spec @@ -783,10 +783,6 @@ if (!$hasSingleScope) { foreach ($scopePaths as $scope => $paths) { $openapiScope = $openapi; - if (count($paths) == 0) { - $paths = new stdClass(); - } - $scopeSuffix = ($hasSingleScope || $scope === 'default') ? '' : '-' . $scope; $openapiScope['info']['title'] .= $scopeSuffix; $openapiScope['paths'] = $paths; @@ -849,6 +845,12 @@ foreach ($scopePaths as $scope => $paths) { $openapiScope['components']['schemas'] = $scopedSchemas; } + $pathsCount = count($openapiScope['paths']); + if ($pathsCount === 0) { + // Make sure the paths array is always a dictionary + $openapiScope['paths'] = new \stdClass(); + } + $startExtension = strrpos($out, '.'); if ($startExtension !== false) { // Path + filename (without extension) @@ -862,5 +864,5 @@ foreach ($scopePaths as $scope => $paths) { file_put_contents($scopeOut, json_encode($openapiScope, Helpers::jsonFlags())); - Logger::info('app', 'Generated scope ' . $scope . ' with ' . count($openapiScope['paths']) . ' routes!'); + Logger::info('app', 'Generated scope ' . $scope . ' with ' . $pathsCount . ' routes!'); } From 443740f66245daa6fc6f5ed0a4bf6ebd30973263 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Jan 2024 16:28:34 +0100 Subject: [PATCH 28/29] fix(schemas): Make sure schemas are always a dictionary Signed-off-by: Joas Schilling --- generate-spec | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/generate-spec b/generate-spec index b6ba981..19971c0 100755 --- a/generate-spec +++ b/generate-spec @@ -842,6 +842,10 @@ foreach ($scopePaths as $scope => $paths) { $scopedSchemas['PublicCapabilities'] = $schemas['PublicCapabilities']; } + if (count($scopedSchemas) === 0) { + $scopedSchemas = new \stdClass(); + } + $openapiScope['components']['schemas'] = $scopedSchemas; } From a479a343d7b91409a5b44cd1facd0a06d07fa3c3 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 17 Jan 2024 16:29:00 +0100 Subject: [PATCH 29/29] fix(scopes): Don't generate "full" scope when there is none Signed-off-by: Joas Schilling --- generate-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate-spec b/generate-spec index 19971c0..147358c 100755 --- a/generate-spec +++ b/generate-spec @@ -773,7 +773,7 @@ if ($useTags) { $openapi["tags"] = $tags; } -$hasSingleScope = count($scopePaths) === 1; +$hasSingleScope = count($scopePaths) <= 1; $fullScopePathArrays = []; if (!$hasSingleScope) {