mirror of https://gitee.com/bigwinds/arangodb
429 lines
12 KiB
JavaScript
429 lines
12 KiB
JavaScript
'use strict';
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief Foxx Router Swagger Context
|
|
///
|
|
/// @file
|
|
///
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 2015 triagens GmbH, Cologne, Germany
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
///
|
|
/// Copyright holder is triAGENS GmbH, Cologne, Germany
|
|
///
|
|
/// @author Alan Plum
|
|
/// @author Copyright 2015-2016, triAGENS GmbH, Cologne, Germany
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
const joi = require('joi');
|
|
const mimeTypes = require('mime-types');
|
|
const mediaTyper = require('media-typer');
|
|
const joi2schema = require('joi-to-json-schema');
|
|
const tokenize = require('@arangodb/foxx/router/tokenize');
|
|
|
|
const DEFAULT_BODY_SCHEMA = joi.object().optional().meta({allowInvalid: true});
|
|
const DEFAULT_ERROR_SCHEMA = joi.object().keys({
|
|
error: joi.allow(true).required(),
|
|
errorNum: joi.number().integer().optional(),
|
|
errorMessage: joi.string().optional(),
|
|
code: joi.number().integer().optional()
|
|
});
|
|
|
|
|
|
module.exports = exports = class SwaggerContext {
|
|
constructor(path) {
|
|
if (!path) {
|
|
path = '';
|
|
}
|
|
{
|
|
let n = path.length - 1;
|
|
if (path.charAt(n) === '/') {
|
|
path = path.slice(0, n);
|
|
}
|
|
if (path.charAt(0) !== '/') {
|
|
path = `/${path}`;
|
|
}
|
|
}
|
|
this._headers = new Map();
|
|
this._queryParams = new Map();
|
|
this._bodyParam = null;
|
|
this._responses = new Map();
|
|
this._summary = null;
|
|
this._description = null;
|
|
this._deprecated = false;
|
|
this.path = path;
|
|
this._pathParams = new Map();
|
|
this._pathParamNames = [];
|
|
this._pathTokens = tokenize(path, this);
|
|
}
|
|
|
|
header(name, type, description) {
|
|
this._headers.set(name, {type: type, description: description});
|
|
return this;
|
|
}
|
|
|
|
pathParam(name, type, description) {
|
|
this._pathParams.set(name, {type: type, description: description});
|
|
return this;
|
|
}
|
|
|
|
queryParam(name, type, description) {
|
|
this._queryParams.set(name, {type: type, description: description});
|
|
return this;
|
|
}
|
|
|
|
body(type, description) {
|
|
if (type === null) {
|
|
this._bodyParam = {};
|
|
} else {
|
|
let contentType = 'application/json; charset=utf-8';
|
|
if (type === 'binary') {
|
|
contentType = 'application/octet-stream';
|
|
} else if (typeof type !== 'object') {
|
|
contentType = mimeTypes.contentType(type) || type;
|
|
}
|
|
const parsed = mediaTyper.parse(contentType);
|
|
this._bodyParam = {
|
|
type: type,
|
|
description: description,
|
|
contentType: {
|
|
header: contentType,
|
|
parsed: parsed,
|
|
mime: mediaTyper.format({
|
|
type: parsed.type,
|
|
subtype: parsed.subtype,
|
|
suffix: parsed.suffix
|
|
})
|
|
}
|
|
};
|
|
}
|
|
return this;
|
|
}
|
|
|
|
response(status, type, description) {
|
|
let statusCode = Number(status);
|
|
if (!statusCode || Number.isNaN(statusCode)) {
|
|
description = type;
|
|
type = status;
|
|
statusCode = 200;
|
|
}
|
|
if (type === null) {
|
|
if (statusCode === 200) {
|
|
this._responses.remove(200);
|
|
statusCode = 204;
|
|
}
|
|
this._responses.set(statusCode, {});
|
|
} else {
|
|
let contentType = 'application/json; charset=utf-8';
|
|
if (type === 'binary') {
|
|
contentType = 'application/octet-stream';
|
|
} else if (typeof type !== 'object') {
|
|
contentType = mimeTypes.contentType(type) || type;
|
|
}
|
|
const parsed = mediaTyper.parse(contentType);
|
|
this._responses.set(statusCode, {
|
|
type: type,
|
|
description: description,
|
|
contentType: {
|
|
header: contentType,
|
|
parsed: parsed,
|
|
mime: mediaTyper.format({
|
|
type: parsed.type,
|
|
subtype: parsed.subtype,
|
|
suffix: parsed.suffix
|
|
})
|
|
}
|
|
});
|
|
}
|
|
return this;
|
|
}
|
|
|
|
error(status, description) {
|
|
return this.response(status, undefined, description);
|
|
}
|
|
|
|
summary(text) {
|
|
this._summary = text;
|
|
return this;
|
|
}
|
|
|
|
description(text) {
|
|
this._description = text;
|
|
return this;
|
|
}
|
|
|
|
deprecated(flag) {
|
|
this._deprecated = typeof flag === 'boolean' ? flag : true;
|
|
return this;
|
|
}
|
|
|
|
_merge(swaggerObj, pathOnly) {
|
|
if (!pathOnly) {
|
|
for (let header of swaggerObj._headers.entries()) {
|
|
this._headers.set(header[0], header[1]);
|
|
}
|
|
for (let queryParam of swaggerObj._queryParams.entries()) {
|
|
this._queryParams.set(queryParam[0], queryParam[1]);
|
|
}
|
|
for (let response of swaggerObj._responses.entries()) {
|
|
this._responses.set(response[0], response[1]);
|
|
}
|
|
if (swaggerObj._bodyParam) {
|
|
if (!this._bodyParam || swaggerObj._bodyParam.type !== DEFAULT_BODY_SCHEMA) {
|
|
this._bodyParam = swaggerObj._bodyParam;
|
|
}
|
|
}
|
|
this._deprecated = swaggerObj._deprecated || this._deprecated;
|
|
this._description = swaggerObj._description || this._description;
|
|
this._summary = swaggerObj._summary || this._summary;
|
|
}
|
|
if (this.path.charAt(this.path.length - 1) === '*') {
|
|
this.path = this.path.slice(0, this.path.length - 1);
|
|
}
|
|
if (this.path.charAt(this.path.length - 1) === '/') {
|
|
this.path = this.path.slice(0, this.path.length - 1);
|
|
}
|
|
this._pathTokens.pop();
|
|
this._pathTokens = this._pathTokens.concat(swaggerObj._pathTokens);
|
|
for (let pathParam of swaggerObj._pathParams.entries()) {
|
|
let name = pathParam[0];
|
|
let def = pathParam[1];
|
|
if (this._pathParams.has(name)) {
|
|
let baseName = name;
|
|
let i = 2;
|
|
let match = name.match(/(^.+)([0-9]+)$/i);
|
|
if (match) {
|
|
baseName = match[1];
|
|
i = Number(match[2]);
|
|
}
|
|
while (this._pathParams.has(baseName + i)) {
|
|
i++;
|
|
}
|
|
name = baseName + i;
|
|
}
|
|
this._pathParams.set(name, def);
|
|
this._pathParamNames.push(name);
|
|
}
|
|
this.path = tokenize.reverse(this._pathTokens, this._pathParamNames);
|
|
}
|
|
|
|
_buildOperation() {
|
|
const operation = {
|
|
produces: [],
|
|
parameters: []
|
|
};
|
|
if (this._deprecated) {
|
|
operation.deprecated = this._deprecated;
|
|
}
|
|
if (this._description) {
|
|
operation.description = this._description;
|
|
}
|
|
if (this._summary) {
|
|
operation.summary = this._summary;
|
|
}
|
|
if (this._bodyParam) {
|
|
operation.consumes = (
|
|
this._bodyParam.contentType
|
|
? [this._bodyParam.contentType.mime]
|
|
: []
|
|
);
|
|
}
|
|
for (const response of this._responses.values()) {
|
|
if (operation.produces.indexOf(response.contentType.mime) === -1) {
|
|
operation.produces.push(response.contentType.mime);
|
|
}
|
|
}
|
|
if (operation.produces.indexOf('application/json') === -1) {
|
|
// ArangoDB errors use 'application/json'
|
|
operation.produces.push('application/json');
|
|
}
|
|
|
|
for (const param of this._pathParams.entries()) {
|
|
const name = param[0];
|
|
const def = param[1];
|
|
const parameter = (
|
|
def.type && def.type.isJoi
|
|
? swaggerifyParam(def.type)
|
|
: {type: 'string'}
|
|
);
|
|
parameter.name = name;
|
|
parameter.in = 'path';
|
|
parameter.required = true;
|
|
if (def.description) {
|
|
parameter.description = def.description;
|
|
}
|
|
operation.parameters.push(parameter);
|
|
}
|
|
|
|
for (const param of this._queryParams.entries()) {
|
|
const name = param[0];
|
|
const def = param[1];
|
|
const parameter = (
|
|
def.type && def.type.isJoi
|
|
? swaggerifyParam(def.type)
|
|
: {type: 'string'}
|
|
);
|
|
parameter.name = name;
|
|
parameter.in = 'query';
|
|
if (def.description) {
|
|
parameter.description = def.description;
|
|
}
|
|
operation.parameters.push(parameter);
|
|
}
|
|
|
|
for (const param of this._headers.entries()) {
|
|
const name = param[0];
|
|
const def = param[1];
|
|
const parameter = (
|
|
def.type && def.type.isJoi
|
|
? swaggerifyParam(def.type)
|
|
: {type: 'string'}
|
|
);
|
|
parameter.name = name;
|
|
parameter.in = 'header';
|
|
if (def.description) {
|
|
parameter.description = def.description;
|
|
}
|
|
operation.parameters.push(parameter);
|
|
}
|
|
|
|
if (this._bodyParam && this._bodyParam.contentType) {
|
|
// TODO handle multipart and form-urlencoded
|
|
const def = this._bodyParam;
|
|
const schema = (
|
|
def.type
|
|
? (def.type.schema || def.type)
|
|
: null
|
|
);
|
|
const parameter = (
|
|
schema && schema.isJoi
|
|
? swaggerifyBody(schema)
|
|
: {schema: {type: 'string'}}
|
|
);
|
|
parameter.name = 'body';
|
|
parameter.in = 'body';
|
|
if (def.description) {
|
|
parameter.description = def.description;
|
|
}
|
|
operation.parameters.push(parameter);
|
|
}
|
|
|
|
operation.responses = {
|
|
default: {
|
|
description: 'Unexpected error',
|
|
schema: joi2schema(DEFAULT_ERROR_SCHEMA)
|
|
}
|
|
};
|
|
|
|
for (const entry of this._responses.entries()) {
|
|
const code = entry[0];
|
|
const def = entry[1];
|
|
const schema = (
|
|
def.type
|
|
? (def.type.schema || def.type)
|
|
: null
|
|
);
|
|
const response = {};
|
|
if (def.contentType) {
|
|
response.schema = (
|
|
schema && schema.isJoi
|
|
? joi2schema(schema)
|
|
: {type: 'string'}
|
|
);
|
|
}
|
|
if (schema && schema.isJoi && schema._description) {
|
|
response.description = schema._description;
|
|
}
|
|
if (def.description) {
|
|
response.description = def.description;
|
|
}
|
|
operation.responses[code] = response;
|
|
}
|
|
|
|
return operation;
|
|
}
|
|
};
|
|
|
|
exports.DEFAULT_BODY_SCHEMA = DEFAULT_BODY_SCHEMA;
|
|
|
|
|
|
function swaggerifyType(joi) {
|
|
switch (joi._type) {
|
|
default:
|
|
return ['string'];
|
|
case 'binary':
|
|
return ['string', 'binary'];
|
|
case 'boolean':
|
|
return ['boolean'];
|
|
case 'date':
|
|
return ['string', 'date-time'];
|
|
case 'func':
|
|
return ['string'];
|
|
case 'number':
|
|
if (joi._tests.some(function (test) {
|
|
return test.name === 'integer';
|
|
})) {
|
|
return ['integer'];
|
|
}
|
|
return ['number'];
|
|
case 'array':
|
|
return ['array'];
|
|
case 'object':
|
|
return ['object'];
|
|
case 'string':
|
|
if (joi._meta.some(function (meta) {
|
|
return meta.secret;
|
|
})) {
|
|
return ['string', 'password'];
|
|
}
|
|
return ['string'];
|
|
}
|
|
}
|
|
|
|
function swaggerifyParam(joi) {
|
|
const param = {
|
|
required: joi._presence === 'required',
|
|
description: joi._description
|
|
};
|
|
let item = param;
|
|
if (joi._meta.some(function (meta) {
|
|
return meta.allowMultiple;
|
|
})) {
|
|
param.type = 'array';
|
|
param.collectionFormat = 'multi';
|
|
param.items = {};
|
|
item = param.items;
|
|
}
|
|
const type = swaggerifyType(joi);
|
|
item.type = type[0];
|
|
if (type.length > 1) {
|
|
item.format = type[1];
|
|
}
|
|
if (joi._valids._set) {
|
|
item.enum = joi._valids._set;
|
|
}
|
|
if (joi._flags.hasOwnProperty('default')) {
|
|
item.default = joi._flags.default;
|
|
}
|
|
return param;
|
|
}
|
|
|
|
function swaggerifyBody(joi) {
|
|
return {
|
|
required: joi._presence === 'required',
|
|
description: joi._description,
|
|
schema: joi2schema(joi)
|
|
};
|
|
}
|