Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions generate-spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@
}
}

/** @var array<string, list<Route>> $routes */
$routes = [];
foreach ($controllers as $controllerName => $stmts) {
$controllerClass = null;
Expand Down Expand Up @@ -465,7 +466,6 @@
$isIgnored = Helpers::classMethodHasAnnotationOrAttribute($methodFunction, 'IgnoreOpenAPI');
$isPasswordConfirmation = Helpers::classMethodHasAnnotationOrAttribute($methodFunction, 'PasswordConfirmationRequired');
$isExApp = Helpers::classMethodHasAnnotationOrAttribute($methodFunction, 'ExAppRequired');
$isCORS = Helpers::classMethodHasAnnotationOrAttribute($methodFunction, 'CORS');
$scopes = Helpers::getOpenAPIAttributeScopes($classMethod, $routeName);

if ($isIgnored) {
Expand Down Expand Up @@ -519,7 +519,7 @@
];
}

$classMethodInfo = ControllerMethod::parse($routeName, $definitions, $methodFunction, $isAdmin, $isDeprecated, $isPasswordConfirmation, $isCORS);
$classMethodInfo = ControllerMethod::parse($routeName, $definitions, $methodFunction, $isPublic, $isAdmin, $isDeprecated, $isPasswordConfirmation, $isCORS, $isOCS);
if (count($classMethodInfo->responses) == 0) {
Logger::error($routeName, 'Returns no responses');
continue;
Expand Down Expand Up @@ -724,10 +724,11 @@
} else {
$mergedContentTypeResponses[$contentType] = [
'schema' => [
[$hasEmpty ? 'anyOf' : 'oneOf' => array_map(function (ControllerMethodResponse $response) use ($route): \stdClass|array {
// At least one should match, but it's possible that multiple match, so oneOf can't be used.
'anyOf' => array_map(function (ControllerMethodResponse $response) use ($route): stdClass|array {
$schema = Helpers::cleanEmptyResponseArray($response->type->toArray());
return Helpers::wrapOCSResponse($route, $response, $schema);
}, $uniqueResponses)],
}, $uniqueResponses),
],
];
}
Expand Down
42 changes: 42 additions & 0 deletions src/ControllerMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,12 @@ public function __construct(
public static function parse(string $context,
array $definitions,
ClassMethod $method,
bool $isPublic,
bool $isAdmin,
bool $isDeprecated,
bool $isPasswordConfirmation,
bool $isCORS,
bool $isOCS,
): ControllerMethod {
global $phpDocParser, $lexer, $nodeFinder, $allowMissingDocs;

Expand Down Expand Up @@ -409,6 +411,46 @@ public static function parse(string $context,
Logger::error($context, 'Missing @return annotation');
}

if (!$isPublic || $isAdmin) {
$statusCodes = [];
if (!$isPublic) {
$responseDescriptions[401] ??= 'Current user is not logged in';
$statusCodes[] = 401;
}
if ($isAdmin) {
$responseDescriptions[403] ??= 'Logged in account must be an admin';
$statusCodes[] = 403;
}

foreach ($statusCodes as $statusCode) {
if ($isOCS) {
$responses[] = new ControllerMethodResponse(
'DataResponse',
$statusCode,
'application/json',
new OpenApiType($context),
);
} else {
$responses[] = new ControllerMethodResponse(
'JsonResponse',
$statusCode,
'application/json',
new OpenApiType(
$context,
type: 'object',
properties: [
'message' => new OpenApiType(
$context,
type: 'string',
),
],
required: ['message'],
),
);
}
}
}

$responseStatusCodes = array_unique(array_map(static fn (ControllerMethodResponse $response): int => $response->statusCode, $responses));
$unusedResponseDescriptions = array_diff(array_keys($responseDescriptions), $responseStatusCodes);
if ($unusedResponseDescriptions !== []) {
Expand Down
4 changes: 2 additions & 2 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ public static function wrapOCSResponse(Route $route, ControllerMethodResponse $r
return $schema;
}

public static function cleanEmptyResponseArray(array $schema): array|stdClass {
if (array_key_exists('type', $schema) && $schema['type'] === 'array' && array_key_exists('maxItems', $schema) && $schema['maxItems'] === 0) {
public static function cleanEmptyResponseArray(array|stdClass $schema): array|stdClass {
if (is_array($schema) && array_key_exists('type', $schema) && $schema['type'] === 'array' && array_key_exists('maxItems', $schema) && $schema['maxItems'] === 0) {
return new stdClass();
}

Expand Down
5 changes: 5 additions & 0 deletions tests/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@
['name' => 'Settings#samePathPost', 'url' => '/api/{apiVersion}/same-path', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#requestHeader', 'url' => '/api/{apiVersion}/request-header', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#requestParams', 'url' => '/api/{apiVersion}/request-params', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#publicPageAnnotation', 'url' => '/api/{apiVersion}/public-page/annotation', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#publicPageAttribute', 'url' => '/api/{apiVersion}/public-page/attribute', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#mergedResponses', 'url' => '/api/{apiVersion}/merged-responses', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#custom401', 'url' => '/api/{apiVersion}/custom/401', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#custom403', 'url' => '/api/{apiVersion}/custom/403', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'V1\SubDir#subDirRoute', 'url' => '/sub-dir', 'verb' => 'GET'],
],
];
61 changes: 61 additions & 0 deletions tests/lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\CORS;
use OCP\AppFramework\Http\Attribute\IgnoreOpenAPI;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\RequestHeader;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;

Expand Down Expand Up @@ -790,4 +793,62 @@ public function requestParams(): DataResponse {

return new DataResponse();
}

/**
* A public page with annotation.
*
* @PublicPage
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
*
* 200: Admin settings updated
*/
public function publicPageAnnotation(): DataResponse {
return new DataResponse();
}

/**
* A public page with attribute.
*
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
*
* 200: Admin settings updated
*/
#[PublicPage]
public function publicPageAttribute(): DataResponse {
return new DataResponse();
}

/**
* A with merged responses.
*
* @return DataResponse<Http::STATUS_OK, array{a: string}, array{}>|JSONResponse<Http::STATUS_OK, array{b: int}, array{}>
*
* 200: Admin settings updated
*/
public function mergedResponses(): DataResponse|JSONResponse {
return new DataResponse();
}

/**
* A page with a custom 401.
*
* @return DataResponse<Http::STATUS_UNAUTHORIZED, array{a: string}, array{}>
*
* 401: Admin settings updated
*/
#[NoAdminRequired]
public function custom401(): DataResponse {
return new DataResponse();
}

/**
* A page with a custom 403.
*
* @return DataResponse<Http::STATUS_FORBIDDEN, array{a: string}, array{}>
*
* 403: Admin settings updated
*/
public function custom403(): DataResponse {
return new DataResponse();
}
}
Loading
Loading