"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 utils_1 = require("./utils");
const j2o = require('joi-to-json');
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 OpenApi {
    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,
            });
        });
    }
    /**
     * 生成OpenApi3.0定义
     *
     * @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} openapi 3.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);
        const doc = (0, lodash_1.cloneDeep)(baseSpec);
        doc.openapi = '3.0.3';
        doc.paths = doc.paths || {};
        doc.tags = doc.tags || [];
        doc.components = doc.components || {};
        doc.components.schemas = doc.components.schemas || {};
        this.apiRoutes.forEach(apiRoute => {
            const routeOptions = {
                ...options,
                prefix: apiRoute.prefix,
            };
            const routePaths = routeToOpenApiPaths(doc, apiRoute.route, routeOptions);
            mergeOpenApiPaths(doc.paths, routePaths, options);
        });
        return _replaceKeysDeep(doc, renameKeys || {});
    }
}
exports.default = OpenApi;
const captureRegex = /(:\w+)/g;
function mergeOpenApiPaths(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 OpenApi paths.
 * @param doc
 * @param {object} route
 * @param options
 * @returns {object} paths
 */
function routeToOpenApiPaths(doc, 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 不能为空');
        }
        const { operationId, parameters, requestBody, requestBodySchema } = validateToOpenApiParameters(route);
        routeDesc.operationId = operationId;
        routeDesc.parameters = parameters;
        routeDesc.requestBody = requestBody;
        doc.components.schemas[requestBodySchema.key] = requestBodySchema.value;
        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();
                    const { responseBody, responseBodySchema } = outputToOpenApi(route, output[x]);
                    doc.components.schemas[responseBodySchema.key] =
                        responseBodySchema.value;
                    routeDesc.responses[code] = Object.assign({
                        description: 'Success',
                    }, options.defaultResponses[code], responseBody);
                });
            });
        }
    }
    if (route.meta && route.meta.openapi) {
        Object.assign(routeDesc, route.meta.openapi);
    }
    // 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 = parseOpenApiPath(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 openApiObject = j2o(schema, 'open-api');
    if (openApiObject.type === 'object' && openApiObject.properties) {
        (0, lodash_1.each)(openApiObject.properties, (value, name) => {
            var _a;
            const parameter = {
                name,
                schema: {
                    type: value.type,
                    example: value.example,
                },
                description: value.description,
                in: location,
                required: (_a = openApiObject.required) === null || _a === void 0 ? void 0 : _a.includes(name),
            };
            parameters.push(parameter);
        });
    }
}
function outputToOpenApi({ path, method }, { body, headers, responseType }) {
    const operationId = method.toUpperCase() +
        path.replace(/\//g, '__').replace(/:/g, '').toUpperCase();
    const responseBodySchema = {
        key: `${operationId}__RESPONSE`,
        value: undefined,
    };
    let responseBody = undefined;
    if ((0, utils_1.isNotBlank)(responseType)) {
        responseBody = {
            content: {},
        };
        if (body) {
            responseBodySchema.value = j2o(body, 'open-api');
            const schema = {
                $ref: `#/components/schemas/${responseBodySchema.key}`,
            };
            if (responseType === 'json') {
                responseBody.content['application/json'] = {
                    schema,
                };
            }
            else if (responseType === 'xml') {
                responseBody.content['application/xml'] = {
                    schema,
                };
            }
        }
        else {
            if (responseType === 'image') {
                responseBody.content['image/png'] = {
                    schema: { type: 'string', format: 'binary' },
                };
            }
            else if (responseType === 'file') {
                responseBody.content['application/octet-stream'] = {
                    schema: { type: 'string', format: 'binary' },
                };
            }
        }
        if (headers) {
            const keys = Object.keys(headers);
            responseBody.headers = {};
            for (const key of keys) {
                const { type, description, example } = j2o(headers[key], 'open-api');
                responseBody.headers[key] = {
                    schema: {
                        type,
                        example,
                    },
                    description,
                };
            }
        }
    }
    return { responseBody, responseBodySchema };
}
/**
 * 解析地址栏和header参数
 */
function validateToOpenApiParameters({ validate, path, method }) {
    var _a;
    const operationId = method.toUpperCase() +
        path.replace(/\//g, '__').replace(/:/g, '').toUpperCase();
    const parameters = [];
    const requestBodySchema = {
        key: `${operationId}__REQUEST`,
        value: undefined,
    };
    let requestBody;
    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) {
            requestBodySchema.value = j2o(validate.body, 'open-api');
            requestBody = {
                content: {},
            };
            requestBody.content['application/json'] = {
                schema: {
                    $ref: `#/components/schemas/${requestBodySchema.key}`,
                },
            };
        }
    }
    else if (validate.type === 'form' || validate.type === 'multipart') {
        requestBodySchema.value = j2o(validate.body, 'open-api');
        if ((_a = requestBodySchema.value) === null || _a === void 0 ? void 0 : _a.properties) {
            const keys = Object.keys(requestBodySchema.value.properties);
            for (const key of keys) {
                if (requestBodySchema.value.properties[key].format === 'binary') {
                    requestBodySchema.value.properties[key].type = 'string';
                }
            }
        }
        requestBody = {
            content: {},
        };
        requestBody.content[validate.type === 'multipart'
            ? 'multipart/form-data'
            : 'application/x-www-form-urlencoded'] = {
            schema: {
                $ref: `#/components/schemas/${requestBodySchema.key}`,
            },
        };
    }
    return {
        parameters,
        requestBody,
        operationId,
        requestBodySchema,
    };
}
/* Convert a joi-router path into a OpenApi parameterized path,
 * e.g. /users/:userId becomes /users/{userId}
 *
 * FIXME: incomplete handling, escaping, etc
 * FIXME: throw error if a complex regex is used
 */
function parseOpenApiPath(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.
}
