loopback-component-explorer/lib/route-helper.js

230 lines
6.4 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* Module dependencies.
*/
var debug = require('debug')('loopback-explorer:routeHelpers');
var _cloneDeep = require('lodash.clonedeep');
var translateKeys = require('./translate-keys');
/**
* Export the routeHelper singleton.
*/
var routeHelper = module.exports = {
/**
* 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 {Class} class All remoting classes used by the API.
* @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) {
// Some fixes to returns/accepts
var processedRoute = doRouteParameterHacks(route, classDef);
// Add the api to the spec. If the path already exists, add as another operation
// under the api; otherwise add a new api.
addRouteToDoc(processedRoute, doc);
}
};
/**
* 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.
*
* @param {Route} route Route.
* @param {Object} doc Current document.
*/
function addRouteToDoc(route, doc) {
var api = routeToAPI(route);
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);
}
}
/**
* Process a route.
* Contains some hacks to fix some incompatibilities in the accepts and returns
* descriptions.
* @param {Route} route A Route.
* @param {Class} classDef The backing strong remoting class.
*/
function doRouteParameterHacks(route, classDef) {
// Don't modify the existing route as some pieces (such as `returns`) may be shared between routes.
route = _cloneDeep(route);
var split = route.method.split('.');
if (classDef && classDef.sharedCtor && classDef.sharedCtor.accepts && split.length > 2 /* HACK */) {
route.accepts = (route.accepts || []).concat(classDef.sharedCtor.accepts);
}
// Filter out parameters that are generated from the incoming request or body,
// or generated by functions that use those resources.
route.accepts = (route.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;
});
// HACK: makes autogenerated REST routes return the correct model name.
var returns = route.returns && route.returns[0];
if (returns && returns.arg === 'data') {
if (returns.type === 'object') {
returns.type = classDef.name;
} else if (returns.type === 'array') {
returns.type = 'array';
returns.items = {
'$ref': classDef.name
};
}
}
// Translate LDL keys to Swagger keys.
route.accepts = (route.accepts || []).map(translateKeys);
route.returns = (route.returns || []).map(translateKeys);
debug('route %j', route);
return route;
}
/**
* 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
*/
function routeToAPI(route) {
var returnDesc = route.returns && route.returns[0];
return {
path: convertPathFragments(route.path),
operations: [{
method: convertVerb(route.verb),
nickname: route.method.replace(/\./g, '_'), // [rfeng] Swagger UI doesn't escape '.' for jQuery selector
type: returnDesc ? returnDesc.model || prepareDataType(returnDesc.type) : 'void',
items: returnDesc ? returnDesc.items : '',
parameters: route.accepts ? route.accepts.map(acceptToParameter(route)) : [],
responseMessages: [], // TODO(schoon) - We don't have descriptions for this yet.
summary: route.description, // TODO(schoon) - Excerpt?
notes: '' // TODO(schoon) - `description` metadata?
}]
};
}
function convertPathFragments(path) {
return path.split('/').map(function (fragment) {
if (fragment.charAt(0) === ':') {
return '{' + fragment.slice(1) + '}';
}
return fragment;
}).join('/');
}
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.
*/
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 = {
paramType: paramType || type,
name: name,
description: accepts.description,
type: accepts.model || prepareDataType(accepts.type),
required: !!accepts.required,
defaultValue: accepts.defaultValue,
minimum: accepts.minimum,
maximum: accepts.maximum,
allowMultiple: false
};
// HACK: Derive the type from model
if(out.name === 'data' && out.type === 'object') {
out.type = route.method.split('.')[0];
}
if (out.type === 'array') {
out.items = {
type: prepareDataType(accepts.type[0])
};
}
return out;
};
}
/**
* Converts from an sl-remoting data type to a Swagger dataType.
*/
function prepareDataType(type) {
if (!type) {
return 'void';
}
if(Array.isArray(type)) {
return 'array';
}
// TODO(schoon) - Add support for complex dataTypes, "models", etc.
switch (type) {
case 'buffer':
return 'byte';
case 'date':
return 'Date';
case 'number':
return 'double';
}
return type;
}