Source: read-only-not-required.js

/**
 * @copyright Copyright 2019 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 * @module "openapi-transformers/read-only-not-required.js"
 */

import OpenApiTransformerBase from 'openapi-transformer-base';

/**
 * Transformer to ensure readOnly schema properties are not required, to work
 * around lack of support in generators.
 *
 * The behavior of required readOnly/writeOnly properties is defined in OAS3
 * (the requirement only applies on response/request), but not in OAS2 (where
 * writeOnly is not defined and "Properties marked as readOnly being true
 * SHOULD NOT be in the required list").
 *
 * AutoRest Behavior:
 * - required properties must be specified in the constructor.
 * - required properties are non-nullable (unless x-nullable: true).
 * - non-required properties are nullable (unless x-nullable: false).
 * - validation is applied to non-null values in requests, not responses.
 */
export default class ReadOnlyNotRequiredTransformer
  extends OpenApiTransformerBase {
  constructor({ removeValidation, setNonNullable } = {}) {
    super();

    if (removeValidation !== undefined
      && typeof removeValidation !== 'boolean') {
      throw new TypeError('options.removeValidation must be boolean');
    }

    if (setNonNullable !== undefined
      && typeof setNonNullable !== 'boolean') {
      throw new TypeError('options.setNonNullable must be boolean');
    }

    this.options = {
      removeValidation,
      setNonNullable,
    };
  }

  transformSchema(schema) {
    const newSchema = super.transformSchema(schema);
    const { properties, required } = newSchema;
    if (!properties || !Array.isArray(required) || required.length === 0) {
      // No properties are defined or none are required
      return newSchema;
    }

    const readOnlyNames = Object.keys(properties)
      .filter((propName) => properties[propName].readOnly);
    if (readOnlyNames.length === 0) {
      // No readOnly properties
      return newSchema;
    }

    const newRequired = required
      .filter((propName) => !readOnlyNames.includes(propName));
    if (newRequired.length === required.length) {
      // No readOnly properties are required
      return newSchema;
    }

    let newProperties = properties;
    if (this.options.removeValidation || this.options.setNonNullable) {
      newProperties = { ...properties };
      const readOnlyRequired = required
        .filter((propName) => readOnlyNames.includes(propName));
      for (const readOnlyName of readOnlyRequired) {
        const newProperty = { ...properties[readOnlyName] };

        if (this.options.removeValidation) {
          // FIXME: Should handle $ref, which is complicated
          delete newProperty.exclusiveMaximum;
          delete newProperty.exclusiveMinimum;
          delete newProperty.maxItems;
          delete newProperty.maxLength;
          delete newProperty.maximum;
          delete newProperty.minItems;
          delete newProperty.minLength;
          delete newProperty.minimum;
          delete newProperty.multipleOf;
          delete newProperty.pattern;
        }

        if (this.options.setNonNullable
          && !Object.hasOwn(newProperty, 'x-nullable')) {
          // Note: x-nullable: false is ignored for enum and class types
          // FIXME: Adding x-nullable to $ref works in AutoRest (if $ref is to
          // non-nullable type other than enum) but violates spec.
          newProperty['x-nullable'] = false;
        }

        newProperties[readOnlyName] = newProperty;
      }
    }

    const updatedSchema = {
      ...newSchema,
      properties: newProperties,
      required: newRequired,
    };
    if (newRequired.length === 0) {
      delete updatedSchema.required;
    }
    return updatedSchema;
  }
}