Source: server-vars-to-x-ms-parameterized-host.js

/**
 * @copyright Copyright 2019-2020 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 * @module "openapi-transformers/server-vars-to-x-ms-parameterized-host.js"
 */

import OpenApiTransformerBase from 'openapi-transformer-base';
import visit from 'openapi-transformer-base/visit.js';

/** Gets the scheme and authority portions of a URL template.
 *
 * @private
 */
function getSchemeAuthority(url) {
  // Note: parameterized host may contain host and/or schema parameters.
  // A single parameter may contain both schema and host.
  // No way to disambiguate url like {schema}{host}{pathPrefix}.
  // Assume the scheme+authority parts end at first slash following //
  // (if present).  (Note: protocol-relative url may start with //)
  const match =
    /^(?:(?:\{[^{}]*\}|[^/{}])*\/\/)?(?:\{[^{}]*\}|[^/{}])+/.exec(url);
  return match && match[0];
}

function parseServerUrl(url) {
  if (typeof url !== 'string') {
    this.warn('Ignoring Server with non-string url', url);
    return undefined;
  }

  const schemeAuth = getSchemeAuthority(url);
  if (!schemeAuth) {
    this.warn('Unable to determine scheme and authority for url', url);
    return undefined;
  }

  // Remove non-templated scheme, if present
  const hostTemplate = schemeAuth.replace(/^[a-zA-Z_+.-]+:\/\//, '');
  if (!hostTemplate) {
    this.warn('Unable to determine host for url', url);
    return undefined;
  }

  const hostTemplateExprs = hostTemplate.match(/\{[^{}]*\}/g);
  if (!hostTemplateExprs) {
    this.warn('No template expressions in scheme and authority for url', url);
    return undefined;
  }

  return {
    hostTemplate,
    // If non-templated scheme was removed, use the declared schemes
    useSchemePrefix: hostTemplate !== schemeAuth,
    hostTemplateVarNames: hostTemplateExprs
      .map((templateExpr) => templateExpr.slice(1, -1)),
  };
}

/**
 * Transformer to convert Server Variables in host portion to
 * x-ms-parameterized-host for use with OpenAPI 2.
 *
 * https://github.com/Azure/autorest/tree/main/docs/extensions#x-ms-parameterized-host
 *
 * TODO: Roll this into OpenAPI 3 -> 2 conversion?
 *
 * Note: Currently expects OAS3 input (required for Server Objects) but
 * produces OAS2 parameters as specified for x-ms-parameterized-host.
 */
export default class ServerVarsToParamHostTransformer
  extends OpenApiTransformerBase {
  constructor({ omitDefault, parameter, xMsParameterizedHost } = {}) {
    super();

    if (omitDefault !== undefined && !Array.isArray(omitDefault)) {
      throw new TypeError('options.omitDefault must be an Array');
    }

    if (parameter !== undefined
      && (parameter === null
        || typeof parameter !== 'object')) {
      throw new TypeError('options.parameter must be a object');
    }

    if (xMsParameterizedHost !== undefined
      && (xMsParameterizedHost === null
        || typeof xMsParameterizedHost !== 'object')) {
      throw new TypeError('options.xMsParameterizedHost must be a object');
    }

    this.options = {
      omitDefault: omitDefault || [],
      parameter,
      xMsParameterizedHost,
    };
  }

  transformServerVariableToParameter(serverVariable) {
    if (serverVariable === null
      || typeof serverVariable !== 'object'
      || Array.isArray(serverVariable)) {
      this.warn('Ignoring non-object Server Variable', serverVariable);
      return undefined;
    }

    return {
      name: undefined,
      type: 'string',
      ...serverVariable,
      in: 'path',
      required: true,
      ...this.options.parameter,
    };
  }

  transformServerVariablesToParameters(variables, varNames) {
    if (variables === null
      || typeof variables !== 'object'
      || Array.isArray(variables)) {
      this.warn('Ignoring non-object Map', variables);
      return undefined;
    }

    const parameters = [];
    for (const varName of varNames) {
      const variable =
        Object.hasOwn(variables, varName) ? variables[varName]
          : undefined;
      if (variable === undefined) {
        this.warn('Unable to convert Server missing variable %j', varName);
        return undefined;
      }

      const parameter = visit(
        this,
        this.transformServerVariableToParameter,
        varName,
        variable,
      );
      if (!parameter) {
        return undefined;
      }

      parameter.name = varName;

      if (this.options.omitDefault.includes(varName)
        && Object.hasOwn(parameter, 'default')) {
        delete parameter.default;
      }

      parameters.push(parameter);
    }

    return parameters;
  }

  transformServerToParameterizedHost(server) {
    if (server === null
      || typeof server !== 'object'
      || Array.isArray(server)) {
      this.warn('Ignoring non-object Server', server);
      return undefined;
    }

    const { url, variables = {} } = server;

    const urlParts = visit(this, parseServerUrl, 'url', url);
    if (!urlParts) {
      return undefined;
    }

    const {
      hostTemplate,
      useSchemePrefix,
      hostTemplateVarNames,
    } = urlParts;

    const parameters = visit(
      this,
      this.transformServerVariablesToParameters,
      'variables',
      variables,
      hostTemplateVarNames,
    );
    if (!parameters) {
      return undefined;
    }

    return {
      hostTemplate,
      useSchemePrefix,
      parameters,
      ...this.options.xMsParameterizedHost,
    };
  }

  transformServersToParameterizedHost(servers) {
    if (!Array.isArray(servers)) {
      this.warn('Ignoring non-Array servers', servers);
      return undefined;
    }

    if (servers.length === 0) {
      this.warn('Ignoring empty servers', servers);
      return undefined;
    }

    if (servers.length > 1) {
      this.warn('Ignoring all but first server', servers);
    }

    return visit(
      this,
      this.transformServerToParameterizedHost,
      '0',
      servers[0],
    );
  }

  transformOpenApi(openApi) {
    if (openApi === null
      || typeof openApi !== 'object'
      || Array.isArray(openApi)) {
      this.warn('Ignoring non-object OpenAPI', openApi);
      return openApi;
    }

    if (openApi.servers === undefined) {
      return openApi;
    }

    const parameterizedHost = visit(
      this,
      this.transformServersToParameterizedHost,
      'servers',
      openApi.servers,
    );
    if (parameterizedHost === undefined) {
      return openApi;
    }

    return {
      ...openApi,
      'x-ms-parameterized-host': parameterizedHost,
    };
  }
}