openapi: add response objects

This commit is contained in:
Viljami Kuosmanen 2020-03-29 18:47:42 +02:00 committed by muxator
parent 03d8964a7a
commit 5792f7224a

View file

@ -11,22 +11,30 @@ const apiLogger = log4js.getLogger('API');
// https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0
const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version
// enum for two different styles of API paths used for etherpad
const info = {
title: 'Etherpad API',
description:
'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on your server, under your control.',
termsOfService: 'https://etherpad.org/',
contact: {
name: 'The Etherpad Foundation',
url: 'https://etherpad.org/',
email: 'support@example.com',
},
license: {
name: 'Apache 2.0',
url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
},
version: apiHandler.latestApiVersion,
};
const APIPathStyle = {
FLAT: 'api', // flat paths e.g. /api/createGroup
REST: 'rest', // restful paths e.g. /rest/group/create
};
// helper to get api root
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`;
// helper to generate an OpenAPI server object when serving definitions
const generateServerForApiVersion = (apiRoot, req) => ({
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
});
// operations
const API = {
// API resources
const resources = {
// Group
group: {
create: {
@ -46,7 +54,7 @@ const API = {
listPads: {
func: 'listPads',
description: 'returns all pads of this group',
response: { padIDs: { type: 'List', items: { type: 'string' } } },
response: { padIDs: { type: 'array', items: { type: 'string' } } },
},
createPad: {
func: 'createGroupPad',
@ -55,12 +63,12 @@ const API = {
listSessions: {
func: 'listSessionsOfGroup',
description: '',
response: { sessions: { type: 'List', items: { type: 'SessionInfo' } } },
response: { sessions: { type: 'array', items: { $ref: '#/components/schemas/SessionInfo' } } },
},
list: {
func: 'listAllGroups',
description: '',
response: { groupIDs: { type: 'List', items: { type: 'string' } } },
response: { groupIDs: { type: 'array', items: { type: 'string' } } },
},
},
@ -79,18 +87,18 @@ const API = {
listPads: {
func: 'listPadsOfAuthor',
description: 'returns an array of all pads this author contributed to',
response: { padIDs: { type: 'List', items: { type: 'string' } } },
response: { padIDs: { type: 'array', items: { type: 'string' } } },
},
listSessions: {
func: 'listSessionsOfAuthor',
description: 'returns all sessions of an author',
response: { sessions: { type: 'List', items: { type: 'SessionInfo' } } },
response: { sessions: { type: 'array', items: { $ref: '#/components/schemas/SessionInfo' } } },
},
// We need an operation that return a UserInfo so it can be picked up by the codegen :(
getName: {
func: 'getAuthorName',
description: 'Returns the Author Name of the author',
response: { info: { type: 'UserInfo' } },
response: { info: { $ref: '#/components/schemas/UserInfo' } },
},
},
@ -109,7 +117,7 @@ const API = {
info: {
func: 'getSessionInfo',
description: 'returns informations about a session',
response: { info: { type: 'SessionInfo' } },
response: { info: { $ref: '#/components/schemas/SessionInfo' } },
},
},
@ -118,7 +126,7 @@ const API = {
listAll: {
func: 'listAllPads',
description: 'list all the pads',
response: { padIDs: { type: 'List', items: { type: 'string' } } },
response: { padIDs: { type: 'array', items: { type: 'string' } } },
},
createDiffHTML: {
func: 'createDiffHTML',
@ -188,7 +196,7 @@ const API = {
authors: {
func: 'listAuthorsOfPad',
description: 'returns an array of authors who contributed to this pad',
response: { authorIDs: { type: 'List', items: { type: 'string' } } },
response: { authorIDs: { type: 'array', items: { type: 'string' } } },
},
usersCount: {
func: 'padUsersCount',
@ -198,7 +206,7 @@ const API = {
users: {
func: 'padUsers',
description: 'returns the list of users that are currently editing this pad',
response: { padUsers: { type: 'List', items: { type: 'UserInfo' } } },
response: { padUsers: { type: 'array', items: { $ref: '#/components/schemas/UserInfo' } } },
},
sendClientsMessage: {
func: 'sendClientsMessage',
@ -211,13 +219,13 @@ const API = {
getChatHistory: {
func: 'getChatHistory',
description: 'returns the chat history',
response: { messages: { type: 'List', items: { type: 'Message' } } },
response: { messages: { type: 'array', items: { $ref: '#/components/schemas/Message' } } },
},
// We need an operation that returns a Message so it can be picked up by the codegen :(
getChatHead: {
func: 'getChatHead',
description: 'returns the chatHead (chat-message) of the pad',
response: { chatHead: { type: 'Message' } },
response: { chatHead: { $ref: '#/components/schemas/Message' } },
},
appendChatMessage: {
func: 'appendChatMessage',
@ -227,20 +235,181 @@ const API = {
};
const defaultResponses = {
Success: {
description: 'ok (code 0)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {
type: 'integer',
example: 0,
},
message: {
type: 'string',
example: 'ok',
},
data: {
type: 'object',
example: null,
},
},
},
},
},
},
ApiError: {
description: 'generic api error (code 1)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {
type: 'integer',
example: 1,
},
message: {
type: 'string',
example: 'error message',
},
data: {
type: 'object',
example: null,
},
},
},
},
},
},
InternalError: {
description: 'internal api error (code 2)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {
type: 'integer',
example: 2,
},
message: {
type: 'string',
example: 'internal error',
},
data: {
type: 'object',
example: null,
},
},
},
},
},
},
NotFound: {
description: 'no such function (code 4)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {
type: 'integer',
example: 3,
},
message: {
type: 'string',
example: 'no such function',
},
data: {
type: 'object',
example: null,
},
},
},
},
},
},
Unauthorized: {
description: 'no or wrong API key (code 4)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {
type: 'integer',
example: 4,
},
message: {
type: 'string',
example: 'no or wrong API key',
},
data: {
type: 'object',
example: null,
},
},
},
},
},
},
};
const defaultResponseRefs = {
200: {
description: 'ok',
$ref: '#/components/responses/Success',
},
400: {
$ref: '#/components/responses/ApiError',
},
401: {
$ref: '#/components/responses/Unauthorized',
},
500: {
$ref: '#/components/responses/InternalError',
},
};
// convert to a flat list of OAS Operation objects
const operations = [];
for (const resource in API) {
for (const action in API[resource]) {
const { func: operationId, description, response } = API[resource][action];
for (const resource in resources) {
for (const action in resources[resource]) {
const { func: operationId, description, response } = resources[resource][action];
const responses = { ...defaultResponseRefs };
if (response) {
responses[200] = {
description: 'ok (code 0)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
code: {
type: 'integer',
example: 0,
},
message: {
type: 'string',
example: 'ok',
},
data: {
type: 'object',
properties: response,
},
},
},
},
},
};
}
const operation = {
operationId,
summary: description,
responses: defaultResponses,
responses,
tags: [resource],
_restPath: `/${resource}/${action}`,
};
@ -251,15 +420,9 @@ for (const resource in API) {
const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
const definition = {
openapi: OPENAPI_VERSION,
info: {
title: 'Etherpad API',
description:
'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on your server, under your control.',
version,
},
info,
paths: {},
components: {
responses: {},
parameters: {},
schemas: {
SessionInfo: {
@ -314,6 +477,9 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
},
},
},
responses: {
...defaultResponses,
},
securitySchemes: {
ApiKey: {
type: 'apiKey',
@ -334,7 +500,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// console.warn(`No operation found for function: ${funcName}`);
operation = {
operationId: funcName,
responses: defaultResponses,
responses: defaultResponseRefs,
};
}
@ -390,12 +556,14 @@ exports.expressCreateServer = (_, args) => {
// serve openapi definition file
app.get(`${apiRoot}/openapi.json`, (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] });
});
// serve latest openapi definition file under /api/openapi.json
if (version === apiHandler.latestApiVersion) {
app.get(`/${style}/openapi.json`, (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] });
});
}
@ -449,7 +617,19 @@ exports.expressCreateServer = (_, args) => {
// start and bind to express
api.init();
app.use(apiRoot, async (req, res) => api.handleRequest(req, req, res));
app.use(apiRoot, async (req, res) => {
// allow cors
res.header('Access-Control-Allow-Origin', '*');
return api.handleRequest(req, req, res);
});
}
}
};
// helper to get api root
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`;
// helper to generate an OpenAPI server object when serving definitions
const generateServerForApiVersion = (apiRoot, req) => ({
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
});