lib/SwaggerManager.js

Summary

Maintainability
F
6 days
Test Coverage
var _ = require('lodash'),
    fs = require('fs');

var SwaggerManager = function(options) {

    options = options || {};

    var swaggerHooks = options.swaggerHooks || {};

    var apiDocsCache = {};

    var consumesDefaults = ['application/json', 'application/x-www-form-urlencoded'];

    var getRoutesGroupedByName = function(routes) {
            var filteredRoutes = _.filter(routes, function(route) {
                if (route.settings.plugins && route.settings.plugins[options.pluginName]) {
                    return (route.settings.plugins[options.pluginName].swagger === undefined || route.settings.plugins[options.pluginName].swagger === true);
                }
                return false;
            });
            return _.groupBy(filteredRoutes, function(route) {
                if (swaggerHooks.routeNameGroup){
                    return swaggerHooks.routeNameGroup(route);
                }
                var groupName = route.path;
                if (groupName.lastIndexOf('}') === groupName.length - 1) {
                    groupName = groupName.substring(0, groupName.lastIndexOf('/'));
                }
                return groupName.substr(1).replace(/\{/g, '_').replace(/\}/g, '_');
            });
        },

        swaggerParamTypeMap = {
            path: 'path',
            query: 'query',
            payload: 'body',
            headers: 'header'
        },

        getRoutesGroupedByPath = function(routes) {
            return _.groupBy(routes, function(item) {
                return item.path;
            });
        },

        findPackageJson = function(startingDirectory) {
            if (!startingDirectory) {
                return false;
            }
            if (fs.existsSync(startingDirectory + '/package.json')) {
                return startingDirectory + '/package.json';
            }
            return findPackageJson(startingDirectory.replace(/\/[^\/]+?$/g, ''));
        },

        getApplicationVersion = function() {
            var executingFile = process.argv[1];
            var packageLoc = findPackageJson(executingFile.replace(/\/[^\/]+?$/g, ''));

            if (packageLoc) {
                return require(packageLoc).version;
            }
            return 'unknown';
        },

        getSwaggerParams = function(route, type, operationNickname) {
            var params = [], prop, param;
            if (swaggerParamTypeMap[type] &&
                route.settings.plugins &&
                route.settings.plugins[options.pluginName] &&
                route.settings.plugins[options.pluginName][type]) {
                var schema = route.settings.plugins[options.pluginName][type];

                if (type === 'payload') {
                    // keep an eye on https://github.com/wordnik/swagger-ui/issues/72
                    // looks like there might be change as far as body params go

                    if (!route.settings.payload || !route.settings.payload.allow || route.settings.payload.allow.indexOf('application/json') >= 0) {
                        param = {
                            paramType: swaggerParamTypeMap[type],
                            name: 'body',
                            description: schema.description,
                            type: operationNickname ? operationNickname + '_body' : schema.type,
                            required: true
                        };
                        params.push(param);
                    }
                    else {
                        for (prop in schema.properties) {
                            if (schema.properties.hasOwnProperty(prop)) {
                                var formParam = {
                                    paramType: 'form',
                                    name: prop,
                                    description: schema.properties[prop].description,
                                    type: schema.properties[prop].type,
                                    required: (schema.required && schema.required.indexOf(prop) >= 0)
                                };
                                params.push(formParam);
                            }
                        }
                    }
                }
                else {
                    for (prop in schema.properties) {
                        if (schema.properties.hasOwnProperty(prop)) {
                            param = {
                                paramType: swaggerParamTypeMap[type],
                                name: prop,
                                description: schema.properties[prop].description,
                                type: schema.properties[prop].type,
                                required: (schema.required && schema.required.indexOf(prop) >= 0)
                            };
                            if (schema.properties[prop].hasOwnProperty('minimum')) {
                                param.minimum = (schema.properties[prop].exclusiveMinimum) ? schema.properties[prop].minimum + 1 : schema.properties[prop].minimum;
                            }
                            if (schema.properties[prop].hasOwnProperty('maximum')) {
                                param.maximum = schema.properties[prop].maximum;
                            }
                            if (schema.properties[prop].hasOwnProperty('enum')) {
                                param.enum = schema.properties[prop].enum;
                            }
                            if (schema.properties[prop].hasOwnProperty('items')) {
                                param.items = schema.properties[prop].items;
                            }
                            params.push(param);
                        }
                    }
                }
            }

            if (swaggerHooks.params){
                swaggerHooks.params(params, route, type);
            }

            return params;
        },

        setOperationConsumes = function (payloadParameters, route, operation) {
            if (!payloadParameters.length) { //if no payload parameters, no consumes attribute
                return;
            }

            if (route.settings.payload && route.settings.payload.allow && route.settings.payload.parse) {
                operation.consumes = route.settings.payload.allow;
            } else {
                operation.consumes = consumesDefaults;
            }
        },

        getSwaggerOperationForRoute = function(route, resourceType, path) {
            var pathParts = path.split('/'),
                regex = /^\{.+\}$/,
                lastPart = pathParts.pop(),
                resourceId = (regex.test(lastPart)) ? lastPart : null,
                resourceName = (!resourceId) ? lastPart : pathParts.pop(),
                nickname = (!resourceId) ? resourceName : resourceName + '_by_' + resourceId.replace(/[\{\}]*/g, ''),
                operation = {
                    method: route.method,
                    summary: route.settings.description,
                    notes: route.settings.notes,
                    tags: route.settings.tags,
                    type: 'void',
                    nickname: route.settings.plugins[options.pluginName].nickname || (route.method + '_' + nickname),
                    parameters: []
                },
                schema,
                plugins = route.settings.plugins;



            if (plugins &&
                plugins[options.pluginName] &&
                plugins[options.pluginName].response){

                if (plugins[options.pluginName].response.schema){
                    schema = plugins[options.pluginName].response.schema;

                    if (schema.oneOf &&
                        Array.isArray(schema.oneOf) &&
                        schema.oneOf.length > 0) {
                        schema = schema.oneOf[0];
                    }

                    if (schema.type === 'object') {
                        operation.type = operation.nickname + '_response';
                    }
                    else if (schema.type === 'array') {
                        operation.type = schema.type;
                        operation.items = { '$ref': operation.nickname + '_response' };
                    }
                    else {
                        operation.type = schema.type;
                        operation.description = schema.description;
                        operation.defaultValue = schema.defaultValue;
                    }
                }

                if (plugins[options.pluginName].response.messages){
                    operation.responseMessages = plugins[options.pluginName].response.messages;
                }
            }

            operation.parameters = operation.parameters.concat(getSwaggerParams(route, 'path'));

            operation.parameters = operation.parameters.concat(getSwaggerParams(route, 'query'));

            var payloadParameters = getSwaggerParams(route, 'payload', operation.nickname);
            operation.parameters = operation.parameters.concat(payloadParameters);

            operation.parameters = operation.parameters.concat(getSwaggerParams(route, 'headers'));

            setOperationConsumes(payloadParameters, route, operation);

            //TODO: warn if more than 1 param with the same name

            if (swaggerHooks.operation){
                swaggerHooks.operation(operation, route, resourceType, path);
            }

            return operation;
        },

        getModelForRoute = function(route, modelKind, modelGetter){
            var plugins = route.settings.plugins;
            var model;
            if (plugins && plugins[options.pluginName] &&
                plugins[options.pluginName][modelKind] && modelGetter()) {
                model = _.cloneDeep(modelGetter());

                if(model.oneOf && Array.isArray(model.oneOf) && model.oneOf.length > 0) {
                    model = model.oneOf[0];
                }

                if (model.type === 'array') {
                    model = _.cloneDeep(model.items);
                }
                else if (model.type !== 'object') {
                    return null;
                }
            }

            return model;
        },

        getRequestModelForRoute = function(route){
            return getModelForRoute(route, 'payload', function(){
                return route.settings.plugins[options.pluginName].payload;
            });
        },

        getResponseModelForRoute = function(route) {
            return getModelForRoute(route, 'response', function(){
                return route.settings.plugins[options.pluginName].response.schema;
            });
        },

        generateNestedModels = function (model, models, prefix, parent){
            if (!model.properties){
                return;
            }

            parent = parent || '';

            Object.keys(model.properties).forEach(function(prop){
                if (model.properties[prop].properties ||
                    model.properties[prop].oneOf ||
                     model.properties[prop].type === 'object' ||
                     (Array.isArray(model.properties[prop].type) &&
                     model.properties[prop].type.indexOf('object') >= 0)){

                    if (model.properties[prop].oneOf &&
                        Array.isArray(model.properties[prop].oneOf) &&
                        model.properties[prop].oneOf.length > 0) {
                        // Replace the node with the first occurrence of the Array and go through this model again.
                        model.properties[prop] = model.properties[prop].oneOf[0];
                        return generateNestedModels(model, models, prefix, parent);
                    }

                    models[prefix + prop] = _.cloneDeep(model.properties[prop]);
                    generateNestedModels(model.properties[prop], models, prefix, parent + prop + '.');
                    model.properties[prop] = { $ref: prefix + prop};
                }
            });
        },

        defaultResourceListingModel = {
            apiVersion: options.apiVersion || getApplicationVersion().split('.')[0],
            swaggerVersion: '1.2',
            apis: []
        },
        defaultApiDeclaration = {
            apiVersion: options.apiVersion || getApplicationVersion().split('.')[0],
            swaggerVersion: '1.2',
            basePath: '',
            resourcePath: '',
            produces: null,
            apis: null
        };

    this.isValidApi = function(routes, apiName) {
        var routesByGroupNames = getRoutesGroupedByName(routes);
        return routesByGroupNames.hasOwnProperty(apiName);
    };

    this.getResourceListingModel = function(routes) {
        var routesByGroupNames = getRoutesGroupedByName(routes),
            resourceListingModel = _.cloneDeep(defaultResourceListingModel);

        resourceListingModel.apis = [];
        Object.keys(routesByGroupNames).forEach(function(item) {
            resourceListingModel.apis.push({ path: '/' + item });
        });

        return resourceListingModel;
    };

    this.getApiDeclarationModel = function(routes, apiName) {
        var cached = apiDocsCache[apiName];
        if (cached){
            return cached;
        }

        var routesByGroupNames = getRoutesGroupedByName(routes),
            routesByPath = getRoutesGroupedByPath(routesByGroupNames[apiName]),
            apiObj = _.cloneDeep(defaultApiDeclaration);

        apiObj.apis = [];
        apiObj.basePath = options.baseUrl;
        apiObj.resourcePath = '/' + apiName;
        apiObj.produces = options.responseContentTypes;

        apiObj.consumes = _.chain(routesByPath)
                            .map(function(val) { return val; })
                            .flatten()
                            .map(function(route) {
                                if (route.settings.payload && route.settings.payload.parse) {
                                    return (route.settings.payload.allow) ? route.settings.payload.allow : consumesDefaults;
                                }
                                return null;
                            })
                            .flatten()
                            .uniq()
                            .compact()
                            .value();

        var models = {};

        for (var prop in routesByPath) {
            if (routesByPath.hasOwnProperty(prop)) {
                var api = {
                    path: prop,
                    operations: []
                };

                routesByPath[prop].forEach(function(route) {
                    var operation = getSwaggerOperationForRoute(route, apiName, prop);
                    api.operations.push(operation);

                    var responseModel = getResponseModelForRoute(route);

                    if (responseModel) {
                        models[operation.nickname + '_response'] = responseModel;
                        generateNestedModels(responseModel, models, operation.nickname + '_response_');
                    }

                    var requestModel = getRequestModelForRoute(route);

                    if (requestModel) {
                        models[operation.nickname + '_body'] = requestModel;
                        generateNestedModels(requestModel, models, operation.nickname + '_body_');
                    }
                });

                apiObj.apis.push(api);
            }
        }

        if (swaggerHooks.models){
            swaggerHooks.models(models);
        }

        apiObj.models = models;

        apiDocsCache[apiName] = apiObj;

        return apiObj;
    };

};

module.exports = SwaggerManager;