"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const assert = require("assert");
const pathToRegex = require("path-to-regexp");
const lodash_1 = require("lodash");
const error_1 = require("../error");
const joi_to_swagger_1 = require("joi-to-swagger");
function _replaceKeysDeep(obj, keysMap) {
    return (0, lodash_1.transform)(obj, (result, value, key) => {
        // transform to a new object
        const currentKey = keysMap[key] || key;
        // eslint-disable-next-line no-param-reassign
        result[currentKey] = (0, lodash_1.isObject)(value)
            ? _replaceKeysDeep(value, keysMap)
            : value;
    });
}
class SwaggerAPI {
    constructor() {
        this.apiRoutes = [];
    }
    /**
     * Add a `koa-joi-router` router to the API.
     */
    addJoiRouter({ router, routes }) {
        const prefix = router.opts.prefix;
        routes.forEach(route => {
            this.apiRoutes.push({
                route,
                prefix,
            });
        });
    }
    /**
     * Generate a Swagger 2.0 specification as an object for this API.
     *
     * @param {object} baseSpec - base document
     * @param {object} baseSpec.info
     * @param {string} baseSpec.info.title
     * @param {string} baseSpec.info.version
     * @param {object} baseSpec.tags
     * @param {string} baseSpec.tags.name
     * @param {string} baseSpec.tags.description
     * @param {object} [options]
     * @param renameKeys
     * @param {object} [options.warnFunc]
     * @param {object} [options.defaultResponses]
     * @param {object} [options.definitions]
     * @returns {object} swagger 2.0 specification
     */
    generateSpec(baseSpec, options, renameKeys) {
        options = Object.assign({
            warnFunc: console.warn,
            defaultResponses: {
                200: {
                    description: 'Success',
                },
                400: {
                    description: 'Bad Request',
                },
                500: {
                    description: 'Failure',
                },
            },
        }, options);
        assert(baseSpec.info, 'baseSpec.info 不能为空');
        assert(baseSpec.info.title, 'baseSpec.info.title 不能为空');
        assert(baseSpec.info.version, 'baseSpec.info.version 不能为空');
        if (!options.defaultResponses) {
            options.defaultResponses = {};
        }
        Object.freeze(options.defaultResponses);
        if (baseSpec.definitions) {
            for (const key in baseSpec.definitions) {
                const val = baseSpec.definitions[key];
                // if it's not a swagger object, convert it
                if (val && val.type !== 'object' && !val.properties) {
                    baseSpec.definitions[key] = (0, joi_to_swagger_1.default)(val).swagger;
                }
            }
        }
        const doc = (0, lodash_1.cloneDeep)(baseSpec);
        doc.swagger = '2.0';
        doc.paths = doc.paths || {};
        doc.tags = doc.tags || [];
        this.apiRoutes.forEach(apiRoute => {
            const routeOptions = {
                ...options,
                prefix: apiRoute.prefix,
            };
            const routePaths = routeToSwaggerPaths(apiRoute.route, routeOptions);
            mergeSwaggerPaths(doc.paths, routePaths, options);
        });
        return _replaceKeysDeep(doc, renameKeys || {});
    }
}
exports.default = SwaggerAPI;
const captureRegex = /(:\w+)/g;
function mergeSwaggerPaths(paths, newPaths, options) {
    const warn = options && options.warnFunc;
    (0, lodash_1.each)(newPaths, (newPathItemObj, path) => {
        const pathItemObj = (paths[path] = paths[path] || {});
        // Merge operations into path
        (0, lodash_1.each)(newPathItemObj, (operationObj, method) => {
            if (pathItemObj[method]) {
                // already exists!
                if (warn)
                    warn(`${path}[${method}] exists in multiple routes`);
                return;
            }
            pathItemObj[method] = operationObj;
        });
    });
    return paths;
}
/**
 * For a given joi-router route, return an array of Swagger paths.
 * @param {object} route
 * @param options
 * @returns {object} paths
 */
function routeToSwaggerPaths(route, options) {
    options = options || {};
    const paths = {};
    const routeDesc = {
        responses: {},
    };
    if (route.validate) {
        const type = route.validate.type;
        if (!type) {
            throw new error_1.ValidationError('type 不能为空');
        }
        else if (type === 'json') {
            routeDesc.consumes = ['application/json'];
        }
        else if (type === 'form') {
            routeDesc.consumes = ['application/x-www-form-urlencoded'];
        }
        else if (type === 'multipart') {
            routeDesc.consumes = ['multipart/form-data'];
        }
        routeDesc.parameters = validateToSwaggerParameters(route.validate);
        const output = route.output;
        if (output) {
            routeDesc.responses = Object.assign({}, options.defaultResponses);
            Object.keys(output).forEach(x => {
                x = x.toString();
                // 200,206,300
                const respCodes = x.split(',');
                respCodes.forEach(code => {
                    // 200-299
                    if (code.includes('-')) {
                        code = code.split('-')[0];
                    }
                    code = code.trim();
                    routeDesc.responses[code] = Object.assign({
                        description: 'Success',
                    }, options.defaultResponses[code], outputToSwagger(output[x]));
                });
            });
        }
    }
    if (route.meta && route.meta.swagger) {
        Object.assign(routeDesc, route.meta.swagger);
    }
    // This sets default 'path' parameters so swagger-ui doesn't complain.
    const noPathParamsExist = !(0, lodash_1.some)(routeDesc.parameters, ['in', 'path']);
    const noPathValidatorExists = (0, lodash_1.get)(route, 'validate.path') === undefined;
    const noParamsValidatorExists = (0, lodash_1.get)(route, 'validate.params') === undefined;
    if (noPathParamsExist && noPathValidatorExists && noParamsValidatorExists) {
        routeDesc.parameters = routeDesc.parameters || [];
        const pathCaptures = route.path.match(captureRegex);
        if (pathCaptures) {
            (0, lodash_1.each)(pathCaptures, pathParameter => {
                routeDesc.parameters.push({
                    name: pathParameter.replace(':', ''),
                    in: 'path',
                    type: 'string',
                    required: true,
                });
            });
        }
    }
    let path = parseSwaggerPath(route.path);
    if (options.prefix) {
        if (options.prefix.endsWith('/') || path.startsWith('/')) {
            path = `${options.prefix}${path}`;
        }
        else {
            path = `${options.prefix}/${path}`;
        }
    }
    const pathItemObj = (paths[path] = {});
    const methods = Array.isArray(route.method) ? route.method : [route.method];
    methods.forEach(method => {
        pathItemObj[method.toLowerCase()] = routeDesc;
    });
    return paths;
}
function addSchemaParameters(parameters, location, schema) {
    const swaggerObject = (0, joi_to_swagger_1.default)(schema).swagger;
    if (swaggerObject.type === 'object' && swaggerObject.properties) {
        (0, lodash_1.each)(swaggerObject.properties, (value, name) => {
            var _a;
            const parameter = value;
            parameter.name = name;
            parameter.in = location;
            parameter.required = (_a = swaggerObject.required) === null || _a === void 0 ? void 0 : _a.includes(name);
            parameters.push(parameter);
        });
    }
}
function outputToSwagger(respJson) {
    const obj = {};
    if (respJson && respJson.body) {
        const respBody = respJson.body;
        obj.schema =
            typeof respJson.ref === 'string'
                ? { $ref: respJson.ref }
                : respBody.swagger || (0, joi_to_swagger_1.default)(respBody).swagger;
        if (respJson.schema || obj.schema.description) {
            obj.description = respJson.schema || obj.schema.description;
        }
    }
    return obj;
}
/**
 * Convert joi-router validate object to swagger
 */
function validateToSwaggerParameters(validate) {
    const parameters = [];
    if (validate.header) {
        addSchemaParameters(parameters, 'header', validate.header);
    }
    if (validate.query) {
        addSchemaParameters(parameters, 'query', validate.query);
    }
    if (validate.params) {
        addSchemaParameters(parameters, 'path', validate.params);
    }
    if (validate.type === 'json') {
        if (validate.body) {
            const schema = validate.body;
            parameters.push({
                name: 'body',
                in: 'body',
                schema: (0, joi_to_swagger_1.default)(schema).swagger,
            });
        }
    }
    else {
        if (validate.body) {
            addSchemaParameters(parameters, 'formData', validate.body);
        }
        if (validate.files) {
            addSchemaParameters(parameters, 'formData', validate.files);
        }
    }
    return parameters;
}
/* Convert a joi-router path into a Swagger parameterized path,
 * e.g. /users/:userId becomes /users/{userId}
 *
 * FIXME: incomplete handling, escaping, etc
 * FIXME: throw error if a complex regex is used
 */
function parseSwaggerPath(path) {
    const pathTokens = pathToRegex.parse(path);
    const segments = pathTokens.map(token => {
        let segment;
        if (typeof token === 'object') {
            segment = `{${token.name}}`; //this means this is a complex regex group. for more info, read up on path-to-regexp, koa-joi-router uses it, it's great.
        }
        else {
            segment = token.replace('/', ''); //remove leading slash, to handle things like complex routes: /users/:userId/friends/:friendId
        }
        return segment;
    });
    return '/' + segments.join('/'); //path is normalized, just add that leading slash back to handle prefixes properly again.
}
//# sourceMappingURL=magic.swagger.js.map