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

253 lines
8.1 KiB
JavaScript

'use strict';
/**
* Module dependencies.
*/
var debug = require('debug')('loopback:explorer:routeHelpers');
var _cloneDeep = require('lodash.clonedeep');
var _assign = require('lodash.assign');
var modelHelper = require('./model-helper');
var typeConverter = require('./type-converter');
/**
* 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' ||
arg.http.source === 'res' ||
arg.http.source === 'context') {
return false;
}
return true;
});
// 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];
}
}
// Convert `returns` into a single object for later conversion into an
// operation object.
if (routeReturns && routeReturns.length > 1) {
// TODO ad-hoc model definition in the case of multiple return values.
routeReturns = { type: 'object' };
} else {
// Per the spec:
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object
// This is the only object that may have a type of 'void'.
routeReturns = routeReturns[0] || { type: 'void' };
}
return routeReturns;
},
/**
* 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) {
// 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);
debug('route %j', route);
var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns);
// Note: Swagger Spec does not provide a way how to specify
// that the responseModel is "array of X". However,
// Swagger UI converts Arrays to the item types anyways,
// therefore it should be ok to do the same here.
var responseModel = responseDoc.type === 'array' ?
responseDoc.items.type : responseDoc.type;
var responseMessages = [{
code: route.returns && route.returns.length ? 200 : 204,
message: 'Request was successful',
responseModel: responseModel
}];
if (route.errors) {
responseMessages.push.apply(responseMessages, route.errors);
}
var apiDoc = {
path: routeHelper.convertPathFragments(route.path),
// Create the operation doc.
// Note that we are not calling `extendWithType`, as the response type
// is specified in the first response message.
operations: [{
method: routeHelper.convertVerb(route.verb),
// [rfeng] Swagger UI doesn't escape '.' for jQuery selector
nickname: route.method.replace(/\./g, '_'),
parameters: accepts,
responseMessages: responseMessages,
summary: typeConverter.convertText(route.description),
notes: typeConverter.convertText(route.notes),
deprecated: route.deprecated
}]
};
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 = {
paramType: paramType || type,
name: name,
description: typeConverter.convertText(accepts.description),
required: !!accepts.required,
allowMultiple: false
};
out = routeHelper.extendWithType(out, accepts);
// 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.
* @param {Object} ldlType LDL type definition
* @return {Object} Extended object.
*/
extendWithType: function extendWithType(obj, ldlType) {
obj = _cloneDeep(obj);
// Format the `type` property using our LDL converter.
var typeDesc = modelHelper.LDLPropToSwaggerDataType(ldlType);
// The `typeDesc` may have additional attributes, such as
// `format` for non-primitive types.
_assign(obj, typeDesc);
return obj;
}
};