'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) { /** * 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 'string'; 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 responseDoc = modelHelper.LDLPropToSwaggerDataType(returns); var responseMessages = [{ code: route.returns && route.returns.length ? 200 : 204, message: 'Request was successful' }]; if (route.errors) { responseMessages.push.apply(responseMessages, route.errors); } var apiDoc = { path: routeHelper.convertPathFragments(route.path), // Create the operation doc. // We are using extendWithType to use `type` for the top-level (200) // response type. We use responseModels for error responses. // see https://github.com/strongloop/loopback-explorer/issues/75 operations: [routeHelper.extendWithType({ 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(/.*?\./, ''), deprecated: route.deprecated, consumes: ['application/json', 'application/xml', 'text/xml'], produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'], parameters: accepts, responseMessages: responseMessages, type: returns.model || returns.type || 'void', summary: typeConverter.convertText(route.description), notes: typeConverter.convertText(route.notes) }, returns)] }; 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: typeConverter.convertText(accepts.description) }; 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. Object.keys(typeDesc).forEach(function(key){ obj[key] = typeDesc[key]; }); //Ensure brief properties are first if (typeof obj === 'object') { var keysToSink = ['authorizations', 'consumes', 'notes', 'produces', 'parameters', 'responseMessages', 'summary']; var outKeys = Object.keys(obj); for (var outKeyIdx in outKeys) { var outKey = outKeys[outKeyIdx]; if (keysToSink.indexOf(outKey) != -1) { var outValue = obj[outKey]; delete obj[outKey]; obj[outKey] = outValue; } } } _assign(obj, typeDesc); return obj; } };