openapi: add response objects
This commit is contained in:
parent
03d8964a7a
commit
5792f7224a
1 changed files with 217 additions and 37 deletions
|
@ -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}`,
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue