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 // https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0
const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version 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 = { const APIPathStyle = {
FLAT: 'api', // flat paths e.g. /api/createGroup FLAT: 'api', // flat paths e.g. /api/createGroup
REST: 'rest', // restful paths e.g. /rest/group/create REST: 'rest', // restful paths e.g. /rest/group/create
}; };
// helper to get api root // API resources
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`; const resources = {
// 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 = {
// Group // Group
group: { group: {
create: { create: {
@ -46,7 +54,7 @@ const API = {
listPads: { listPads: {
func: 'listPads', func: 'listPads',
description: 'returns all pads of this group', description: 'returns all pads of this group',
response: { padIDs: { type: 'List', items: { type: 'string' } } }, response: { padIDs: { type: 'array', items: { type: 'string' } } },
}, },
createPad: { createPad: {
func: 'createGroupPad', func: 'createGroupPad',
@ -55,12 +63,12 @@ const API = {
listSessions: { listSessions: {
func: 'listSessionsOfGroup', func: 'listSessionsOfGroup',
description: '', description: '',
response: { sessions: { type: 'List', items: { type: 'SessionInfo' } } }, response: { sessions: { type: 'array', items: { $ref: '#/components/schemas/SessionInfo' } } },
}, },
list: { list: {
func: 'listAllGroups', func: 'listAllGroups',
description: '', description: '',
response: { groupIDs: { type: 'List', items: { type: 'string' } } }, response: { groupIDs: { type: 'array', items: { type: 'string' } } },
}, },
}, },
@ -79,18 +87,18 @@ const API = {
listPads: { listPads: {
func: 'listPadsOfAuthor', func: 'listPadsOfAuthor',
description: 'returns an array of all pads this author contributed to', 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: { listSessions: {
func: 'listSessionsOfAuthor', func: 'listSessionsOfAuthor',
description: 'returns all sessions of an author', 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 :( // We need an operation that return a UserInfo so it can be picked up by the codegen :(
getName: { getName: {
func: 'getAuthorName', func: 'getAuthorName',
description: 'Returns the Author Name of the author', 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: { info: {
func: 'getSessionInfo', func: 'getSessionInfo',
description: 'returns informations about a session', description: 'returns informations about a session',
response: { info: { type: 'SessionInfo' } }, response: { info: { $ref: '#/components/schemas/SessionInfo' } },
}, },
}, },
@ -118,7 +126,7 @@ const API = {
listAll: { listAll: {
func: 'listAllPads', func: 'listAllPads',
description: 'list all the pads', description: 'list all the pads',
response: { padIDs: { type: 'List', items: { type: 'string' } } }, response: { padIDs: { type: 'array', items: { type: 'string' } } },
}, },
createDiffHTML: { createDiffHTML: {
func: 'createDiffHTML', func: 'createDiffHTML',
@ -188,7 +196,7 @@ const API = {
authors: { authors: {
func: 'listAuthorsOfPad', func: 'listAuthorsOfPad',
description: 'returns an array of authors who contributed to this pad', 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: { usersCount: {
func: 'padUsersCount', func: 'padUsersCount',
@ -198,7 +206,7 @@ const API = {
users: { users: {
func: 'padUsers', func: 'padUsers',
description: 'returns the list of users that are currently editing this pad', 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: { sendClientsMessage: {
func: 'sendClientsMessage', func: 'sendClientsMessage',
@ -211,13 +219,13 @@ const API = {
getChatHistory: { getChatHistory: {
func: 'getChatHistory', func: 'getChatHistory',
description: 'returns the chat history', 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 :( // We need an operation that returns a Message so it can be picked up by the codegen :(
getChatHead: { getChatHead: {
func: 'getChatHead', func: 'getChatHead',
description: 'returns the chatHead (chat-message) of the pad', description: 'returns the chatHead (chat-message) of the pad',
response: { chatHead: { type: 'Message' } }, response: { chatHead: { $ref: '#/components/schemas/Message' } },
}, },
appendChatMessage: { appendChatMessage: {
func: 'appendChatMessage', func: 'appendChatMessage',
@ -227,20 +235,181 @@ const API = {
}; };
const defaultResponses = { 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: { 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 // convert to a flat list of OAS Operation objects
const operations = []; const operations = [];
for (const resource in API) { for (const resource in resources) {
for (const action in API[resource]) { for (const action in resources[resource]) {
const { func: operationId, description, response } = API[resource][action]; 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 = { const operation = {
operationId, operationId,
summary: description, summary: description,
responses: defaultResponses, responses,
tags: [resource], tags: [resource],
_restPath: `/${resource}/${action}`, _restPath: `/${resource}/${action}`,
}; };
@ -251,15 +420,9 @@ for (const resource in API) {
const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
const definition = { const definition = {
openapi: OPENAPI_VERSION, openapi: OPENAPI_VERSION,
info: { 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,
},
paths: {}, paths: {},
components: { components: {
responses: {},
parameters: {}, parameters: {},
schemas: { schemas: {
SessionInfo: { SessionInfo: {
@ -314,6 +477,9 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
}, },
}, },
}, },
responses: {
...defaultResponses,
},
securitySchemes: { securitySchemes: {
ApiKey: { ApiKey: {
type: 'apiKey', type: 'apiKey',
@ -334,7 +500,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// console.warn(`No operation found for function: ${funcName}`); // console.warn(`No operation found for function: ${funcName}`);
operation = { operation = {
operationId: funcName, operationId: funcName,
responses: defaultResponses, responses: defaultResponseRefs,
}; };
} }
@ -390,12 +556,14 @@ exports.expressCreateServer = (_, args) => {
// serve openapi definition file // serve openapi definition file
app.get(`${apiRoot}/openapi.json`, (req, res) => { app.get(`${apiRoot}/openapi.json`, (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] });
}); });
// serve latest openapi definition file under /api/openapi.json // serve latest openapi definition file under /api/openapi.json
if (version === apiHandler.latestApiVersion) { if (version === apiHandler.latestApiVersion) {
app.get(`/${style}/openapi.json`, (req, res) => { app.get(`/${style}/openapi.json`, (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] });
}); });
} }
@ -449,7 +617,19 @@ exports.expressCreateServer = (_, args) => {
// start and bind to express // start and bind to express
api.init(); 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}`,
});