Tutorial de OpenAPI para OCS¶
Resumen¶
Esta página explica, para quienes desarrollan apps, cómo añadir compatibilidad con OpenAPI a una app para generar automáticamente su especificación desde el código: requisitos, buenas prácticas, un tutorial paso a paso y la generación de la especificación con openapi-extractor.
Fuente: documentación de Nextcloud, «developer_manual/client_apis/OCS/ocs-openapi», versión 3ad9158, licencia CC BY 3.0; traducida y adaptada para APS Conecta Gestión.
Esta página explica cómo añadir compatibilidad con OpenAPI a una app para poder generar automáticamente una especificación OpenAPI a partir del código.
Leer el tutorial completo antes de empezar a adaptar la app.
No hace falta entenderlo todo de antemano.
La herramienta openapi-extractor emitirá advertencias y fallará si algo está fundamentalmente roto.
Ejecutarla pronto y a menudo; normalmente señalará lo que hay que corregir.
Psalm también ayuda a validar los cambios y a detectar problemas de tipos antes de que se conviertan en problemas en tiempo de ejecución.
Requisitos y requisitos previos¶
Antes de empezar, asegurarse de que se cumplen los siguientes requisitos:
La app es compatible con >=APS Conecta Gestión 28. APS Conecta Gestión 28 es la primera versión que incluye los cambios del framework necesarios para la extracción de OpenAPI.
Psalm está instalado y configurado. Psalm se usa para validar los tipos y las formas de los valores devueltos, de modo que
openapi-extractorpueda inferir esquemas precisos.Instalar Psalm en la app como se explica en https://psalm.dev/docs/running_psalm/installation.
Se necesita Psalm
>=5.9.0. Las versiones anteriores contienen un error que impide que funcionen los cambios de este tutorial.Instalar y activar las extensiones necesarias como se explica en Extensiones de PHP necesarias.
Configurar los siguientes manejadores de incidencias en la configuración de Psalm (ver https://psalm.dev/docs/running_psalm/dealing_with_code_issues para un tutorial):
<LessSpecificReturnStatement errorLevel="error"/> <LessSpecificReturnType errorLevel="error"/> <LessSpecificImplementedReturnType errorLevel="error"/> <MoreSpecificReturnType errorLevel="error"/>
openapi-extractorestá instalado. Esta herramienta genera la especificación OpenAPI a partir del código del lado del servidor.Instalar
openapi-extractoren la app como se explica en https://github.com/nextcloud/openapi-extractor.
Buenas prácticas¶
Tener en cuenta que tras esta sección hay un tutorial paso a paso. Se puede leer primero el tutorial y volver luego aquí como guía.
En resumen:
Preferir endpoints OCS (
OCSController+DataResponse) para las API públicasAñadir tipos explícitos en todas partes (parámetros, métodos auxiliares y tipos de retorno)
Usar
\stdClasspara representar un objeto JSON vacíoEn los endpoints OCS, lanzar solo OCS*Exceptions
Mantener coherentes las formas de las respuestas dentro de cada grupo de códigos de estado (2xx juntos, 4xx juntos)
Usar
setHeaders()en lugar deaddHeader()Evitar envoltorios de errores genéricos que hacen posible cualquier error en cualquier endpoint
Añadir descripciones para los controladores, los parámetros, los métodos y los códigos de estado
Diseño y coherencia de la API¶
PREFERIR exponer las API mediante OCS¶
Ofrece una forma más estandarizada y más sencilla de escribir las API. Los demás métodos se consideran heredados. Para más detalles, ver OCS.
Ejemplos
Incorrecto:
class SomeController extends ApiController {
...
public function someControllerMethod(): JSONResponse {
...
return new JSONResponse(...);
}
}
Correcto:
class SomeController extends OCSController {
...
public function someControllerMethod(): DataResponse {
...
return new DataResponse(...);
}
}
Tratar CON CUIDADO los valores vacíos en las respuestas JSON¶
Al definir las respuestas de la API, es importante dejar explícito si un valor vacío debe ser null, un objeto vacío ({}) o un array vacío ([]) en el JSON resultante. El tipo de PHP que se devuelve lo determina, y usar el equivocado puede producir fácilmente resultados confusos o incoherentes para quienes consumen la API.
Nota
En PHP, null, [] y new \stdClass() son tipos distintos y se serializan a valores distintos en JSON. Esto es especialmente importante para quienes consumen OpenAPI, que a menudo esperan un tipo coherente.
Así se serializan los valores de PHP a JSON:
Valor de PHP |
Salida JSON |
|---|---|
null |
null |
new \stdClass() |
{} |
[] |
[] |
Usar
nullpara indicar que un valor está explícitamente ausente. Esto es lo preferible para la mayoría de las respuestas «vacías».Usar
new \stdClass()si y solo si el cliente espera un objeto vacío ({}) en lugar de null. Esto lo exigen a veces contratos de esquema que siempre esperan la forma de un objeto, aunque esté vacío.
Importante
Devolver new \stdClass() como respuesta de la API requiere al menos APS Conecta Gestión 28 para que se serialice de forma fiable a {}.
Evitar devolver
[]en endpoints de los que se espera un objeto JSON, ya que se serializará a un array JSON ([]), lo que obliga a quienes consumen la API a lidiar con tipos impredecibles.
Si se modifican o amplían API existentes y no es posible usar null o \stdClass() sin romper la compatibilidad con versiones anteriores, se puede tipar el resultado como list<empty> para indicar que se espera un array vacío.
Ejemplos
Incorrecto (devuelve un array vacío en lugar de un objeto vacío o null):
/**
* @return DataResponse<Http::STATUS_OK, array, array{}>
*/
public function someControllerMethod() {
// ...
return new DataResponse([]);
}
Correcto (datos vacíos como null):
/**
* @return DataResponse<Http::STATUS_OK, null, array{}>
*/
public function someControllerMethod() {
// ...
return new DataResponse(null); // Serializes to: null in JSON
}
Correcto (objeto vacío explícito):
/**
* @return DataResponse<Http::STATUS_OK, \stdClass, array{}>
*/
public function someControllerMethod() {
// ...
return new DataResponse(new \stdClass()); // Serializes to: {} in JSON
}
Para casos heredados o de compatibilidad (lista vacía explícita):
/**
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
*/
public function someControllerMethod() {
// ...
return new DataResponse([]); // Serializes to: [] in JSON
}
USAR las mismas estructuras de datos para el mismo grupo de respuestas¶
Se recomienda usar null para representar datos vacíos.
Mantener coherentes las formas de las respuestas dentro de cada grupo de códigos de estado: todas las respuestas 2xx deben usar la misma estructura de datos, y
todas las respuestas 4xx deben usar la misma estructura de datos.
Ejemplos
Incorrecto:
/**
* @return DataResponse<Http::STATUS_OK, array{name: string}, array{}>|DataResponse<Http::STATUS_CREATED, array{id: int, name: string}, array{}>
*/
public function someControllerMethod() {
...
if (...) {
return new DataResponse(["name" => $name], Http::STATUS_OK);
} else {
return new DataResponse(["id" => $id, "name" => $name], Http::STATUS_CREATED);
}
}
/**
* @return DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{message: string}, array{}>
*/
public function someControllerMethod() {
...
if (...) {
return new DataResponse(["error" => "bad request"], Http::STATUS_BAD_REQUEST);
} else {
return new DataResponse(["message" => "forbidden"], Http::STATUS_FORBIDDEN);
}
}
Correcto:
/**
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{id: int, name: string}, array{}>
*/
public function someControllerMethod() {
...
if (...) {
return new DataResponse(["id" => $id, "name" => $name], Http::STATUS_OK);
} else {
return new DataResponse(["id" => $id, "name" => $name], Http::STATUS_CREATED);
}
}
/**
* @return DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN, array{error: string}, array{}>
*/
public function someControllerMethod() {
...
if (...) {
return new DataResponse(["error" => "bad request"], Http::STATUS_BAD_REQUEST);
} else {
return new DataResponse(["error" => "forbidden"], Http::STATUS_FORBIDDEN);
}
}
TENER EN CUENTA cómo se usará la API¶
Al construir la API, probablemente solo se piense en cómo implementarla de la forma más sencilla o mejor. Hay que considerar lo que el código implica para quien intente usar la API a través de la especificación OpenAPI.
Un error habitual es un manejador de errores genérico («catch-all») que se reutiliza en muchos endpoints. Son estupendos para la implementación de la API, porque ofrecen una solución sencilla que lo captura todo y no hay que preocuparse de manejar cada error correctamente. No son buenos para la documentación OpenAPI ni para quienes consumen la API, porque verán que cualquier error puede producirse en cualquier endpoint, lo que la mayoría de las veces no es correcto. En su lugar, conviene implementar el manejo de errores a mano y devolver solo los errores pertinentes allí donde realmente pueden aparecer. Se pueden seguir usando métodos auxiliares con manejadores de incidencias genéricos donde tenga sentido, pero solo si todos los métodos del controlador que llaman a ese método auxiliar lanzan realmente las excepciones capturadas.
En particular, evitar patrones que hacen que cada endpoint parezca lanzar cada error que maneja un método auxiliar compartido, aunque el endpoint no pueda producir realmente esos errores.
Ejemplos
Incorrecto:
/**
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*/
public function someControllerMethod() {
return $this->handleError(function() {
...
if (...) {
throw new PermissionError("some error");
}
...
return ["message" => "some message"];
});
}
/**
* @template T
* @param Closure():T $callback
*
* @return DataResponse<Http::STATUS_OK, T, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*/
private function handleError(Closure $callback): DataResponse {
try {
return new DataResponse($callback());
} catch (PermissionError $e) {
$message = ["error" => $e->getMessage()];
return new DataResponse($message, Http::STATUS_FORBIDDEN);
} catch (NotFoundError $e) {
$message = ["error" => $e->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND);
}
}
Correcto:
/**
* @return DataResponse<Http::STATUS_OK, array{message: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: string}, array{}>
*/
public function someControllerMethod() {
try {
...
if (...) {
throw new PermissionError("some error");
}
...
return new DataResponse(["message" => "some message"]);
} catch (PermissionError $e) {
$message = ["error" => $e->getMessage()];
return new DataResponse($message, Http::STATUS_FORBIDDEN);
}
}
Tipado y documentación¶
TIPAR los métodos de los controladores y los auxiliares de la forma más explícita posible¶
Cuanto más se acote un tipo sin violar ninguna restricción, mejor será la especificación resultante. Psalm detectará estos problemas si se configuraron correctamente los manejadores de incidencias mencionados más arriba.
Ejemplos
Incorrecto:
public function someHelperMethod(): array {
...
return [
"id" => $id,
"name" => $name,
];
}
Correcto:
/**
* @return array{id: int, name: string}
*/
public function someHelperMethod(): array {
...
return [
"id" => $id,
"name" => $name,
];
}
ESTABLECER todas las descripciones de los parámetros y los métodos¶
Mejora la documentación y facilita entender qué hace la API.
También se pueden establecer descripciones para los controladores. Se incluirán en la especificación. Allí se puede explicar qué hacen las API del controlador o dar ejemplos de cómo usar varios endpoints juntos.
Ejemplos
Incorrecto:
class SomeController extends OCSController {
/**
* @return DataResponse<Http::STATUS_OK, array{name: string}, array{}>
*/
public function someControllerMethod(int $id) {
...
return new DataResponse(["name" => name], Http::STATUS_CREATED);
}
}
Correcto:
/**
* Here you can put some explanations about all the endpoints or example code.
*/
class SomeController extends OCSController {
/**
* Here you give a short summary of the method
*
* Here you can give even more details about your method
* and how you can use it.
*
* @param int $id Here you can describe your parameter
*
* @return DataResponse<Http::STATUS_OK, array{name: string}, array{}>
*/
public function someControllerMethod(int $id) {
...
return new DataResponse(["name" => name], Http::STATUS_CREATED);
}
}
Errores y cabeceras¶
NO lanzar excepciones que no sean OCS*Exceptions¶
En los endpoints OCS, lanzar solo OCS*Exceptions. Otros tipos de excepción pueden producir respuestas de error que no son JSON (texto plano/HTML) y no se representarán correctamente en la especificación OpenAPI extraída.
Ejemplos
Incorrecto:
/**
* @throws BadRequestException
*/
public function someControllerMethod() {
...
throw new BadRequestException([]);
}
Correcto:
/**
* @throws OCSBadRequestException
*/
public function someControllerMethod() {
...
throw new OCSBadRequestException("some message");
}
NO usar addHeader (usar setHeaders)¶
Psalm no puede rastrear las cabeceras establecidas con addHeader(), por lo que no pueden validarse ni incluirse correctamente en la especificación extraída.
Usar en su lugar el método setHeaders.
Ejemplos
Incorrecto:
$response = new DataResponse();
$response->addHeader("X-My-Header", "some value");
return $response;
Correcto:
$response = new DataResponse();
$response->setHeaders(["X-My-Header" => "some value"]);
return $response;
Consejos y trucos¶
openapi-extractorespera descripciones en muchos lugares. Para acelerar la adopción inicial, se puede usar--allow-missing-docspara ignorar las descripciones que faltan.De forma predeterminada, la herramienta puede detenerse en el primer error. Para listar varios problemas en una sola ejecución, usar
--continue-on-error.
Advertencia
No usar estas opciones al generar la especificación final.
Pueden ocultar problemas reales del código.
En particular, --continue-on-error es arriesgada, porque el comando puede parecer «correcto» aunque queden problemas.
Usar estas opciones solo para acelerar el proceso de adaptación inicial.
Tutorial: cómo añadir compatibilidad con OpenAPI a una API OCS¶
Supóngase que se construyó una app de lista de tareas (Todo) para APS Conecta Gestión y se tiene el siguiente controlador:
class TodoApiController extends OCSController {
#[NoAdminRequired]
public function create(string $title, ?string $description = null, ?string $image = null): DataResponse {
$todo = $this->service->createTodo($title, $description, $image);
return $this->formatTodo($todo);
}
#[NoAdminRequired]
public function get(int $id): DataResponse {
try {
$todo = $this->service->getTodo($id);
} catch (NotFoundException $e) {
return new DataResponse(["error" => "Todo not found"], Http::STATUS_NOT_FOUND);
}
return $this->formatTodo($todo);
}
#[NoAdminRequired]
public function update(int $id, string $etag, ?string $title = null, ?string $description = null, ?string $image = null): DataResponse {
try {
$todo = $this->service->updateTodo($id, $etag, $title, $description, $image);
} catch (NotFoundException $e) {
return new DataResponse(["error" => "Todo not found"], Http::STATUS_NOT_FOUND);
} catch (ForbiddenException $e) {
return new DataResponse(["error" => "ETag does not match"], Http::STATUS_BAD_REQUEST);
}
return $this->formatTodo($todo);
}
#[NoAdminRequired]
public function delete(int $id): DataResponse {
try {
$todo = $this->service->deleteTodo($id);
} catch (NotFoundException $e) {
return new DataResponse(["error" => "Todo not found"], Http::STATUS_NOT_FOUND);
}
return new DataResponse(null);
}
private function formatTodo(Todo $todo): DataResponse {
return new DataResponse([
"id" => $todo->id,
"title" => $todo->title,
"description" => $todo->description,
"image" => $todo->image,
], Http::STATUS_OK, [
"ETag" => $todo->etag,
]);
}
}
Lo que se quiere hacer ahora es, en primer lugar, crear las anotaciones correctas de los parámetros y añadir descripciones. Podría quedar así:
/**
* Create a new Todo
*
* @param string $title The title of the new Todo item
* @param string|null $description The description of the new Todo item. Can be left empty
* @param string|null $image The base64-encoded image of the new Todo item. Can be left empty
*/
#[NoAdminRequired]
public function create(string $title, ?string $description = null, ?string $image = null): DataResponse {
...
}
/**
* Get a Todo item
*
* @param int $id ID of the Todo item
*/
#[NoAdminRequired]
public function get(int $id): DataResponse {
...
}
/**
* Update a Todo item
*
* @param int $id ID of the Todo item
* @param string $etag ETag of the Todo item. If it does not match the ETag that is stored on the server the request will be rejected
* @param string|null $title The new title of the Todo item. Can be left empty to not update the title
* @param string|null $description The new description of the Todo item. Can be left empty to not update the description
* @param string|null $image The new base64-encoded image of the Todo item. Can be left empty to not update the image
*/
#[NoAdminRequired]
public function update(int $id, string $etag, string $title = null, string $description = null, string $image = null): DataResponse {
...
}
/**
* Delete a Todo item
*
* @param int $id ID of the Todo item
*/
#[NoAdminRequired]
public function delete(int $id): DataResponse {
...
}
El siguiente paso es añadir los tipos de retorno. Este es el paso más importante para que la API quede documentada.
Lo mejor es empezar por los métodos auxiliares que se usan varias veces, como el método formatTodo de este ejemplo:
/**
* @return DataResponse<Http::STATUS_OK, array{id: int, title: string, description: ?string, image: ?string}, array{ETag: string}>
*/
private function formatTodo(Todo $todo): DataResponse() {
...
}
El tipo de retorno de DataResponse y de otras clases que heredan de Response espera argumentos distintos. Ver las anotaciones de sus clases en el código. La siguiente tabla lista algunas habituales:
Clase que hereda de Response |
Argumentos esperados |
|---|---|
DataResponse |
código de estado, datos, cabeceras |
RedirectResponse |
código de estado, cabeceras |
StreamResponse |
código de estado, cabeceras |
TemplateResponse |
código de estado, cabeceras |
Después se pueden añadir los tipos de retorno a todos los demás métodos.
Si dos códigos de estado distintos devuelven la misma estructura de datos y las mismas cabeceras, se puede usar el operador de unión para indicarlo: Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND.
Para dudas sobre la sintaxis de los tipos de retorno, puede ser útil la documentación de Psalm sobre el tipado en Psalm.
Hay que añadir una descripción para cada código de estado que devuelve el método.
/**
* ...
*
* @return DataResponse<Http::STATUS_OK, array{id: int, title: string, description: ?string, image: ?string}, array{ETag: string}>
*
* 200: Todo item created
*/
#[NoAdminRequired]
public function create(string $title, string $description = null, string $image = null): DataResponse {
...
}
/**
* ...
*
* @return DataResponse<Http::STATUS_OK, array{id: int, title: string, description: ?string, image: ?string}, array{ETag: string}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: Todo item returned
* 404: Todo item not found
*/
#[NoAdminRequired]
public function get(int $id): DataResponse {
...
}
/**
* ...
*
* @return DataResponse<Http::STATUS_OK, array{id: int, title: string, description: ?string, image: ?string}, array{ETag: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: Todo item created
* 400: ETag of the Todo item does not match
* 404: Todo item not found
*/
#[NoAdminRequired]
public function update(int $id, string $etag, string $title = null, string $description = null, string $image = null): DataResponse {
...
}
/**
* ...
*
* @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: Todo item deleted
* 404: Todo item not found
*/
#[NoAdminRequired]
public function delete(int $id): DataResponse {
...
}
Ámbitos¶
En algunos casos, quien consume la API puede no querer o no necesitar implementar todas las API que ofrece la app.
Algunos ejemplos son la federación entre apps de distintos servidores, los endpoints relacionados con la administración y otros.
El cliente predeterminado, que debería implementar la funcionalidad principal, se llama OpenAPI::SCOPE_DEFAULT.
Hay constantes disponibles en OCP\AppFramework\Http\Attribute\OpenAPI::SCOPE_* para una mejor experiencia entre apps.
Un controlador y sus métodos pueden tener varios ámbitos; sin embargo, cuando un método tiene el atributo establecido,
se ignoran todos los ámbitos del controlador.
Los métodos que requieren permisos de administración, porque les falta #[NoAdminRequired] o tienen un atributo #[PublicPage] o la
anotación correspondiente, tienen como ámbito predeterminado OpenAPI::SCOPE_ADMINISTRATION.
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)]
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
#[OpenAPI(scope: 'myscope')]
public function show(): TemplateResponse {
...
}
Los distintos ámbitos se guardan como openapi.json para el ámbito predeterminado y como openapi-{scope}.json para los demás.
Etiquetas¶
Para organizar los endpoints de la API dentro de un ámbito, se pueden usar etiquetas que los agrupen. De forma predeterminada se usa el nombre del controlador. Las etiquetas también pueden diferir entre ámbitos distintos.
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['mytag1'])]
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION, tags: ['settings', 'custom2'])]
public function saveSettings(): TemplateResponse {
...
}
Definiciones de tipos de respuesta compartidas¶
En los pasos anteriores se ha reutilizado varias veces la misma estructura de datos, pero se copió cada vez.
Esto es tedioso y propenso a errores, por lo que se quieren crear algunas definiciones de tipos compartidas.
Crear un archivo nuevo llamado ResponseDefinitions.php en la carpeta lib de la app.
Solo funciona con ese nombre de archivo y en esa ubicación.
/**
* @psalm-type TodoItem = array{
* id: int,
* title: string,
* description: ?string,
* image: ?string,
* }
*/
class ResponseDefinitions {}
El nombre de cada definición de tipo debe empezar por el ID legible de la app, tal como lo espera openapi-extractor.
Es una forma en TitleCase / normalizada que se usa para dar a los tipos un espacio de nombres por app (por ejemplo, la app Tables
usa tipos como TablesColumn.
Para importar y usar la definición de tipo, hay que importarla en el controlador:
/**
* @psalm-import-type TodoItem from ResponseDefinitions
*/
class TodoApiController extends OCSController {
...
}
Ahora se puede sustituir cada aparición de array{id: int, title: string, description: ?string, image: ?string} por TodoItem.
Manejar excepciones¶
A veces se quiere terminar con una excepción en lugar de devolver una respuesta.
En este ejemplo, update lanzará una excepción cuando el ETag no coincida:
#[NoAdminRequired]
public function update(int $id, string $etag, string $title = null, string $description = null, string $image = null): DataResponse {
...
} catch (ForbiddenException $e) {
throw new OCSBadRequestException("ETag does not match");
}
...
}
Añadir la anotación correcta se hace así:
/**
* ...
*
* @throws OCSBadRequestException ETag of the Todo item does not match
*/
#[NoAdminRequired]
public function update(int $id, string $etag, string $title = null, string $description = null, string $image = null): DataResponse {
...
}
La descripción que sigue al nombre de la clase de la excepción funciona exactamente igual que la descripción de los códigos de estado añadida antes. Tener en cuenta que solo deben usarse OCS*Exceptions, ya que cualquier otra excepción produce un cuerpo de texto plano en lugar de JSON.
Ignorar ciertos endpoints¶
La herramienta ya ignora todos los endpoints que no son accesibles desde fuera, pero algunas apps tienen endpoints accesibles que no son API (p. ej., los que sirven HTML).
Para ignorarlos se puede añadir el atributo #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] al método del controlador
o a la clase del controlador. También existe un atributo obsoleto, #[IgnoreOpenAPI] (obsoleto desde APS Conecta Gestión 28), por compatibilidad, pero
debe preferirse OpenAPI::SCOPE_IGNORE:
/**
* ...
*
* @IgnoreOpenAPI
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
#[NoAdminRequired]
public function show(): TemplateResponse {
...
}
Exponer capacidades¶
Supóngase que, con la misma app Todo del ejemplo anterior, se quieren exponer algunas capacidades para que los clientes sepan qué pueden esperar. Primero, crear Capabilities en la carpeta lib:
class Capabilities implements ICapability {
public function getCapabilities() {
return [
"todo" => [
"supported-operations" => ["create", "read", "update", "delete"],
"emojis-supported" => true,
],
];
}
}
Ahora hay que añadir la anotación correcta del tipo de retorno:
class Capabilities implements ICapability {
/**
* @return array{todo: array{supported-operations: list<string>, emojis-supported: bool}}
*/
public function getCapabilities() {
return [
"todo" => [
"supported-operations" => ["create", "read", "update", "delete"],
"emojis-supported" => true,
],
];
}
}
Las capacidades aparecerán automáticamente en la especificación generada.
Hay que registrar Capabilities en el archivo lib/AppInfo/Application.php e implementar IBootstrap:
namespace OCA\Todo\AppInfo;
use OCA\Todo\Capabilities;
use OCP\AppFramework\Bootstrap\IBootstrap;
class Application extends App implements IBootstrap {
public const APP_ID = 'todo';
public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
}
public function register(IRegistrationContext $context): void {
// code
$context->registerCapability(Capabilities::class);
}
public function boot(IBootContext $context): void {
}
}
Generar la especificación¶
Si se siguieron las instrucciones de instalación de openapi-extractor, se puede ejecutar composer exec generate-spec en la
carpeta raíz de la app y se obtendrá un archivo nuevo llamado openapi.json (según los ámbitos usados).
Si la herramienta falla en algún punto, indicará qué está mal y, a menudo, también cómo corregir el problema.
Además, conviene ejecutar Psalm para comprobar si hay algún problema.