tjwebb/sails-swagger

View on GitHub
lib/xfmr.js

Summary

Maintainability
C
1 day
Test Coverage
import hoek from 'hoek'
import _ from 'lodash'
import Spec from './spec'
import pluralize from 'pluralize'

const methodMap = {
    post: 'Create Object(s)',
    get: 'Read Object(s)',
    put: 'Update Object(s)',
    patch: 'Update Object(s)',
    delete: 'Destroy Object(s)',
    options: 'Get Resource Options',
    head: 'Get Resource headers'
}

function getBlueprintPrefixes() {
    // Add a "/" to a prefix if it's missing
    function formatPrefix(prefix) {
        return (prefix.indexOf('/') !== 0 ? '/' : '') + prefix
    }

    let prefixes = []
        // Check if blueprints hook is not removed
    if (sails.config.blueprints) {
        if (sails.config.blueprints.prefix) {
            // Case of blueprints prefix
            prefixes.push(formatPrefix(sails.config.blueprints.prefix))
            if (sails.config.blueprints.rest && sails.config.blueprints.restPrefix) {
                // Case of blueprints prefix + rest prefix
                prefixes.unshift(prefixes[0] + formatPrefix(sails.config.blueprints.restPrefix))
            }
        } else if (sails.config.blueprints.rest && sails.config.blueprints.restPrefix) {
            // Case of rest prefix
            prefixes.push(formatPrefix(sails.config.blueprints.restPrefix))
        }
    }
    return prefixes
}

const Transformer = {

    getSwagger(sails, pkg) {
        return {
            swagger: '2.0',
            info: Transformer.getInfo(pkg),
            host: sails.config.swagger.host,
            tags: Transformer.getTags(sails),
            definitions: Transformer.getDefinitions(sails),
            paths: Transformer.getPaths(sails)
        }
    },

    /**
     * Convert a package.json file into a Swagger Info Object
     * http://swagger.io/specification/#infoObject
     */
    getInfo(pkg) {
        return hoek.transform(pkg, {
            'title': 'name',
            'description': 'description',
            'version': 'version',

            'contact.name': 'author',
            'contact.url': 'homepage',

            'license.name': 'license'
        })
    },

    /**
     * http://swagger.io/specification/#tagObject
     */
    getTags(sails) {
        return _.map(_.pluck(sails.controllers, 'globalId'), tagName => {
            return {
                name: tagName
                    //description: `${tagName} Controller`
            }
        })
    },

    /**
     * http://swagger.io/specification/#definitionsObject
     */
    getDefinitions(sails) {
        let definitions = _.transform(sails.models, (definitions, model, modelName) => {
            definitions[model.identity] = {
                properties: Transformer.getDefinitionProperties(model.definition)
            }
        })

        delete definitions['undefined']

        return definitions
    },

    getDefinitionProperties(definition) {

        return _.mapValues(definition, (def, attrName) => {
            let property = _.pick(def, [
                'type', 'description', 'format', 'model'
            ])

            return property.model && sails.config.blueprints.populate ? { '$ref': Transformer.generateDefinitionReference(property.model) } : Spec.getPropertyType(property.type)
        })
    },

    /**
     * Convert the internal Sails route map into a Swagger Paths
     * Object
     * http://swagger.io/specification/#pathsObject
     * http://swagger.io/specification/#pathItemObject
     */
    getPaths(sails) {
        let routes = sails.router._privateRouter.routes
        let pathGroups = _.chain(routes)
            .values()
            .flatten()
            .unique(route => {
                return route.path + route.method + JSON.stringify(route.keys)
            })
            .reject({ path: '/*' })
            .reject({ path: '/__getcookie' })
            .reject({ path: '/csrfToken' })
            .reject({ path: '/csrftoken' })
            .groupBy('path')
            .value()

        pathGroups = _.reduce(pathGroups, function(result, routes, path) {
            path = path.replace(/:(\w+)\??/g, '{$1}')
            if (result[path])
                result[path] = _.union(result[path], routes)
            else
                result[path] = routes
            return result
        }, [])

        let inferredPaths = _.mapValues(pathGroups, pathGroup => {
            return Transformer.getPathItem(sails, pathGroup)
        }) || [];
        return  _.merge( inferredPaths ,  Transformer.getDefinitionsFromRouteConfig(sails)  );
    },

    /**
     * Convert the swagger routes defined in sails.config.routes to route map
     * Object
     * http://swagger.io/specification/#pathsObject
     * http://swagger.io/specification/#pathItemObject
     */
    getDefinitionsFromRouteConfig(sails) {
        let routes = sails.config.routes,
            swaggerdefs = _.pick(routes, function(routeConfig, route) {
                return _.has(routeConfig, 'swagger');
            });

        let swaggerDefinitions = _.chain(routes)
            .pick(function(routeConfig, route) {
                return _.has(routeConfig, 'swagger');
            }).mapValues(function(route, key) {
                var swaggerdef = route.swagger || {};
                swaggerdef.responses = _.chain(swaggerdef.responses || {})
                    .mapValues(function(response, responseCode) {
                        if (response.schema || response.model) {
                            response.schema = response.schema || response.model;
                            if (typeof response.schema == 'string') {
                                response.schema = {
                                    '$ref': '#/definitions/' + (response.schema || '').toLowerCase()
                                };
                            }else if(typeof response.schema == 'object'){
                                if( (response.schema.type || '').toLowerCase()=='array' ){
                                    response.schema.items = response.schema.items || {
                                        '$ref': '#/definitions/'+(response.schema.model || '').toLowerCase()
                                    };
                                    delete response.schema.model;
                                }
                            }
                        }
                        return response;
                    }).value();
                swaggerdef.parameters = _.chain(swaggerdef.parameters || [])
                    .map(function(parameter) {

                        if (typeof parameter == 'string') {
                            return '#/definitions/' + (parameter || '').toLowerCase();

                        } else {
                            parameter.schema = parameter.schema || null;
                            return parameter;
                        }
                    }).value();

                var methods = swaggerdef.methods || ['get'];
                delete swaggerdef.methods;
                var defs = {};
                _.map(methods, function(method) {
                    defs[(method || '').toLowerCase().trim()] = swaggerdef;
                });
                return defs;
            }).value();

        var swaggerPaths = {};
        for (var defRoute in swaggerDefinitions) {
            var sPath = (defRoute || '').toLowerCase().replace(/(get|post|put|option|delete)? ?/g, '');
            sPath = sPath.replace(/:(\w+)\??/g, '{$1}');
            swaggerPaths[sPath] = _.merge(  swaggerPaths[sPath] || {}, swaggerDefinitions[defRoute] );
        }

        return swaggerPaths || [];
    },

    getModelFromPath(sails, path) {
        let [$, parentModelName, parentId, childAttributeName, childId] = path.split('/')
        let parentModel = sails.models[parentModelName] || parentModelName ? sails.models[pluralize.singular(parentModelName)] : undefined
        let childAttribute = _.get(parentModel, ['attributes', childAttributeName])
        let childModelName = _.get(childAttribute, 'collection') || _.get(childAttribute, 'model')
        let childModel = sails.models[childModelName] || childModelName ? sails.models[pluralize.singular(childModelName)] : undefined

        return childModel || parentModel
    },

    getModelIdentityFromPath(sails, path) {
        let model = Transformer.getModelFromPath(sails, path)
        if (model) {
            return model.identity
        }
    },

    /**
     * http://swagger.io/specification/#definitionsObject
     */
    getDefinitionReferenceFromPath(sails, path) {
        let model = Transformer.getModelFromPath(sails, path)
        if (model) {
            return Transformer.generateDefinitionReference(model.identity)
        }
    },

    generateDefinitionReference(modelIdentity) {
        return '#/definitions/' + modelIdentity
    },

    /**
     * http://swagger.io/specification/#pathItemObject
     */
    getPathItem(sails, pathGroup) {
        let methodGroups = _.chain(pathGroup)
            .indexBy('method')
            .pick([
                'get', 'post', 'put', 'head', 'options', 'patch', 'delete'
            ])
            .value()

        return _.mapValues(methodGroups, (methodGroup, method) => {
            return Transformer.getOperation(sails, methodGroup, method)
        })
    },

    /**
     * http://swagger.io/specification/#operationObject
     */
    getOperation(sails, methodGroup, method) {
        return {
            summary: methodMap[method],
            consumes: ['application/json'],
            produces: ['application/json'],
            parameters: Transformer.getParameters(sails, methodGroup),
            responses: Transformer.getResponses(sails, methodGroup),
            tags: Transformer.getPathTags(sails, methodGroup)
        }
    },

    /**
     * A list of tags for API documentation control. Tags can be used for logical
     * grouping of operations by resources or any other qualifier.
     */
    getPathTags(sails, methodGroup) {
        return _.unique(_.compact([
            Transformer.getPathModelTag(sails, methodGroup),
            Transformer.getPathControllerTag(sails, methodGroup),
            Transformer.getControllerFromRoute(sails, methodGroup)
        ]))
    },

    getPathModelTag(sails, methodGroup) {
        let model = Transformer.getModelFromPath(sails, methodGroup.path)
        return model && model.globalId
    },

    getPathControllerTag(sails, methodGroup) {
        // Fist check if we can find a controller tag using prefixed blueprint routes
        for (var prefix of getBlueprintPrefixes()) {
            if (methodGroup.path.indexOf(prefix) === 0) {
                let [$, pathToken] = methodGroup.path.replace(prefix, '').split('/')
                let tag = _.get(sails.controllers, [pathToken, 'globalId'])
                if (tag) return tag
            }
        }

        let [$, pathToken] = methodGroup.path.split('/')
        return _.get(sails.controllers, [pathToken, 'globalId'])
    },

    getControllerFromRoute(sails, methodGroup) {
        let route = sails.config.routes[`${methodGroup.method} ${methodGroup.path}`]
        if (!route) return

        let pattern = /(.+)Controller/
        let controller = route.controller || (_.isString(route) && route.split('.')[0])

        if (!controller) return

        let [$, name] = /(.+)Controller/.exec(controller)

        return name
    },

    /**
     * http://swagger.io/specification/#parameterObject
     */
    getParameters(sails, methodGroup) {
        let method = methodGroup.method
        let routeKeys = methodGroup.keys

        let canHavePayload = method === 'post' || method === 'put'

        if (!routeKeys.length && !canHavePayload) return []

        let parameters = _.map(routeKeys, param => {
            return {
                name: param.name,
                in : 'path',
                required: true,
                type: 'string'
            }
        })

        if (canHavePayload) {
            let path = methodGroup.path
            let modelIdentity = Transformer.getModelIdentityFromPath(sails, path)

            if (modelIdentity) {
                parameters.push({
                    name: modelIdentity,
                    in : 'body',
                    required: true,
                    schema: {
                        $ref: Transformer.getDefinitionReferenceFromPath(sails, path)
                    }
                })
            }
        }

        return parameters
    },

    /**
     * http://swagger.io/specification/#responsesObject
     */
    getResponses(sails, methodGroup) {
        let $ref = Transformer.getDefinitionReferenceFromPath(sails, methodGroup.path)
        let ok = {
            description: 'The requested resource'
        }
        if ($ref) {
            ok.schema = { '$ref': $ref }
        }
        return {
            '200': ok,
            '404': { description: 'Resource not found' },
            '500': { description: 'Internal server error' }
        }
    }
}

export default Transformer