mirror of https://gitee.com/bigwinds/arangodb
570 lines
16 KiB
JavaScript
570 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / DISCLAIMER
|
|
// /
|
|
// / Copyright 2015-2016 ArangoDB 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 ArangoDB GmbH, Cologne, Germany
|
|
// /
|
|
// / @author Alan Plum
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
const _ = require('lodash');
|
|
const joi = require('joi');
|
|
const statuses = require('statuses');
|
|
const mimeTypes = require('mime-types');
|
|
const mediaTyper = require('media-typer');
|
|
const check = require('@arangodb/foxx/check-args');
|
|
const joiToJsonSchema = require('joi-to-json-schema');
|
|
const tokenize = require('@arangodb/foxx/router/tokenize');
|
|
|
|
const MIME_JSON = 'application/json; charset=utf-8';
|
|
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()
|
|
});
|
|
|
|
const PARSED_JSON_MIME = (function (mime) {
|
|
const contentType = mimeTypes.contentType(mime) || mime;
|
|
const parsed = mediaTyper.parse(contentType);
|
|
return mediaTyper.format(_.pick(parsed, [
|
|
'type',
|
|
'subtype',
|
|
'suffix'
|
|
]));
|
|
}(MIME_JSON));
|
|
|
|
const repeat = (times, value) => {
|
|
const arr = Array(times);
|
|
for (let i = 0; i < times; i++) {
|
|
arr[i] = value;
|
|
}
|
|
return arr;
|
|
};
|
|
|
|
module.exports = exports =
|
|
class SwaggerContext {
|
|
constructor (path) {
|
|
if (!path) {
|
|
path = '';
|
|
}
|
|
{
|
|
const 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);
|
|
this._tags = new Set();
|
|
}
|
|
|
|
header (...args) {
|
|
const argv = check(
|
|
'endpoint.header',
|
|
args,
|
|
[['name', 'string'], ['schema', check.validateSchema], ['description', 'string']],
|
|
[['name', 'string'], ['description', 'string']],
|
|
[['name', 'string'], ['schema', check.validateSchema]],
|
|
[['name', 'string']]
|
|
);
|
|
let schema = argv.schema;
|
|
let description = argv.description;
|
|
this._headers.set(argv.name.toLowerCase(), {schema, description});
|
|
return this;
|
|
}
|
|
|
|
pathParam (...args) {
|
|
const argv = check(
|
|
'endpoint.pathParam',
|
|
args,
|
|
[['name', 'string'], ['schema', check.validateSchema], ['description', 'string']],
|
|
[['name', 'string'], ['description', 'string']],
|
|
[['name', 'string'], ['schema', check.validateSchema]],
|
|
[['name', 'string']]
|
|
);
|
|
let schema = argv.schema;
|
|
let description = argv.description;
|
|
this._pathParams.set(argv.name, {schema, description});
|
|
return this;
|
|
}
|
|
|
|
queryParam (...args) {
|
|
const argv = check(
|
|
'endpoint.queryParam',
|
|
args,
|
|
[['name', 'string'], ['schema', check.validateSchema], ['description', 'string']],
|
|
[['name', 'string'], ['description', 'string']],
|
|
[['name', 'string'], ['schema', check.validateSchema]],
|
|
[['name', 'string']]
|
|
);
|
|
let schema = argv.schema;
|
|
let description = argv.description;
|
|
this._queryParams.set(argv.name, {schema, description});
|
|
return this;
|
|
}
|
|
|
|
body (...args) {
|
|
const argv = check(
|
|
'endpoint.body',
|
|
args,
|
|
[['model', check.validateModel], ['mimes', check.validateMimes], ['description', 'string']],
|
|
[['model', check.validateModel], ['description', 'string']],
|
|
[['mimes', check.validateMimes], ['description', 'string']],
|
|
[['model', check.validateModel], ['mimes', check.validateMimes]],
|
|
[['model', check.validateModel]],
|
|
[['mimes', check.validateMimes]],
|
|
[['description', 'string']]
|
|
);
|
|
let model = argv.model;
|
|
let mimes = argv.mimes;
|
|
let description = argv.description;
|
|
|
|
if (!model) {
|
|
model = {multiple: false};
|
|
}
|
|
|
|
if (model.model === null) {
|
|
this._bodyParam = {
|
|
model: null,
|
|
multiple: model.multiple,
|
|
contentTypes: null,
|
|
description};
|
|
return this;
|
|
}
|
|
|
|
if (!mimes) {
|
|
mimes = [];
|
|
}
|
|
|
|
if (!mimes.length && model.model) {
|
|
mimes.push(PARSED_JSON_MIME);
|
|
}
|
|
|
|
this._bodyParam = {
|
|
model: model.model,
|
|
multiple: model.multiple,
|
|
contentTypes: mimes,
|
|
description};
|
|
return this;
|
|
}
|
|
|
|
response (...args) {
|
|
const argv = check(
|
|
'endpoint.response',
|
|
args,
|
|
[['status', check.validateStatus], ['model', check.validateModel], ['mimes', check.validateMimes], ['description', 'string']],
|
|
[['status', check.validateStatus], ['model', check.validateModel], ['description', 'string']],
|
|
[['status', check.validateStatus], ['mimes', check.validateMimes], ['description', 'string']],
|
|
[['status', check.validateStatus], ['model', check.validateModel], ['mimes', check.validateMimes]],
|
|
[['model', check.validateModel], ['mimes', check.validateMimes], ['description', 'string']],
|
|
[['status', check.validateStatus], ['model', check.validateModel]],
|
|
[['status', check.validateStatus], ['mimes', check.validateMimes]],
|
|
[['status', check.validateStatus], ['description', 'string']],
|
|
[['model', check.validateModel], ['mimes', check.validateMimes]],
|
|
[['model', check.validateModel], ['description', 'string']],
|
|
[['mimes', check.validateMimes], ['description', 'string']],
|
|
[['model', check.validateModel]],
|
|
[['mimes', check.validateMimes]],
|
|
[['description', 'string']]
|
|
);
|
|
let status = argv.status;
|
|
let model = argv.model;
|
|
let mimes = argv.mimes;
|
|
let description = argv.description;
|
|
|
|
if (!model) {
|
|
model = {multiple: false};
|
|
}
|
|
|
|
if (!status) {
|
|
status = model.model === null ? 204 : 200;
|
|
}
|
|
|
|
if (model.model === null) {
|
|
this._responses.set(status, {
|
|
model: null,
|
|
multiple: model.multiple,
|
|
contentTypes: null,
|
|
description});
|
|
return this;
|
|
}
|
|
|
|
if (!mimes) {
|
|
mimes = [];
|
|
}
|
|
|
|
if (!mimes.length && model.model) {
|
|
mimes.push(PARSED_JSON_MIME);
|
|
}
|
|
|
|
this._responses.set(status, {
|
|
model: model.model,
|
|
multiple: model.multiple,
|
|
contentTypes: mimes,
|
|
description});
|
|
return this;
|
|
}
|
|
|
|
error (...args) {
|
|
const argv = check(
|
|
'endpoint.error',
|
|
args,
|
|
[['status', check.validateStatus], ['description', 'string']],
|
|
[['status', check.validateStatus]]
|
|
);
|
|
let status = argv.status;
|
|
let description = argv.description;
|
|
this._responses.set(status, {
|
|
model: DEFAULT_ERROR_SCHEMA,
|
|
multiple: false,
|
|
contentTypes: [PARSED_JSON_MIME],
|
|
description
|
|
});
|
|
return this;
|
|
}
|
|
|
|
summary (text) {
|
|
[text] = check(
|
|
'endpoint.summary',
|
|
[text],
|
|
[['text', 'string']]
|
|
);
|
|
this._summary = text;
|
|
return this;
|
|
}
|
|
|
|
description (text) {
|
|
[text] = check(
|
|
'endpoint.description',
|
|
[text],
|
|
[['text', 'string']]
|
|
);
|
|
this._description = text;
|
|
return this;
|
|
}
|
|
|
|
tag (...tags) {
|
|
tags = check(
|
|
'endpoint.tag',
|
|
tags,
|
|
[...repeat(Math.max(1, tags.length), ['tag', 'string'])]
|
|
);
|
|
for (const tag of tags) {
|
|
this._tags.add(tag);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
deprecated (...args) {
|
|
const [flag] = check(
|
|
'endpoint.summary',
|
|
args,
|
|
[['flag', 'boolean']],
|
|
[]
|
|
);
|
|
this._deprecated = typeof flag === 'boolean' ? flag : true;
|
|
return this;
|
|
}
|
|
|
|
_merge (swaggerObj, pathOnly) {
|
|
if (!pathOnly) {
|
|
for (const header of swaggerObj._headers.entries()) {
|
|
this._headers.set(header[0], header[1]);
|
|
}
|
|
for (const queryParam of swaggerObj._queryParams.entries()) {
|
|
this._queryParams.set(queryParam[0], queryParam[1]);
|
|
}
|
|
for (const response of swaggerObj._responses.entries()) {
|
|
this._responses.set(response[0], response[1]);
|
|
}
|
|
for (const tag of swaggerObj._tags) {
|
|
this._tags.add(tag);
|
|
}
|
|
if (!this._bodyParam && swaggerObj._bodyParam) {
|
|
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 (const pathParam of swaggerObj._pathParams.entries()) {
|
|
let name = pathParam[0];
|
|
const def = pathParam[1];
|
|
if (this._pathParams.has(name)) {
|
|
let baseName = name;
|
|
let i = 2;
|
|
const 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._tags) {
|
|
operation.tags = Array.from(this._tags);
|
|
}
|
|
if (this._bodyParam) {
|
|
operation.consumes = (
|
|
this._bodyParam.contentTypes
|
|
? this._bodyParam.contentTypes.slice()
|
|
: []
|
|
);
|
|
}
|
|
for (const response of this._responses.values()) {
|
|
if (!response.contentTypes) {
|
|
continue;
|
|
}
|
|
for (const contentType of response.contentTypes) {
|
|
if (operation.produces.indexOf(contentType) === -1) {
|
|
operation.produces.push(contentType);
|
|
}
|
|
}
|
|
}
|
|
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.schema
|
|
? swaggerifyParam(def.schema.isJoi ? def.schema : joi.object(def.schema))
|
|
: {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.schema
|
|
? swaggerifyParam(def.schema.isJoi ? def.schema : joi.object(def.schema))
|
|
: {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.schema
|
|
? swaggerifyParam(def.schema.isJoi ? def.schema : joi.object(def.schema))
|
|
: {type: 'string'}
|
|
);
|
|
parameter.name = name;
|
|
parameter.in = 'header';
|
|
if (def.description) {
|
|
parameter.description = def.description;
|
|
}
|
|
operation.parameters.push(parameter);
|
|
}
|
|
|
|
if (this._bodyParam && this._bodyParam.contentTypes) {
|
|
const def = this._bodyParam;
|
|
const schema = (
|
|
def.model
|
|
? (def.model.isJoi ? def.model : def.model.schema)
|
|
: null
|
|
);
|
|
const parameter = (
|
|
schema
|
|
? swaggerifyBody(schema, def.multiple)
|
|
: {schema: {type: 'string'}}
|
|
);
|
|
parameter.name = 'body';
|
|
parameter.in = 'body';
|
|
if (def.description) {
|
|
parameter.description = def.description;
|
|
}
|
|
operation.parameters.push(parameter);
|
|
}
|
|
|
|
operation.responses = {
|
|
500: {
|
|
description: 'Default error response.',
|
|
schema: joi2schema(DEFAULT_ERROR_SCHEMA)
|
|
}
|
|
};
|
|
|
|
for (const entry of this._responses.entries()) {
|
|
const code = entry[0];
|
|
const def = entry[1];
|
|
const schema = (
|
|
def.model
|
|
? (def.model.isJoi ? def.model : def.model.schema)
|
|
: null
|
|
);
|
|
const response = {};
|
|
if (def.contentTypes && def.contentTypes.length) {
|
|
response.schema = (
|
|
schema
|
|
? joi2schema(schema, def.multiple)
|
|
: {type: 'string'}
|
|
);
|
|
}
|
|
if (schema && schema._description) {
|
|
response.description = schema._description;
|
|
delete response.schema.description;
|
|
}
|
|
if (def.description) {
|
|
response.description = def.description;
|
|
}
|
|
if (!response.description) {
|
|
const message = statuses[code];
|
|
response.description = message ? `HTTP ${code} ${message}.` : (
|
|
response.schema
|
|
? `Nondescript ${code} response.`
|
|
: `Nondescript ${code} response without body.`
|
|
);
|
|
}
|
|
operation.responses[code] = response;
|
|
}
|
|
|
|
return operation;
|
|
}
|
|
};
|
|
|
|
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((test) => test.name === 'integer')) {
|
|
return ['integer'];
|
|
}
|
|
return ['number'];
|
|
case 'array':
|
|
return ['array'];
|
|
case 'object':
|
|
return ['object'];
|
|
case 'string':
|
|
if (joi._meta.some((meta) => meta.secret)) {
|
|
return ['string', 'password'];
|
|
}
|
|
return ['string'];
|
|
}
|
|
}
|
|
|
|
function swaggerifyParam (joi) {
|
|
const param = {
|
|
required: joi._presence === 'required',
|
|
description: joi._description || undefined
|
|
};
|
|
let item = param;
|
|
if (joi._meta.some((meta) => 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 && joi._valids._set.length) {
|
|
item.enum = joi._valids._set;
|
|
}
|
|
if (joi._flags.hasOwnProperty('default')) {
|
|
item.default = joi._flags.default;
|
|
}
|
|
return param;
|
|
}
|
|
|
|
function swaggerifyBody (joi, multiple) {
|
|
return {
|
|
required: joi._presence === 'required',
|
|
description: joi._description || undefined,
|
|
schema: joi2schema(joi, multiple)
|
|
};
|
|
}
|
|
|
|
function joi2schema (schema, multiple) {
|
|
if (multiple) {
|
|
schema = joi.array().items(schema);
|
|
}
|
|
return joiToJsonSchema(schema);
|
|
}
|