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

253 lines
8.6 KiB
JavaScript

'use strict';
/**
* Module dependencies.
*/
var debug = require('debug')('loopback:explorer:routeHelpers');
var _assign = require('lodash').assign;
var typeConverter = require('./type-converter');
var schemaBuilder = require('./schema-builder');
/**
* 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 path entry.
*
* 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 {TypeRegistry} typeRegistry Registry of types and models.
* @param {Object} paths Swagger Path Object,
* see https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#pathsObject
*/
addRouteToSwaggerPaths: function(route, classDef, typeRegistry, paths) {
var entryToAdd = routeHelper.routeToPathEntry(route, classDef,
typeRegistry);
if (!(entryToAdd.path in paths)) {
paths[entryToAdd.path] = {};
}
paths[entryToAdd.path][entryToAdd.method] = entryToAdd.operation;
},
/**
* Massage route.accepts.
* @param {Object} route Strong Remoting Route object.
* @param {Class} classDef Strong Remoting class.
* @param {TypeRegistry} typeRegistry Registry of types and models.
* @return {Array} Array of param docs.
*/
convertAcceptsToSwagger: function(route, classDef, typeRegistry) {
var accepts = route.accepts || [];
var split = route.method.split('.');
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, classDef, typeRegistry));
return accepts;
},
/**
* Massage route.returns.
* @param {Object} route Strong Remoting Route object.
* @return {Object} A single returns param doc.
*/
convertReturnsToSwagger: function(route, typeRegistry) {
var routeReturns = route.returns;
if (!routeReturns || !routeReturns.length) {
// An operation that returns nothing will have
// no schema declaration for its response.
return undefined;
}
if (routeReturns.length === 1 && routeReturns[0].root) {
if (routeReturns[0].model)
return { $ref: typeRegistry.reference(routeReturns[0].model) };
return schemaBuilder.buildFromLoopBackType(routeReturns[0], typeRegistry);
}
// Convert `returns` into a single object for later conversion into an
// operation object.
// TODO ad-hoc model definition in the case of multiple return values.
// It is enough to replace 'object' with an anonymous type definition
// based on all routeReturn items. The schema converter should take
// care of the remaning conversions.
var def = { type: 'object' };
return schemaBuilder.buildFromLoopBackType(def, typeRegistry);
},
/**
* Converts from an sl-remoting-formatted "Route" description to a
* Swagger-formatted "Path Item Object"
* See swagger-spec/2.0.md#pathItemObject
*/
routeToPathEntry: function(route, classDef, typeRegistry) {
// Some parameters need to be altered; eventually most of this should
// be removed.
var accepts = routeHelper.convertAcceptsToSwagger(route, classDef,
typeRegistry);
var returns = routeHelper.convertReturnsToSwagger(route, typeRegistry);
var defaultCode = route.returns && route.returns.length ? 200 : 204;
// TODO - support strong-remoting's option for a custom response code
var responseMessages = {};
responseMessages[defaultCode] = {
description: 'Request was successful',
schema: returns,
// TODO - headers, examples
};
if (route.errors) {
// TODO define new LDL syntax that is status-code-indexed
// and which allow users to specify headers & examples
route.errors.forEach(function(msg) {
responseMessages[msg.code] = {
description: msg.message,
schema: schemaBuilder.buildFromLoopBackType(msg.responseModel,
typeRegistry),
// TODO - headers, examples
};
});
}
debug('route %j', route);
var tags = [];
if (classDef && classDef.name) {
tags.push(classDef.name);
}
var entry = {
path: routeHelper.convertPathFragments(route.path),
method: routeHelper.convertVerb(route.verb),
operation: {
tags: tags,
summary: typeConverter.convertText(route.description),
description: typeConverter.convertText(route.notes),
// [bajtos] We used to remove leading model name from the operation
// name for Swagger Spec 1.2. Swagger Spec 2.0 requires
// operation ids to be unique, thus we have to include the model name.
operationId: route.method,
// [bajtos] we are omitting consumes and produces, as they are same
// for all methods and they are already specified in top-level fields
parameters: accepts,
responses: responseMessages,
deprecated: !!route.deprecated,
// TODO: security
}
};
return entry;
},
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.toLowerCase();
},
/**
* A generator to convert from an sl-remoting-formatted "Accepts" description
* to a Swagger-formatted "Parameter" description.
*/
acceptToParameter: function acceptToParameter(route, classDef, typeRegistry) {
var DEFAULT_TYPE =
route.verb.toLowerCase() === 'get' ? 'query' : 'formData';
return function (accepts) {
var name = accepts.name || accepts.arg;
var paramType = DEFAULT_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;
}
// TODO: ensure that paramType has a valid value
// path, query, header, body, formData
// See swagger-spec/2.0.md#parameterObject
var paramObject = {
name: name,
in: paramType,
description: typeConverter.convertText(accepts.description),
required: !!accepts.required
};
var schema = schemaBuilder.buildFromLoopBackType(accepts, typeRegistry);
if (paramType === 'body') {
// HACK: Derive the type from model
if (paramObject.name === 'data' && schema.type === 'object') {
paramObject.schema = { $ref: typeRegistry.reference(classDef.name) };
} else {
paramObject.schema = schema;
}
} else {
var isComplexType = schema.type === 'object' ||
schema.type === 'array' ||
schema.$ref;
if (isComplexType) {
paramObject.type = 'string';
paramObject.format = 'JSON';
// TODO support array of primitive types
// and map them to Swagger array of primitive types
} else {
_assign(paramObject, schema);
}
}
return paramObject;
};
},
};