Source: index.js

/**
 * @copyright Copyright 2019-2020 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 * @module openapi-transformer-base
 */

'use strict';

const { METHODS } = require('http');
const { debuglog } = require('util');

const toJsonPointer = require('./lib/to-json-pointer.js');
const visit = require('./visit.js');

const { isArray } = Array;

const debug = debuglog('openapi-transformer-base');

/** HTTP method names which are properties of a Path Item Object that have
 * Operation Object values.
 *
 * @private
 */
const httpMethodSet = new Set(METHODS.map((method) => method.toLowerCase()));

/** Transforms a value which has type object<string,ValueType> but is not
 * defined as Map[string,ValueType] in OpenAPI.
 *
 * Note: This is currently used for schema properties, where #transformMap()
 * often complicates transformations due to differences with Map[string,Schema]
 * on definitions/components.schema and complicates optimizations.
 *
 * @private
 * @template ValueType, TransformedType
 * @this {!OpenApiTransformerBase}
 * @param {!object<string,ValueType>|*} obj Map-like object to transform.
 * @param {function(this:!OpenApiTransformerBase, ValueType): TransformedType
 * } transform Method which transforms values in obj.
 * @param {string} logName Name of object being transformed (for logging).
 * @param {boolean=} skipExtensions If true, do not call transform on {@link
 * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#specificationExtensions
 * Specification Extensions} (i.e.  properties starting with "x-").
 * Such properties are copied to the returned object without transformation.
 * @returns {!object<string,TransformedType>|*} If obj is a Map, a plain object
 * with the same own enumerable string-keyed properties as obj with values
 * returned by transform.  Otherwise, obj is returned unchanged.
 */
function transformMapLike(obj, transform, logName, skipExtensions) {
  if (typeof obj !== 'object' || obj === null) {
    this.warn(`Ignoring non-object ${logName}`, obj);
    return obj;
  }

  if (isArray(obj)) {
    // Note: This function is only called for values specified as Map[X,Y]
    // in the OpenAPI Specification.  Array values are invalid and it would
    // be unsafe to assume that their contents are type Y.  Return unchanged.
    this.warn(`Ignoring non-object ${logName}`, obj);
    return obj;
  }

  const newObj = { ...obj };
  for (const [propName, propValue] of Object.entries(obj)) {
    if (propValue !== undefined
      && (!skipExtensions || !propName.startsWith('x-'))) {
      newObj[propName] = visit(this, transform, propName, propValue);
    }
  }

  return newObj;
}

/** Base class for traversing or transforming OpenAPI 2.x or 3.x documents
 * using a modified visitor design pattern to traverse object types within
 * the OpenAPI document tree.
 *
 * This class uses the following conventions:
 *
 * <ul>
 * <li>Objects passed as arguments are never modified by this class.
 *   Subclasses are encouraged to maintain this invariant by returning modified
 *   copies rather than modifying the argument objects.</li>
 * <li>JSON References, if present, are passed to the transform method for
 *   the type required by their position.  (e.g. a schema $ref is passed to
 *   {@link transformSchema}).</li>
 * <li>Properties which are not defined in the OpenAPI specification are
 *   preserved in returned objects unchanged. (e.g. <code>x-</code> extension
 *   properties)</li>
 * <li>The order that properties are visited is not defined and may change
 *   in future versions.</li>
 * <li>The behavior of this class is not conditional on the declared OpenAPI
 *   version in the document.  It will traverse properties which are present,
 *   regardless of whether they are specified in the declared version.</li>
 * <li>Callers may begin traversal at any point in the document (e.g. by
 *   calling {@link transformSchema} directly, instead of transitively through
 *   {@link transformOpenApi}).</li>
 * <li>No validation is performed on the OpenAPI document and every attempt is
 *   made to handle invalid values gracefully.  Child classes should be
 *   prepared for arguments with any type or value if the document may not be
 *   valid.</li>
 * <li>Minimal effort is made to handle or preserve values which are not
 *   representable in JSON (e.g. Date, RegExp, non-Object.prototype, Symbol
 *   and non-enumerable properties, etc.), which are usually treated as generic
 *   objects.</li>
 * <li>One exception to the previous point: transform methods are not called on
 *   <code>undefined</code> values, which are treated like missing properties
 *   except that they are copied to the transformed object to preserve the
 *   object shape.</li>
 * <li>Returned values are added to the transformed object, regardless of value.
 *   Therefore, unless overridden, returned objects will have the same
 *   properties as the original object, some of which may be undefined.</li>
 * </ul>
 */
class OpenApiTransformerBase {
  constructor() {
    /** Property names traversed in current transformation.
     *
     * @type {!Array<string>}
     */
    Object.defineProperty(this, 'transformPath', { value: [] });
  }

  /** Transforms an <code>Array[ValueType]</code> using a given transform
   * method.
   *
   * @template ValueType, TransformedType
   * @param {!Array<ValueType>|*} arr Array to transform.
   * @param {function(this:!OpenApiTransformerBase, ValueType): TransformedType
   * } transform Method which transforms values in arr.
   * @returns {!Array<TransformedType>|*} If arr is an Array, the result of
   * Array#map(transform).  Otherwise, obj is returned unchanged.
   */
  transformArray(arr, transform) {
    if (!isArray(arr)) {
      this.warn('Ignoring non-Array', arr);
      return arr;
    }

    // eslint-disable-next-line unicorn/no-array-method-this-argument
    return arr.map(transform, this);
  }

  /** Transforms a <code>Map[string, ValueType]</code> using a given transform
   * method.
   *
   * Similar to modify-values and _.mapValues from lodash.
   *
   * Unlike the above:
   * <ul>
   * <li>If the first argument is not an object, it is returned unchanged.</li>
   * <li>If the first argument is an Array, it is returned unchanged.</li>
   * <li>If the first argument is a non-null object, the returned object will
   * be a new object with prototype Object.prototype and properties matching
   * the first argument with transformed values.</li>
   * </ul>
   *
   * @template ValueType, TransformedType
   * @param {!object<string,ValueType>|*} obj Map to transform.
   * @param {function(this:!OpenApiTransformerBase, ValueType): TransformedType
   * } transform Method which transforms values in obj.
   * @returns {!object<string,TransformedType>|*} If obj is a Map, a plain
   * object with the same own enumerable string-keyed properties as obj with
   * values returned by transform.  Otherwise, obj is returned unchanged.
   */
  transformMap(obj, transform) {
    return transformMapLike.call(this, obj, transform, 'Map');
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminatorObject
   * Discriminator Object}.
   *
   * @param {!object} discriminator Discriminator Object.
   * @returns {!object} Transformed Discriminator Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformDiscriminator(discriminator) {
    return discriminator;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#exampleObject
   * OpenAPI 2.0 Example Object}.
   *
   * @param {!object} example OpenAPI 2.0 Example Object.
   * @returns {!object} Transformed Example Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformExample(example) {
    return example;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject
   * OpenAPI 3.x Example Object}.
   *
   * @param {!object} example OpenAPI 3.x Example Object.
   * @returns {!object} Transformed Example Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformExample3(example) {
    return example;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#externalDocumentationObject
   * External Documentation Object}.
   *
   * @param {!object} externalDocs External Documentation Object.
   * @returns {!object} Transformed External Documentation Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformExternalDocs(externalDocs) {
    return externalDocs;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#xmlObject
   * XML Object}.
   *
   * @param {!object} xml XML Object.
   * @returns {!object} Transformed XML Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformXml(xml) {
    return xml;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
   * Schema Object}.
   *
   * Note: For OpenAPI 2.0 documents, consider overriding
   * {@see transformParameter}, {@see transformItems}, and
   * {@see transformHeader} to transform all schema-like objects.
   *
   * @param {!object} schema Schema Object.
   * @returns {!object} Transformed Schema Object.
   */
  transformSchema(schema) {
    if (typeof schema !== 'object' || schema === null || isArray(schema)) {
      // Note: JSON Schema Core draft-handrews-json-schema-02 (referenced by
      // current/ OpenAPI 3.1 drafts) defines true and false as valid schemas
      // with true equivalent to {} and false equivalent to {not:{}}
      // https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.4.3.2
      if (typeof schema !== 'boolean') {
        this.warn('Ignoring non-object Schema', schema);
      }

      return schema;
    }

    const newSchema = { ...schema };
    const {
      contains,
      dependentSchemas,
      discriminator,
      externalDocs,
      items,
      patternProperties,
      properties,
      propertyNames,
      unevaluatedItems,
      unevaluatedProperties,
      xml,
    } = schema;

    if (discriminator !== undefined) {
      newSchema.discriminator = visit(
        this,
        this.transformDiscriminator,
        'discriminator',
        discriminator,
      );
    }

    if (externalDocs !== undefined) {
      newSchema.externalDocs = visit(
        this,
        this.transformExternalDocs,
        'externalDocs',
        externalDocs,
      );
    }

    if (xml !== undefined) {
      newSchema.xml = visit(this, this.transformXml, 'xml', xml);
    }

    if (items !== undefined) {
      // Note: OpenAPI 3.0 disallows Arrays, 2.0 and 3.1 drafts allow it
      if (isArray(items)) {
        newSchema.items = this.transformArray(items, this.transformSchema);
      } else {
        newSchema.items = visit(this, this.transformSchema, 'items', items);
      }
    }

    for (const schemaProp of ['if', 'then', 'else', 'not']) {
      const subSchema = schema[schemaProp];
      if (subSchema !== undefined) {
        newSchema[schemaProp] = visit(
          this,
          this.transformSchema,
          'subSchema',
          subSchema,
        );
      }
    }

    if (properties !== undefined) {
      newSchema.properties = visit(
        this,
        this.transformSchemaProperties,
        'properties',
        properties,
      );
    }

    if (patternProperties !== undefined) {
      newSchema.patternProperties = visit(
        this,
        transformMapLike,
        'patternProperties',
        patternProperties,
        this.transformSchema,
        'Schema',
      );
    }

    if (unevaluatedProperties !== undefined) {
      newSchema.unevaluatedProperties = visit(
        this,
        this.transformSchema,
        'unevaluatedProperties',
        unevaluatedProperties,
      );
    }

    if (propertyNames !== undefined) {
      newSchema.propertyNames = visit(
        this,
        this.transformSchema,
        'propertyNames',
        propertyNames,
      );
    }

    // Note: JSON Schema Core draft-handrews-json-schema-02 (referenced by
    // current/ OpenAPI 3.1 drafts) defines true and false as valid schemas
    // with true equivalent to {} and false equivalent to {not:{}}
    // https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.4.3.2
    // so they are now passed to transformSchema.
    for (const schemaProp of ['additionalItems', 'additionalProperties']) {
      const additionalItemsProps = schema[schemaProp];
      if (additionalItemsProps !== undefined) {
        newSchema[schemaProp] = visit(
          this,
          this.transformSchema,
          schemaProp,
          additionalItemsProps,
        );
      }
    }

    if (unevaluatedItems !== undefined) {
      newSchema.unevaluatedItems =
        visit(this, this.transformSchema, 'unevaluatedItems', unevaluatedItems);
    }

    if (dependentSchemas !== undefined) {
      newSchema.dependentSchemas = visit(
        this,
        transformMapLike,
        'dependentSchemas',
        dependentSchemas,
        this.transformSchema,
        'Schema',
      );
    }

    if (contains !== undefined) {
      newSchema.contains = visit(
        this,
        this.transformSchema,
        'contains',
        contains,
      );
    }

    for (const schemaProp of ['allOf', 'anyOf', 'oneOf']) {
      const subSchemas = schema[schemaProp];
      if (subSchemas !== undefined) {
        newSchema[schemaProp] = visit(
          this,
          this.transformArray,
          schemaProp,
          subSchemas,
          this.transformSchema,
        );
      }
    }

    // Note: The examples property is an Array of values, as defined by JSON
    // Schema, and is therefore not suitable for any transformExample* method.
    // See https://github.com/OAI/OpenAPI-Specification/issues/2094

    return newSchema;
  }

  /** Transforms {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject
   * Schema Object} properties.
   *
   * @param {!object} properties Schema Object properties.
   * @returns {!object} Transformed Schema Object properties.
   */
  transformSchemaProperties(properties) {
    return transformMapLike.call(
      this,
      properties,
      this.transformSchema,
      'Schema properties',
    );
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#itemsObject
   * Items Object}.
   *
   * Note: Items Object is a subset of Schema Object with the addition of
   * collectionFormat.
   *
   * @param {!object} items Items Object.
   * @returns {!object} Transformed Items Object.
   */
  transformItems(items) {
    if (typeof items !== 'object' || items === null || isArray(items)) {
      this.warn('Ignoring non-object Items', items);
      return items;
    }

    if (items.items === undefined) {
      return items;
    }

    return {
      ...items,
      items: visit(this, this.transformItems, 'items', items.items),
    };
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject
   * Header Object}.
   *
   * @param {!object} header Header Object.
   * @returns {!object} Transformed Header Object.
   */
  transformHeader(header) {
    if (typeof header !== 'object'
      || header === null
      || isArray(header)) {
      this.warn('Ignoring non-object Header', header);
      return header;
    }

    const newHeader = { ...header };

    if (header.items !== undefined) {
      newHeader.items = visit(this, this.transformItems, 'items', header.items);
    }

    if (header.schema !== undefined) {
      newHeader.schema = visit(
        this,
        this.transformSchema,
        'schema',
        header.schema,
      );
    }

    return newHeader;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#encodingObject
   * Encoding Object}.
   *
   * @param {!object} encoding Encoding Object.
   * @returns {!object} Transformed Encoding Object.
   */
  transformEncoding(encoding) {
    if (typeof encoding !== 'object'
      || encoding === null
      || isArray(encoding)) {
      this.warn('Ignoring non-object Encoding', encoding);
      return encoding;
    }

    if (encoding.headers === undefined) {
      return encoding;
    }

    return {
      ...encoding,
      headers: visit(
        this,
        this.transformMap,
        'headers',
        encoding.headers,
        this.transformHeader,
      ),
    };
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#linkObject
   * Link Object}.
   *
   * @param {!object} link Link Object.
   * @returns {!object} Transformed Link Object.
   */
  transformLink(link) {
    if (typeof link !== 'object'
      || link === null
      || isArray(link)) {
      this.warn('Ignoring non-object Link', link);
      return link;
    }

    if (link.server === undefined) {
      return link;
    }

    return {
      ...link,
      server: visit(this, this.transformServer, 'server', link.server),
    };
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#mediaTypeObject
   * Media Type Object}.
   *
   * @param {!object} mediaType Media Type Object.
   * @returns {!object} Transformed Media Type Object.
   */
  transformMediaType(mediaType) {
    if (typeof mediaType !== 'object'
      || mediaType === null
      || isArray(mediaType)) {
      this.warn('Ignoring non-object Media Type', mediaType);
      return mediaType;
    }

    const newMediaType = { ...mediaType };

    if (mediaType.schema !== undefined) {
      newMediaType.schema = visit(
        this,
        this.transformSchema,
        'schema',
        mediaType.schema,
      );
    }

    if (mediaType.examples !== undefined) {
      newMediaType.examples = visit(
        this,
        this.transformMap,
        'examples',
        mediaType.examples,
        this.transformExample3,
      );
    }

    if (mediaType.encoding !== undefined) {
      newMediaType.encoding = visit(
        this,
        this.transformMap,
        'encoding',
        mediaType.encoding,
        this.transformEncoding,
      );
    }

    return newMediaType;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
   * Response Object}.
   *
   * @param {!object} response Response Object.
   * @returns {!object} Transformed Response Object.
   */
  transformResponse(response) {
    if (typeof response !== 'object'
      || response === null
      || isArray(response)) {
      this.warn('Ignoring non-object Response', response);
      return response;
    }

    const newResponse = { ...response };

    if (response.headers !== undefined) {
      newResponse.headers = visit(
        this,
        this.transformMap,
        'headers',
        response.headers,
        this.transformHeader,
      );
    }

    if (response.content !== undefined) {
      newResponse.content = visit(
        this,
        this.transformMap,
        'content',
        response.content,
        this.transformMediaType,
      );
    }

    if (response.links !== undefined) {
      newResponse.links = visit(
        this,
        this.transformMap,
        'links',
        response.links,
        this.transformLink,
      );
    }

    if (response.schema !== undefined) {
      newResponse.schema = visit(
        this,
        this.transformSchema,
        'schema',
        response.schema,
      );
    }

    if (response.examples !== undefined) {
      newResponse.examples = visit(
        this,
        this.transformExample,
        'examples',
        response.examples,
      );
    }

    return newResponse;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject
   * Parameter Object}.
   *
   * Note: In OpenAPI 2.0, Parameter Object shares many properties with
   * Schema Object (when <code>.in !== 'body'</code>).
   *
   * @param {!object} parameter Parameter Object.
   * @returns {!object} Transformed Parameter Object.
   */
  transformParameter(parameter) {
    if (typeof parameter !== 'object'
      || parameter === null
      || isArray(parameter)) {
      this.warn('Ignoring non-object Parameter', parameter);
      return parameter;
    }

    const newParameter = { ...parameter };

    if (parameter.content !== undefined) {
      newParameter.content = visit(
        this,
        this.transformMap,
        'content',
        parameter.content,
        this.transformMediaType,
      );
    }

    if (parameter.schema !== undefined) {
      newParameter.schema = visit(
        this,
        this.transformSchema,
        'schema',
        parameter.schema,
      );
    }

    if (parameter.items !== undefined) {
      newParameter.items = visit(
        this,
        this.transformItems,
        'items',
        parameter.items,
      );
    }

    if (parameter.examples !== undefined) {
      newParameter.examples = visit(
        this,
        this.transformMap,
        'examples',
        parameter.examples,
        this.transformExample3,
      );
    }

    return newParameter;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responsesObject
   * Responses Object}.
   *
   * @param {!object} responses Responses Object.
   * @returns {!object} Transformed Response Object.
   */
  transformResponses(responses) {
    if (!responses || typeof responses !== 'object' || isArray(responses)) {
      this.warn('Ignoring non-object Responses', responses);
      return responses;
    }

    const newResponses = { ...responses };
    for (const prop of Object.keys(responses)) {
      // Only "default", HTTP response codes, and HTTP response code patterns
      // are defined to contain Response Object.  Other properties may be
      // extensions or defined as something else in future OpenAPI versions.
      //
      // Match using pattern similar to one mentioned in
      // https://github.com/OAI/OpenAPI-Specification/issues/2471#issuecomment-781362295
      // Be lenient about upper/lowercase, and single x in last 2 positions.
      // Although lowercase and single x are not valid, the risk of being
      // anything other than a response object is low enough to justify.
      if (prop === 'default' || /^[1-5][0-9Xx][0-9Xx]$/.test(prop)) {
        const response = responses[prop];
        if (response !== undefined) {
          newResponses[prop] = visit(
            this,
            this.transformResponse,
            prop,
            response,
          );
        }
      } else if (prop !== '$ref' && !prop.startsWith('x-')) {
        this.warn('Ignoring unrecognized property of Responses', prop);
      }
    }

    return newResponses;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject
   * Callback Object}.
   *
   * @param {!object} callback Callback Object.
   * @returns {!object} Transformed Callback Object.
   */
  transformCallback(callback) {
    return transformMapLike.call(
      this,
      callback,
      this.transformPathItem,
      'Callback',
      true,
    );
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject
   * Request Body Object}.
   *
   * @param {!object} requestBody Request Body Object.
   * @returns {!object} Transformed Request Body Object.
   */
  transformRequestBody(requestBody) {
    if (typeof requestBody !== 'object'
      || requestBody === null
      || isArray(requestBody)) {
      this.warn('Ignoring non-object Request Body', requestBody);
      return requestBody;
    }

    if (requestBody.content === undefined) {
      return requestBody;
    }

    return {
      ...requestBody,
      content: visit(
        this,
        this.transformMap,
        'content',
        requestBody.content,
        this.transformMediaType,
      ),
    };
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
   * Operation Object}.
   *
   * @param {!object} operation Operation Object.
   * @returns {!object} Transformed Operation Object.
   */
  transformOperation(operation) {
    if (typeof operation !== 'object'
      || operation === null
      || isArray(operation)) {
      this.warn('Ignoring non-object Operation', operation);
      return operation;
    }

    const newOperation = { ...operation };

    if (operation.externalDocs !== undefined) {
      newOperation.externalDocs = visit(
        this,
        this.transformExternalDocs,
        'externalDocs',
        operation.externalDocs,
      );
    }

    if (operation.parameters !== undefined) {
      newOperation.parameters =
        this.transformArray(operation.parameters, this.transformParameter);
    }

    if (operation.requestBody !== undefined) {
      newOperation.requestBody = visit(
        this,
        this.transformRequestBody,
        'requestBody',
        operation.requestBody,
      );
    }

    if (operation.responses !== undefined) {
      newOperation.responses = visit(
        this,
        this.transformResponses,
        'responses',
        operation.responses,
      );
    }

    if (operation.callbacks !== undefined) {
      newOperation.callbacks = visit(
        this,
        this.transformMap,
        'callbacks',
        operation.callbacks,
        this.transformCallback,
      );
    }

    if (operation.security !== undefined) {
      newOperation.security = this.transformArray(
        operation.security,
        this.transformSecurityRequirement,
      );
    }

    if (operation.servers !== undefined) {
      newOperation.servers =
        this.transformArray(operation.servers, this.transformServer);
    }

    return newOperation;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject
   * Path Item Object}.
   *
   * @param {!object} pathItem Path Item Object.
   * @returns {!object} Transformed Path Item Object.
   */
  transformPathItem(pathItem) {
    if (typeof pathItem !== 'object'
      || pathItem === null
      || isArray(pathItem)) {
      this.warn('Ignoring non-object Path Item', pathItem);
      return pathItem;
    }

    const newPathItem = { ...pathItem };

    if (pathItem.servers !== undefined) {
      newPathItem.servers =
        this.transformArray(pathItem.servers, this.transformServer);
    }

    if (pathItem.parameters !== undefined) {
      newPathItem.parameters =
        this.transformArray(pathItem.parameters, this.transformParameter);
    }

    for (const [method, operation] of Object.entries(pathItem)) {
      if (operation !== undefined && httpMethodSet.has(method.toLowerCase())) {
        newPathItem[method] = visit(
          this,
          this.transformOperation,
          method,
          operation,
        );
      } else if (method !== '$ref'
        && method !== 'description'
        && method !== 'parameters'
        && method !== 'servers'
        && method !== 'summary'
        && !method.startsWith('x-')) {
        this.warn('Ignoring unrecognized property of Path Item', method);
      }
    }

    return newPathItem;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathsObject
   * Paths Object}.
   *
   * Note: Paths Object may be traversed from the x-ms-paths property in
   * addition to the paths property of the OpenAPI Object.
   *
   * @param {!object} paths Paths Object.
   * @returns {!object} Transformed Paths Object.
   */
  transformPaths(paths) {
    return transformMapLike.call(
      this,
      paths,
      this.transformPathItem,
      'Paths',
      true,
    );
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#componentsObject
   * Components Object}.
   *
   * @param {!object} components Components Object.
   * @returns {!object} Transformed Components Object.
   */
  transformComponents(components) {
    if (typeof components !== 'object'
      || components === null
      || isArray(components)) {
      this.warn('Ignoring non-object Components', components);
      return components;
    }

    const newComponents = { ...components };

    if (components.schemas !== undefined) {
      newComponents.schemas = visit(
        this,
        this.transformMap,
        'schemas',
        components.schemas,
        this.transformSchema,
      );
    }

    if (components.responses !== undefined) {
      newComponents.responses = visit(
        this,
        this.transformMap,
        'responses',
        components.responses,
        this.transformResponse,
      );
    }

    if (components.parameters !== undefined) {
      newComponents.parameters = visit(
        this,
        this.transformMap,
        'parameters',
        components.parameters,
        this.transformParameter,
      );
    }

    if (components.examples !== undefined) {
      newComponents.examples = visit(
        this,
        this.transformMap,
        'examples',
        components.examples,
        this.transformExample3,
      );
    }

    if (components.requestBodies !== undefined) {
      newComponents.requestBodies = visit(
        this,
        this.transformMap,
        'requestBodies',
        components.requestBodies,
        this.transformRequestBody,
      );
    }

    if (components.headers !== undefined) {
      newComponents.headers = visit(
        this,
        this.transformMap,
        'headers',
        components.headers,
        this.transformHeader,
      );
    }

    if (components.securitySchemes !== undefined) {
      newComponents.securitySchemes = visit(
        this,
        this.transformMap,
        'securitySchemes',
        components.securitySchemes,
        this.transformSecurityScheme,
      );
    }

    if (components.links !== undefined) {
      newComponents.links = visit(
        this,
        this.transformMap,
        'links',
        components.links,
        this.transformLink,
      );
    }

    if (components.callbacks !== undefined) {
      newComponents.callbacks = visit(
        this,
        this.transformMap,
        'callbacks',
        components.callbacks,
        this.transformCallback,
      );
    }

    if (components.pathItems !== undefined) {
      newComponents.pathItems = visit(
        this,
        this.transformMap,
        'pathItems',
        components.pathItems,
        this.transformPathItem,
      );
    }

    return newComponents;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverVariableObject
   * Server Variable Object}.
   *
   * @param {!object} serverVariable Server Variable Object.
   * @returns {!object} Transformed Server Variable Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformServerVariable(serverVariable) {
    return serverVariable;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject
   * Server Object}.
   *
   * @param {!object} server Server Object.
   * @returns {!object} Transformed Server Object.
   */
  transformServer(server) {
    if (typeof server !== 'object' || server === null || isArray(server)) {
      this.warn('Ignoring non-object Server', server);
      return server;
    }

    if (server.variables === undefined) {
      return server;
    }

    return {
      ...server,
      variables: visit(
        this,
        this.transformMap,
        'variables',
        server.variables,
        this.transformServerVariable,
      ),
    };
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowObject
   * OAuth Flow Object}.
   *
   * @param {!object} flow OAuth Flow Object.
   * @returns {!object} Transformed OAuth Flow Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformOAuthFlow(flow) {
    return flow;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oauthFlowsObject
   * OAuth Flows Object}.
   *
   * @param {!object} flows OAuth Flows Object.
   * @returns {!object} Transformed OAuth Flows Object.
   */
  transformOAuthFlows(flows) {
    if (typeof flows !== 'object' || flows === null || isArray(flows)) {
      this.warn('Ignoring non-object OAuth Flows', flows);
      return flows;
    }

    const newFlows = { ...flows };

    if (flows.implicit) {
      newFlows.implicit = visit(
        this,
        this.transformOAuthFlow,
        'implicit',
        flows.implicit,
      );
    }

    if (flows.password) {
      newFlows.password = visit(
        this,
        this.transformOAuthFlow,
        'password',
        flows.password,
      );
    }

    if (flows.clientCredentials) {
      newFlows.clientCredentials = visit(
        this,
        this.transformOAuthFlow,
        'clientCredentials',
        flows.clientCredentials,
      );
    }

    if (flows.authorizationCode) {
      newFlows.authorizationCode = visit(
        this,
        this.transformOAuthFlow,
        'authorizationCode',
        flows.authorizationCode,
      );
    }

    return newFlows;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject
   * Security Scheme Object}.
   *
   * @param {!object} securityScheme Security Scheme Object.
   * @returns {!object} Transformed Security Scheme Object.
   */
  transformSecurityScheme(securityScheme) {
    if (typeof securityScheme !== 'object'
      || securityScheme === null
      || isArray(securityScheme)) {
      this.warn('Ignoring non-object Security Scheme', securityScheme);
      return securityScheme;
    }

    if (securityScheme.flows === undefined) {
      return securityScheme;
    }

    return {
      ...securityScheme,
      flows: visit(
        this,
        this.transformOAuthFlows,
        'flows',
        securityScheme.flows,
      ),
    };
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securityRequirementObject
   * Security Requirement Object}.
   *
   * @param {!object} securityRequirement Security Requirement Object.
   * @returns {!object} Transformed Security Requirement Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformSecurityRequirement(securityRequirement) {
    return securityRequirement;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#tagObject
   * Tag Object}.
   *
   * @param {!object} tag Tag Object.
   * @returns {!object} Transformed Tag Object.
   */
  transformTag(tag) {
    if (typeof tag !== 'object' || tag === null || isArray(tag)) {
      this.warn('Ignoring non-object Tag', tag);
      return tag;
    }

    if (tag.externalDocs === undefined) {
      return tag;
    }

    return {
      ...tag,
      externalDocs: visit(
        this,
        this.transformExternalDocs,
        'externalDocs',
        tag.externalDocs,
      ),
    };
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#contactObject
   * Contact Object}.
   *
   * @param {!object} contact Contact Object.
   * @returns {!object} Transformed Contact Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformContact(contact) {
    return contact;
  }

  /** Transforms a {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#licenseObject
   * License Object}.
   *
   * @param {!object} license License Object.
   * @returns {!object} Transformed License Object.
   */
  // eslint-disable-next-line class-methods-use-this
  transformLicense(license) {
    return license;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject
   * Info Object}.
   *
   * @param {!object} info Info Object.
   * @returns {!object} Transformed Info Object.
   */
  transformInfo(info) {
    if (typeof info !== 'object' || info === null || isArray(info)) {
      this.warn('Ignoring non-object Info', info);
      return info;
    }

    const newInfo = { ...info };

    if (info.contact !== undefined) {
      newInfo.contact = visit(
        this,
        this.transformContact,
        'contact',
        info.contact,
      );
    }

    if (info.license !== undefined) {
      newInfo.license = visit(
        this,
        this.transformLicense,
        'license',
        info.license,
      );
    }

    return info;
  }

  /** Transforms an {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oasObject
   * OpenAPI 3.0 Object} or {@link
   * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object
   * OpenAPI 2.0 (fka Swagger) Object}.
   *
   * @param {!object} openApi OpenAPI Object.
   * @returns {!object} Transformed OpenAPI Object.
   */
  transformOpenApi(openApi) {
    if (typeof openApi !== 'object' || openApi === null || isArray(openApi)) {
      this.warn('Ignoring non-object OpenAPI', openApi);
      return openApi;
    }

    const newOpenApi = {
      ...openApi,
    };

    if (openApi.info !== undefined) {
      newOpenApi.info = visit(this, this.transformInfo, 'info', openApi.info);
    }

    if (openApi.servers !== undefined) {
      newOpenApi.servers =
        this.transformArray(openApi.servers, this.transformServer);
    }

    // https://github.com/Azure/autorest/blob/master/docs/extensions/readme.md#x-ms-parameterized-host
    const xMsParameterizedHost = openApi['x-ms-parameterized-host'];
    if (typeof xMsParameterizedHost === 'object'
      && xMsParameterizedHost !== null) {
      const { parameters } = xMsParameterizedHost;
      if (parameters !== undefined) {
        newOpenApi['x-ms-parameterized-host'] = {
          ...xMsParameterizedHost,
          parameters: this.transformArray(parameters, this.transformParameter),
        };
      }
    }

    // Note: Transform components and definitions before properties likely
    // to have $refs pointing to them (to simplify renaming).
    // TODO: Guarantee this as part of the API?  Document in JSDoc comment.
    if (openApi.components !== undefined) {
      newOpenApi.components = visit(
        this,
        this.transformComponents,
        'components',
        openApi.components,
      );
    }

    if (openApi.definitions !== undefined) {
      newOpenApi.definitions = visit(
        this,
        this.transformMap,
        'definitions',
        openApi.definitions,
        this.transformSchema,
      );
    }

    if (openApi.parameters !== undefined) {
      newOpenApi.parameters = visit(
        this,
        this.transformMap,
        'parameters',
        openApi.parameters,
        this.transformParameter,
      );
    }

    if (openApi.responses !== undefined) {
      newOpenApi.responses = visit(
        this,
        this.transformMap,
        'responses',
        openApi.responses,
        this.transformResponse,
      );
    }

    if (openApi.paths !== undefined) {
      newOpenApi.paths = visit(
        this,
        this.transformPaths,
        'paths',
        openApi.paths,
      );
    }

    // https://github.com/Azure/autorest/tree/master/docs/extensions#x-ms-paths
    if (openApi['x-ms-paths'] !== undefined) {
      newOpenApi['x-ms-paths'] = visit(
        this,
        this.transformPaths,
        'x-ms-paths',
        openApi['x-ms-paths'],
      );
    }

    if (openApi.webhooks !== undefined) {
      newOpenApi.webhooks = visit(
        this,
        this.transformMap,
        'webhooks',
        openApi.webhooks,
        this.transformPathItem,
      );
    }

    if (openApi.security !== undefined) {
      newOpenApi.security = this.transformArray(
        openApi.security,
        this.transformSecurityRequirement,
      );
    }

    if (openApi.tags !== undefined) {
      newOpenApi.tags = this.transformArray(openApi.tags, this.transformTag);
    }

    if (openApi.externalDocs !== undefined) {
      newOpenApi.externalDocs = visit(
        this,
        this.transformExternalDocs,
        'externalDocs',
        openApi.externalDocs,
      );
    }

    return newOpenApi;
  }

  /** Logs a warning about the transformation.
   *
   * Logs to util.debuglog('openapi-transformer-base') by default.  Designed
   * to be overridden and/or reassigned to log as appropriate for projects
   * which use this class.
   *
   * @param {string|*} message Message with zero or more substitution strings,
   * or first value to log.
   * @param {*} values Additional values to log.  Applied to substitution
   * string in message, if one matches, otherwise appended.
   */
  warn(message, ...values) {
    // Note: debug.enabled defined on Node.js v14.9.0 and later
    if (debug.enabled !== false) {
      debug(message, ...values, 'at', toJsonPointer(this.transformPath));
    }
  }
}

module.exports = OpenApiTransformerBase;