'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; }; }, };