320 lines
10 KiB
JavaScript
320 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var debug = require('debug')('loopback:explorer:routeHelpers');
|
|
var _cloneDeep = require('lodash.clonedeep');
|
|
var translateDataTypeKeys = require('./translate-data-type-keys');
|
|
var modelHelper = require('./model-helper');
|
|
|
|
/**
|
|
* Export the routeHelper singleton.
|
|
*/
|
|
var routeHelper = module.exports = {
|
|
/**
|
|
* Given a route, generate an API description and add it to the doc.
|
|
* If a route shares a path with another route (same path, different verb),
|
|
* add it as a new operation under that API description.
|
|
*
|
|
* Routes can be translated to API declaration 'operations',
|
|
* but they need a little massaging first. The `accepts` and
|
|
* `returns` declarations need some basic conversions to be compatible.
|
|
*
|
|
* This method will convert the route and add it to the doc.
|
|
* @param {Route} route Strong Remoting Route object.
|
|
* @param {Class} classDef Strong Remoting class.
|
|
* @param {Object} doc The class's backing API declaration doc.
|
|
*/
|
|
addRouteToAPIDeclaration: function (route, classDef, doc) {
|
|
var api = routeHelper.routeToAPIDoc(route, classDef);
|
|
var matchingAPIs = doc.apis.filter(function(existingAPI) {
|
|
return existingAPI.path === api.path;
|
|
});
|
|
if (matchingAPIs.length) {
|
|
matchingAPIs[0].operations.push(api.operations[0]);
|
|
} else {
|
|
doc.apis.push(api);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Massage route.accepts.
|
|
* @param {Object} route Strong Remoting Route object.
|
|
* @param {Class} classDef Strong Remoting class.
|
|
* @return {Array} Array of param docs.
|
|
*/
|
|
convertAcceptsToSwagger: function convertAcceptsToSwagger(route, classDef) {
|
|
var split = route.method.split('.');
|
|
var accepts = _cloneDeep(route.accepts) || [];
|
|
if (classDef && classDef.sharedCtor &&
|
|
classDef.sharedCtor.accepts && split.length > 2 /* HACK */) {
|
|
accepts = accepts.concat(classDef.sharedCtor.accepts);
|
|
}
|
|
|
|
// Filter out parameters that are generated from the incoming request,
|
|
// or generated by functions that use those resources.
|
|
accepts = accepts.filter(function(arg){
|
|
if (!arg.http) return true;
|
|
// Don't show derived arguments.
|
|
if (typeof arg.http === 'function') return false;
|
|
// Don't show arguments set to the incoming http request.
|
|
// Please note that body needs to be shown, such as User.create().
|
|
if (arg.http.source === 'req') return false;
|
|
return true;
|
|
});
|
|
|
|
// Translate LDL keys to Swagger keys.
|
|
accepts = accepts.map(translateDataTypeKeys);
|
|
|
|
// Turn accept definitions in to parameter docs.
|
|
accepts = accepts.map(routeHelper.acceptToParameter(route));
|
|
|
|
return accepts;
|
|
},
|
|
|
|
/**
|
|
* Massage route.returns.
|
|
* @param {Object} route Strong Remoting Route object.
|
|
* @param {Class} classDef Strong Remoting class.
|
|
* @return {Object} A single returns param doc.
|
|
*/
|
|
convertReturnsToSwagger: function convertReturnsToSwagger(route, classDef) {
|
|
var routeReturns = _cloneDeep(route.returns) || [];
|
|
// HACK: makes autogenerated REST routes return the correct model name.
|
|
var firstReturn = routeReturns && routeReturns[0];
|
|
if (firstReturn && firstReturn.arg === 'data') {
|
|
if (firstReturn.type === 'object') {
|
|
firstReturn.type = classDef.name;
|
|
} else if (firstReturn.type === 'array') {
|
|
firstReturn.type = [classDef.name];
|
|
}
|
|
}
|
|
|
|
// Translate LDL keys to Swagger keys.
|
|
var returns = routeReturns.map(translateDataTypeKeys);
|
|
|
|
// Convert `returns` into a single object for later conversion into an
|
|
// operation object.
|
|
if (returns && returns.length > 1) {
|
|
// TODO ad-hoc model definition in the case of multiple return values.
|
|
returns = {model: 'object'};
|
|
} else {
|
|
returns = returns[0] || {};
|
|
}
|
|
|
|
return returns;
|
|
},
|
|
|
|
/**
|
|
* Converts from an sl-remoting-formatted "Route" description to a
|
|
* Swagger-formatted "API" description.
|
|
* See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object
|
|
*/
|
|
routeToAPIDoc: function routeToAPIDoc(route, classDef) {
|
|
/**
|
|
* Converts from an sl-remoting data type to a Swagger dataType.
|
|
*/
|
|
function prepareDataType(type) {
|
|
if (!type) {
|
|
return 'void';
|
|
}
|
|
|
|
if(Array.isArray(type)) {
|
|
if (type.length > 0) {
|
|
if (typeof type[0] === 'string') {
|
|
return '[' + type[0] + ']';
|
|
} else if (typeof type[0] === 'function') {
|
|
return '[' + type[0].name + ']';
|
|
} else if (typeof type[0] === 'object') {
|
|
if (typeof type[0].type === 'function') {
|
|
return '[' + type[0].type.name + ']';
|
|
} else {
|
|
return '[' + type[0].type + ']';
|
|
}
|
|
} else {
|
|
return '[' + type + ']';
|
|
}
|
|
}
|
|
return 'array';
|
|
}
|
|
|
|
// TODO(schoon) - Add support for complex dataTypes, "models", etc.
|
|
switch (type) {
|
|
case 'Array':
|
|
return 'array';
|
|
case 'Boolean':
|
|
return 'boolean';
|
|
case 'buffer':
|
|
return 'string';
|
|
case 'Date':
|
|
return 'date';
|
|
case 'number':
|
|
case 'Number':
|
|
return 'double';
|
|
case 'Object':
|
|
return 'object';
|
|
case 'String':
|
|
return 'string';
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
var returnDesc;
|
|
|
|
// Some parameters need to be altered; eventually most of this should
|
|
// be removed.
|
|
var accepts = routeHelper.convertAcceptsToSwagger(route, classDef);
|
|
var returns = routeHelper.convertReturnsToSwagger(route, classDef);
|
|
var responseMessages = [
|
|
{
|
|
code: 200,
|
|
message: 'Request was successful',
|
|
responseModel: returns.model || prepareDataType(returns.type) || 'void'
|
|
}
|
|
];
|
|
if (route.errors) {
|
|
responseMessages.push.apply(responseMessages, route.errors);
|
|
}
|
|
|
|
debug('route %j', route);
|
|
|
|
var apiDoc = {
|
|
path: routeHelper.convertPathFragments(route.path),
|
|
// Create the operation doc. Use `extendWithType` to add the necessary
|
|
// `items` and `format` fields.
|
|
operations: [routeHelper.extendWithType({
|
|
method: routeHelper.convertVerb(route.verb),
|
|
// [rfeng] Swagger UI doesn't escape '.' for jQuery selector
|
|
nickname: route.method.replace(/\./g, '_'),
|
|
deprecated: route.deprecated,
|
|
type: returns.model || returns.type || 'void',
|
|
summary: Array.isArray(route.description) ? route.description.join('') : route.description, // TODO(schoon) - Excerpt?
|
|
notes: Array.isArray(route.notes) ? route.notes.join('') : route.notes, // TODO(schoon) - `description` metadata?
|
|
consumes: ['application/json', 'application/xml', 'text/xml'],
|
|
produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'],
|
|
parameters: accepts,
|
|
responseMessages: responseMessages
|
|
})]
|
|
};
|
|
|
|
return apiDoc;
|
|
},
|
|
|
|
convertPathFragments: function convertPathFragments(path) {
|
|
return path.split('/').map(function (fragment) {
|
|
if (fragment.charAt(0) === ':') {
|
|
return '{' + fragment.slice(1) + '}';
|
|
}
|
|
return fragment;
|
|
}).join('/');
|
|
},
|
|
|
|
convertVerb: function convertVerb(verb) {
|
|
if (verb.toLowerCase() === 'all') {
|
|
return 'POST';
|
|
}
|
|
|
|
if (verb.toLowerCase() === 'del') {
|
|
return 'DELETE';
|
|
}
|
|
|
|
return verb.toUpperCase();
|
|
},
|
|
|
|
/**
|
|
* A generator to convert from an sl-remoting-formatted "Accepts" description
|
|
* to a Swagger-formatted "Parameter" description.
|
|
*/
|
|
acceptToParameter: function acceptToParameter(route) {
|
|
var type = 'form';
|
|
|
|
if (route.verb.toLowerCase() === 'get') {
|
|
type = 'query';
|
|
}
|
|
|
|
return function (accepts) {
|
|
var name = accepts.name || accepts.arg;
|
|
var paramType = type;
|
|
|
|
// TODO: Regex. This is leaky.
|
|
if (route.path.indexOf(':' + name) !== -1) {
|
|
paramType = 'path';
|
|
}
|
|
|
|
// Check the http settings for the argument
|
|
if(accepts.http && accepts.http.source) {
|
|
paramType = accepts.http.source;
|
|
}
|
|
|
|
var out = {
|
|
name: name,
|
|
required: !!accepts.required,
|
|
paramType: paramType || type,
|
|
type: accepts.type,
|
|
$ref: accepts.model,
|
|
items: accepts.items,
|
|
uniqueItems: accepts.uniqueItems,
|
|
format: accepts.format,
|
|
pattern: accepts.pattern,
|
|
defaultValue: accepts.defaultValue,
|
|
enum: accepts.enum,
|
|
minimum: accepts.minimum,
|
|
maximum: accepts.maximum,
|
|
allowMultiple: accepts.allowMultiple,
|
|
description: Array.isArray(accepts.description) ? accepts.description.join('') : accepts.description
|
|
};
|
|
|
|
out = routeHelper.extendWithType(out);
|
|
|
|
// HACK: Derive the type from model
|
|
if(out.name === 'data' && out.type === 'object') {
|
|
out.type = route.method.split('.')[0];
|
|
}
|
|
|
|
return out;
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Extends an Operation Object or Parameter object with
|
|
* a proper Swagger type and optional `format` and `items` fields.
|
|
* Does not modify original object.
|
|
* @param {Object} obj Object to extend.
|
|
* @return {Object} Extended object.
|
|
*/
|
|
extendWithType: function extendWithType(obj) {
|
|
obj = _cloneDeep(obj);
|
|
|
|
// Format the `type` property using our LDL converter.
|
|
var typeDesc = modelHelper
|
|
.LDLPropToSwaggerDataType({type: obj.model || obj.type});
|
|
// The `typeDesc` may have additional attributes, such as
|
|
// `format` for non-primitive types.
|
|
Object.keys(typeDesc).forEach(function(key){
|
|
obj[key] = typeDesc[key];
|
|
});
|
|
|
|
//Ensure brief properties are first
|
|
if (typeof obj === 'object') {
|
|
var keysToSink = ['authorizations', 'consumes', 'notes', 'produces',
|
|
'parameters', 'responseMessages', 'summary'];
|
|
var outKeys = Object.keys(obj);
|
|
for (var outKeyIdx in outKeys) {
|
|
var outKey = outKeys[outKeyIdx];
|
|
if (keysToSink.indexOf(outKey) != -1) {
|
|
var outValue = obj[outKey];
|
|
delete obj[outKey];
|
|
obj[outKey] = outValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
};
|
|
|
|
|