mirror of https://gitee.com/bigwinds/arangodb
584 lines
15 KiB
JavaScript
584 lines
15 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 assert = require('assert');
|
|
const statuses = require('statuses');
|
|
const mimeTypes = require('mime-types');
|
|
const mediaTyper = require('media-typer');
|
|
const joiToJsonSchema = require('joi-to-json-schema');
|
|
const il = require('@arangodb/util').inline;
|
|
const tokenize = require('@arangodb/foxx/router/tokenize');
|
|
|
|
const MIME_JSON = 'application/json; charset=utf-8';
|
|
const MIME_BINARY = 'application/octet-stream';
|
|
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 = '';
|
|
}
|
|
{
|
|
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);
|
|
}
|
|
|
|
header(name, schema, description) {
|
|
if (typeof schema === 'string') {
|
|
description = schema;
|
|
schema = undefined;
|
|
}
|
|
this._headers.set(name, {schema, description});
|
|
return this;
|
|
}
|
|
|
|
pathParam(name, schema, description) {
|
|
if (typeof schema === 'string') {
|
|
description = schema;
|
|
schema = undefined;
|
|
}
|
|
this._pathParams.set(name, {schema, description});
|
|
return this;
|
|
}
|
|
|
|
queryParam(name, schema, description) {
|
|
if (typeof schema === 'string') {
|
|
description = schema;
|
|
schema = undefined;
|
|
}
|
|
this._queryParams.set(name, {schema, description});
|
|
return this;
|
|
}
|
|
|
|
body(model, mimes, description) {
|
|
if (
|
|
typeof model === 'string'
|
|
|| (Array.isArray(model) && typeof model[0] === 'string')
|
|
) {
|
|
description = mimes;
|
|
mimes = model;
|
|
model = undefined;
|
|
}
|
|
|
|
if (typeof mimes === 'string') {
|
|
description = mimes;
|
|
mimes = undefined;
|
|
}
|
|
|
|
let multiple = false;
|
|
if (Array.isArray(model)) {
|
|
if (model.length !== 1) {
|
|
throw new Error(il`
|
|
Model must be a model or schema
|
|
or an array containing exactly one model or schema.
|
|
If you are trying to use multiple schemas
|
|
try using joi.alternatives instead.
|
|
`);
|
|
}
|
|
model = model[0];
|
|
multiple = true;
|
|
}
|
|
|
|
if (model === null && !mimes) {
|
|
this._bodyParam = {
|
|
model: null,
|
|
multiple,
|
|
contentTypes: null,
|
|
description
|
|
};
|
|
return;
|
|
}
|
|
|
|
if (!mimes) {
|
|
mimes = [];
|
|
}
|
|
|
|
if (!mimes.length && model) {
|
|
mimes.push(MIME_JSON);
|
|
}
|
|
|
|
const contentTypes = mimes.map((mime) => {
|
|
if (mime === 'binary') {
|
|
mime = MIME_BINARY;
|
|
}
|
|
const contentType = mimeTypes.contentType(mime) || mime;
|
|
const parsed = mediaTyper.parse(contentType);
|
|
return mediaTyper.format(_.pick(parsed, [
|
|
'type',
|
|
'subtype',
|
|
'suffix'
|
|
]));
|
|
});
|
|
|
|
if (model) {
|
|
if (model.isJoi) {
|
|
model = {schema: model};
|
|
} else {
|
|
model = _.clone(model);
|
|
}
|
|
if (model.schema && !model.schema.isJoi) {
|
|
model.schema = joi.object(model.schema).required();
|
|
}
|
|
if (!model.forClient && typeof model.toClient === 'function') {
|
|
console.warn(il`
|
|
Found unexpected "toClient" method on request body model.
|
|
Did you mean "forClient"?
|
|
`);
|
|
}
|
|
assert(!model.forClient || typeof model.forClient === 'function', il`
|
|
Request body model forClient handler must be a function,
|
|
not ${typeof model.forClient}
|
|
`);
|
|
assert(!model.fromClient || typeof model.fromClient === 'function', il`
|
|
Request body model fromClient handler must be a function,
|
|
not ${typeof model.fromClient}
|
|
`);
|
|
}
|
|
|
|
this._bodyParam = {
|
|
model,
|
|
multiple,
|
|
contentTypes,
|
|
description
|
|
};
|
|
return this;
|
|
}
|
|
|
|
response(status, model, mimes, reason) {
|
|
let statusCode = Number(status);
|
|
|
|
if (!status || Number.isNaN(statusCode)) {
|
|
reason = mimes;
|
|
mimes = model;
|
|
model = status;
|
|
statusCode = model === null ? 204 : 200;
|
|
}
|
|
|
|
if (
|
|
typeof model === 'string'
|
|
|| (Array.isArray(model) && typeof model[0] === 'string')
|
|
) {
|
|
reason = mimes;
|
|
mimes = model;
|
|
model = undefined;
|
|
}
|
|
|
|
if (typeof mimes === 'string') {
|
|
reason = mimes;
|
|
mimes = undefined;
|
|
}
|
|
|
|
let multiple = false;
|
|
if (Array.isArray(model)) {
|
|
if (model.length !== 1) {
|
|
throw new Error(il`
|
|
Model must be a model or schema
|
|
or an array containing exactly one model or schema.
|
|
If you are trying to use multiple schemas
|
|
try using joi.alternatives instead.
|
|
`);
|
|
}
|
|
model = model[0];
|
|
multiple = true;
|
|
}
|
|
|
|
if (!mimes) {
|
|
mimes = [];
|
|
}
|
|
|
|
if (!mimes.length && model) {
|
|
mimes.push(MIME_JSON);
|
|
}
|
|
|
|
const contentTypes = mimes.map((mime) => {
|
|
if (mime === 'binary') {
|
|
mime = MIME_BINARY;
|
|
}
|
|
const contentType = mimeTypes.contentType(mime) || mime;
|
|
const parsed = mediaTyper.parse(contentType);
|
|
return mediaTyper.format(_.pick(parsed, [
|
|
'type',
|
|
'subtype',
|
|
'suffix'
|
|
]));
|
|
});
|
|
|
|
if (model) {
|
|
if (model.isJoi) {
|
|
model = {schema: model};
|
|
} else {
|
|
model = _.clone(model);
|
|
}
|
|
if (model.schema && !model.schema.isJoi) {
|
|
model.schema = joi.object(model.schema).required();
|
|
}
|
|
if (!model.forClient && typeof model.toClient === 'function') {
|
|
console.warn(il`
|
|
Found unexpected "toClient" method on response body model at ${statusCode}.
|
|
Did you mean "forClient"?
|
|
`);
|
|
}
|
|
assert(!model.forClient || typeof model.forClient === 'function', il`
|
|
Response body model forClient handler at ${statusCode} must be a function,
|
|
not ${typeof model.forClient}
|
|
`);
|
|
assert(!model.fromClient || typeof model.fromClient === 'function', il`
|
|
Response body model fromClient handler at ${statusCode} must be a function,
|
|
not ${typeof model.fromClient}
|
|
`);
|
|
}
|
|
|
|
this._responses.set(statusCode, {
|
|
model,
|
|
multiple,
|
|
contentTypes,
|
|
description: reason
|
|
});
|
|
return this;
|
|
}
|
|
|
|
error(status, reason) {
|
|
if (typeof status === 'string') {
|
|
status = statuses(status);
|
|
}
|
|
return this.response(
|
|
status,
|
|
DEFAULT_ERROR_SCHEMA,
|
|
null,
|
|
reason
|
|
);
|
|
}
|
|
|
|
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 (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]);
|
|
}
|
|
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._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 = {
|
|
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.model
|
|
? (def.model.isJoi ? def.model : def.model.schema)
|
|
: null
|
|
);
|
|
const response = {};
|
|
if (def.contentTypes && def.contentTypes.length) {
|
|
response.schema = (
|
|
schema
|
|
? joi2schema(schema.isJoi ? schema : joi.object(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);
|
|
}
|