"use strict";
/**
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.overrideHttpRules = exports.transcode = exports.isProto3OptionalField = exports.flattenObject = exports.match = exports.applyPattern = exports.encodeWithoutSlashes = exports.encodeWithSlashes = exports.buildQueryStringComponents = exports.deleteField = exports.deepCopyWithoutMatchedFields = exports.getField = void 0;
const util_1 = require("./util");
const httpOptionName = '(google.api.http)';
const proto3OptionalName = 'proto3_optional';
// List of methods as defined in google/api/http.proto (see HttpRule)
const supportedHttpMethods = ['get', 'post', 'put', 'patch', 'delete'];
function getField(request, field, allowObjects = false // in most cases, we need leaf fields
) {
    const parts = field.split('.');
    let value = request;
    for (const part of parts) {
        if (typeof value !== 'object') {
            return undefined;
        }
        value = value[part];
    }
    if (!allowObjects &&
        typeof value === 'object' &&
        !Array.isArray(value) &&
        value !== null) {
        return undefined;
    }
    return value;
}
exports.getField = getField;
function deepCopyWithoutMatchedFields(request, fieldsToSkip, fullNamePrefix = '') {
    if (typeof request !== 'object' || request === null) {
        return request;
    }
    const copy = Object.assign({}, request);
    for (const key in copy) {
        if (fieldsToSkip.has(`${fullNamePrefix}${key}`)) {
            delete copy[key];
            continue;
        }
        const nextFullNamePrefix = `${fullNamePrefix}${key}.`;
        if (Array.isArray(copy[key])) {
            // a field of an array cannot be addressed as "request.field", so we omit the skipping logic for array descendants
            copy[key] = copy[key].map(value => deepCopyWithoutMatchedFields(value, new Set()));
        }
        else if (typeof copy[key] === 'object' && copy[key] !== null) {
            copy[key] = deepCopyWithoutMatchedFields(copy[key], fieldsToSkip, nextFullNamePrefix);
        }
    }
    return copy;
}
exports.deepCopyWithoutMatchedFields = deepCopyWithoutMatchedFields;
function deleteField(request, field) {
    const parts = field.split('.');
    while (parts.length > 1) {
        if (typeof request !== 'object') {
            return;
        }
        const part = parts.shift();
        request = request[part];
    }
    const part = parts.shift();
    if (typeof request !== 'object') {
        return;
    }
    delete request[part];
}
exports.deleteField = deleteField;
function buildQueryStringComponents(request, prefix = '') {
    const resultList = [];
    for (const key in request) {
        if (Array.isArray(request[key])) {
            for (const value of request[key]) {
                resultList.push(`${prefix}${encodeWithoutSlashes(key)}=${encodeWithoutSlashes(value.toString())}`);
            }
        }
        else if (typeof request[key] === 'object' && request[key] !== null) {
            resultList.push(...buildQueryStringComponents(request[key], `${key}.`));
        }
        else {
            resultList.push(`${prefix}${encodeWithoutSlashes(key)}=${encodeWithoutSlashes(request[key] === null ? 'null' : request[key].toString())}`);
        }
    }
    return resultList;
}
exports.buildQueryStringComponents = buildQueryStringComponents;
function encodeWithSlashes(str) {
    return str
        .split('')
        .map(c => (c.match(/[-_.~0-9a-zA-Z]/) ? c : encodeURIComponent(c)))
        .join('');
}
exports.encodeWithSlashes = encodeWithSlashes;
function encodeWithoutSlashes(str) {
    return str
        .split('')
        .map(c => (c.match(/[-_.~0-9a-zA-Z/]/) ? c : encodeURIComponent(c)))
        .join('');
}
exports.encodeWithoutSlashes = encodeWithoutSlashes;
function escapeRegExp(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function applyPattern(pattern, fieldValue) {
    if (!pattern || pattern === '*') {
        return encodeWithSlashes(fieldValue);
    }
    if (!pattern.includes('*') && pattern !== fieldValue) {
        return undefined;
    }
    // since we're converting the pattern to a regex, make necessary precautions:
    const regex = new RegExp('^' +
        escapeRegExp(pattern)
            .replace(/\\\*\\\*/g, '(.+)')
            .replace(/\\\*/g, '([^/]+)') +
        '$');
    if (!fieldValue.match(regex)) {
        return undefined;
    }
    return encodeWithoutSlashes(fieldValue);
}
exports.applyPattern = applyPattern;
function fieldToCamelCase(field) {
    const parts = field.split('.');
    return parts.map(part => (0, util_1.toCamelCase)(part)).join('.');
}
function match(request, pattern) {
    let url = pattern;
    const matchedFields = [];
    for (;;) {
        const match = url.match(/^(.*)\{([^}=]+)(?:=([^}]*))?\}(.*)/);
        if (!match) {
            break;
        }
        const [, before, field, pattern, after] = match;
        const camelCasedField = fieldToCamelCase(field);
        matchedFields.push(fieldToCamelCase(camelCasedField));
        const fieldValue = getField(request, camelCasedField);
        if (fieldValue === undefined) {
            return undefined;
        }
        const appliedPattern = applyPattern(pattern, fieldValue === null ? 'null' : fieldValue.toString());
        if (appliedPattern === undefined) {
            return undefined;
        }
        url = before + appliedPattern + after;
    }
    return { matchedFields, url };
}
exports.match = match;
function flattenObject(request) {
    const result = {};
    for (const key in request) {
        if (request[key] === undefined) {
            continue;
        }
        if (Array.isArray(request[key])) {
            // According to the http.proto comments, a repeated field may only
            // contain primitive types, so no extra recursion here.
            result[key] = request[key];
            continue;
        }
        if (typeof request[key] === 'object' && request[key] !== null) {
            const nested = flattenObject(request[key]);
            for (const nestedKey in nested) {
                result[`${key}.${nestedKey}`] = nested[nestedKey];
            }
            continue;
        }
        result[key] = request[key];
    }
    return result;
}
exports.flattenObject = flattenObject;
function isProto3OptionalField(field) {
    return field && field.options && field.options[proto3OptionalName];
}
exports.isProto3OptionalField = isProto3OptionalField;
function transcode(request, parsedOptions) {
    const httpRules = [];
    for (const option of parsedOptions) {
        if (!(httpOptionName in option)) {
            continue;
        }
        const httpRule = option[httpOptionName];
        httpRules.push(httpRule);
        if (httpRule === null || httpRule === void 0 ? void 0 : httpRule.additional_bindings) {
            const additionalBindings = Array.isArray(httpRule.additional_bindings)
                ? httpRule.additional_bindings
                : [httpRule.additional_bindings];
            httpRules.push(...additionalBindings);
        }
    }
    for (const httpRule of httpRules) {
        for (const httpMethod of supportedHttpMethods) {
            if (!(httpMethod in httpRule)) {
                continue;
            }
            const pathTemplate = httpRule[httpMethod];
            const matchResult = match(request, pathTemplate);
            if (matchResult === undefined) {
                continue;
            }
            const { url, matchedFields } = matchResult;
            let data = deepCopyWithoutMatchedFields(request, new Set(matchedFields));
            if (httpRule.body === '*') {
                return { httpMethod, url, queryString: '', data };
            }
            // one field possibly goes to request data, others go to query string
            const queryStringObject = data;
            if (httpRule.body) {
                data = getField(queryStringObject, fieldToCamelCase(httpRule.body), 
                /*allowObjects:*/ true);
                deleteField(queryStringObject, fieldToCamelCase(httpRule.body));
            }
            else {
                data = '';
            }
            const queryStringComponents = buildQueryStringComponents(queryStringObject);
            const queryString = queryStringComponents.join('&');
            if (!data ||
                (typeof data === 'object' && Object.keys(data).length === 0)) {
                data = '';
            }
            return { httpMethod, url, queryString, data };
        }
    }
    return undefined;
}
exports.transcode = transcode;
// Override the protobuf json's the http rules.
function overrideHttpRules(httpRules, protoJson) {
    for (const rule of httpRules) {
        if (!rule.selector) {
            continue;
        }
        const rpc = protoJson.lookup(rule.selector);
        // Not support override on non-exist RPC or a RPC without an annotation.
        // We could reconsider if we have the use case later.
        if (!rpc || !rpc.parsedOptions) {
            continue;
        }
        for (const item of rpc.parsedOptions) {
            if (!(httpOptionName in item)) {
                continue;
            }
            const httpOptions = item[httpOptionName];
            for (const httpMethod in httpOptions) {
                if (httpMethod in rule) {
                    if (httpMethod === 'additional_bindings') {
                        continue;
                    }
                    httpOptions[httpMethod] =
                        rule[httpMethod];
                }
                if (rule.additional_bindings) {
                    httpOptions['additional_bindings'] = !httpOptions['additional_bindings']
                        ? []
                        : Array.isArray(httpOptions['additional_bindings'])
                            ? httpOptions['additional_bindings']
                            : [httpOptions['additional_bindings']];
                    // Make the additional_binding to be an array if it is not.
                    httpOptions['additional_bindings'].push(...rule.additional_bindings);
                }
            }
        }
    }
}
exports.overrideHttpRules = overrideHttpRules;
//# sourceMappingURL=transcoding.js.map