253 lines
8.6 KiB
JavaScript
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;
|
|
};
|
|
},
|
|
};
|