'use strict'; /** * Module dependencies. */ var debug = require('debug')('loopback:explorer:routeHelpers'); var _cloneDeep = require('lodash.clonedeep'); var translateDataTypeKeys = require('./translate-data-type-keys'); var modelHelper = require('./model-helper'); /** * 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') return false; return true; }); // Translate LDL keys to Swagger keys. accepts = accepts.map(translateDataTypeKeys); // 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]; } } // Translate LDL keys to Swagger keys. var returns = routeReturns.map(translateDataTypeKeys); // Convert `returns` into a single object for later conversion into an // operation object. if (returns && returns.length > 1) { // TODO ad-hoc model definition in the case of multiple return values. returns = {model: 'object'}; } else { returns = returns[0] || {}; } return returns; }, /** * 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) { /** * Converts from an sl-remoting data type to a Swagger dataType. */ function prepareDataType(type) { if (!type) { return 'void'; } if(Array.isArray(type)) { if (type.length > 0) { if (typeof type[0] === 'string') { return '[' + type[0] + ']'; } else if (typeof type[0] === 'function') { return '[' + type[0].name + ']'; } else if (typeof type[0] === 'object') { if (typeof type[0].type === 'function') { return '[' + type[0].type.name + ']'; } else { return '[' + type[0].type + ']'; } } else { return '[' + type + ']'; } } return 'array'; } // TODO(schoon) - Add support for complex dataTypes, "models", etc. switch (type) { case 'Array': return 'array'; case 'Boolean': return 'boolean'; case 'buffer': return 'byte'; case 'Date': return 'date'; case 'number': case 'Number': return 'double'; case 'Object': return 'object'; case 'String': return 'string'; } return type; } var returnDesc; // 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); var responseMessages = [ { code: 200, message: 'Request was successful', responseModel: returns.model || prepareDataType(returns.type) || 'void' } ]; if (route.errors) { responseMessages.push.apply(responseMessages, route.errors); } debug('route %j', route); var apiDoc = { path: routeHelper.convertPathFragments(route.path), // Create the operation doc. Use `extendWithType` to add the necessary // `items` and `format` fields. operations: [routeHelper.extendWithType({ method: routeHelper.convertVerb(route.verb), deprecated: route.deprecated, // [rfeng] Swagger UI doesn't escape '.' for jQuery selector nickname: route.method.replace(/\./g, '_'), summary: route.description, // TODO(schoon) - Excerpt? notes: route.notes, // TODO(schoon) - `description` metadata? consumes: ['application/json', 'application/xml', 'text/xml'], produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'], parameters: accepts, responseMessages: responseMessages })] }; 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 = { name: name, required: !!accepts.required, paramType: paramType || type, type: accepts.type, $ref: accepts.model, items: accepts.items, uniqueItems: accepts.uniqueItems, format: accepts.format, pattern: accepts.pattern, defaultValue: accepts.defaultValue, enum: accepts.enum, minimum: accepts.minimum, maximum: accepts.maximum, allowMultiple: accepts.allowMultiple, description: accepts.description }; out = routeHelper.extendWithType(out); // 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. * @return {Object} Extended object. */ extendWithType: function extendWithType(obj) { obj = _cloneDeep(obj); // Format the `type` property using our LDL converter. var typeDesc = modelHelper .LDLPropToSwaggerDataType({type: obj.model || obj.type}); // The `typeDesc` may have additional attributes, such as // `format` for non-primitive types. Object.keys(typeDesc).forEach(function(key){ obj[key] = typeDesc[key]; }); return obj; } };