Source: urlencoded-to-string.js

/**
 * @copyright Copyright 2020 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 * @module "openapi-transformers/urlencoded-to-string.js"
 */

import OpenApiTransformerBase from 'openapi-transformer-base';

const consumesSymbol = Symbol('consumes');

function isFormData(mediaType) {
  return /^multipart\/form-data\s*(?:;.*)?$/i.test(mediaType);
}

function isUrlencoded(mediaType) {
  return /^application\/x-www-form-urlencoded\s*(?:;.*)?$/i.test(mediaType);
}

/**
 * Transformer to change request parameter types to string for operations which
 * only consume application/x-www-form-urlencoded, to work around
 * https://github.com/Azure/autorest/issues/3449
 */
export default class UrlencodedToStringTransformer
  extends OpenApiTransformerBase {
  constructor() {
    super();
    this[consumesSymbol] = undefined;
  }

  transformSchema(schema) {
    if (typeof schema !== 'object' || schema === null) {
      return schema;
    }

    let newSchema = super.transformSchema(schema);

    // Change primitive types to string
    if (['number', 'integer', 'boolean'].includes(schema.type)) {
      newSchema = {
        ...newSchema,
        type: 'string',
      };

      // TODO: Optionally remove constraints which don't apply to string type?
      // Older JSON Schema drafts (e.g. JSON Schema Draft 4 ref'd by OAS2)
      // specify the instance is only valid if it satisfies the constraint.
      // Newer JSON Schema drafts (e.g. Wright 00 ref'd by OAS3.1) specify
      // the constraint is ignored for non-numbers.
      // Autorest ignores these constraints on string properties.
      // They may be useful as documentation for API developers.
      // Is there a reason to remove them?
      /*
      delete newSchema.multipleOf;
      delete newSchema.minimum;
      delete newSchema.exclusiveMaximum;
      delete newSchema.maximum;
      delete newSchema.exclusiveMinimum;
      */
    }

    // Ensure enums are modeled as string
    const { type, 'x-ms-enum': xMsEnum } = newSchema;
    if (type === 'string'
      && xMsEnum
      && xMsEnum.modelAsString !== true) {
      newSchema = { ...newSchema };
      // Note: Could set modelAsString: true, but would cause conflict if any
      // other params/schemas have same name
      delete newSchema['x-ms-enum'];
    }

    // Note: There does not appear to be a need to remove validation properties
    // (e.g. minimum, maximum, etc.) since Autorest ignores them for string
    // properties.

    return newSchema;
  }

  transformParameter(parameter) {
    if (!parameter) {
      return parameter;
    }

    if (parameter.in === 'formData') {
      return this.transformSchema(parameter);
    }

    if (parameter.in === 'body' && parameter.schema) {
      return {
        ...parameter,
        schema: this.transformSchema(parameter.schema),
      };
    }

    return parameter;
  }

  transformRequestBody(requestBody) {
    if (!requestBody || !requestBody.content) {
      return requestBody;
    }

    const { content } = requestBody;
    const consumes = Object.keys(content);
    const urlencoded = consumes.find(isUrlencoded);
    if (!urlencoded) {
      return requestBody;
    }

    return {
      ...requestBody,
      content: {
        ...content,
        [urlencoded]: this.transformMediaType(content[urlencoded]),
      },
    };
  }

  transformOperation(operation) {
    if (!operation) {
      return super.transformOperation(operation);
    }

    const { parameters, requestBody } = operation;
    if (requestBody === undefined && !Array.isArray(parameters)) {
      return operation;
    }

    const newOperation = { ...operation };

    const consumes = operation.consumes || this[consumesSymbol];
    if (parameters !== undefined
      && Array.isArray(consumes)
      && consumes.some(isUrlencoded)
      && !consumes.some(isFormData)) {
      newOperation.parameters =
        parameters.map(this.transformParameter.bind(this));
    }

    if (requestBody !== undefined) {
      newOperation.requestBody =
        this.transformRequestBody(newOperation.requestBody);
    }

    return newOperation;
  }

  transformOpenApi(openApi) {
    this[consumesSymbol] = openApi.consumes;
    try {
      return {
        ...openApi,
        paths: this.transformPaths(openApi.paths),
      };
    } finally {
      this[consumesSymbol] = undefined;
    }
  }
}