'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), // [strml] remove leading model name from op, swagger uses leading // path as class name so it remains unique between models. // route.method is always #{className}.#{methodName} nickname: route.method.replace(/.*?\./, ''), 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; } };