Source: nullable-not-required.js

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

import OpenApiTransformerBase from 'openapi-transformer-base';

/**
 * Determines whether a given Schema can have a null value.
 *
 * @param {!object} schema OpenAPI Schema object.
 * @param {boolean} hasNullType Whether 'null' is a valid Schema type.
 * @param {boolean} refNullable Treat $ref as nullable.
 * @returns {boolean} <c>true</c> if <c>null</c> is a valid value for
 * <c>schema</c>.
 */
function isNullable(schema, hasNullType, refNullable) {
  const {
    $ref,
    nullable,
    'x-nullable': xNullable,
    type,
  } = schema;

  if ($ref) {
    if (typeof nullable === 'boolean') {
      return nullable;
    }

    if (typeof xNullable === 'boolean') {
      return xNullable;
    }

    return refNullable;
  }

  if (hasNullType) {
    if (typeof type === 'string') {
      if (type !== 'null') {
        // null forbidden by non-null type
        return false;
      }
    } else if (Array.isArray(type)
      && !type.includes('null')) {
      // null forbidden by null not in types
      return false;
    }
  } else if (!nullable && !xNullable) {
    // Without null type, null is only allowed with nullable (or x-nullable)
    return false;
  }

  return true;
}

/**
 * Determines whether a given property of a given schema can have a null value.
 *
 * @param {!object} schema OpenAPI Schema object.
 * @param {string} propName Name of property to check.
 * @param {boolean} hasNullType Whether 'null' is a valid Schema type.
 * @param {boolean} refNullable Treat $ref as nullable.
 * @returns {?boolean} <c>true</c> if <c>null</c> is known to be a valid value
 * for <c>propName</c> in <c>schema</c>, <c>false</c> if <c>null</c> is known
 * to be an invalid value for <c>propName</c> in <c>schema</c>.
 * <c>undefined</c> if <c>propName</c> is not constrained by <c>schema</c>.
 */
function isPropNullable(schema, propName, hasNullType, refNullable) {
  const {
    additionalProperties,
    allOf,
    anyOf,
    oneOf,
    properties,
  } = schema;
  let constrained = false;
  const propSchema = properties?.[propName];
  if (propSchema) {
    constrained = true;
    if (!isNullable(propSchema, hasNullType, refNullable)) {
      return false;
    }
  } else if (additionalProperties) {
    constrained = true;
    if (!isNullable(additionalProperties, hasNullType, refNullable)) {
      // schema in additionalProperties does not allow null
      return false;
    }
  } else if (additionalProperties === false) {
    // would fail validation if property is present
    return false;
  }

  if (Array.isArray(allOf)) {
    for (const allSchema of allOf) {
      switch (isPropNullable(allSchema, propName, hasNullType, refNullable)) {
        case false:
          // If an allOf schema doesn't allow null, it's not allowed.
          return false;

        case true:
          // An allOf schema allows null
          constrained = true;
          break;

        default:
          // An allOf schema doesn't constrain propName
          break;
      }
    }
  }

  if (Array.isArray(anyOf)) {
    let anyNullable = false;
    let anyUnconstrained = false;
    for (const anySchema of anyOf) {
      switch (isPropNullable(anySchema, propName, hasNullType, refNullable)) {
        case false:
          // An anyOf schema disallows null
          break;

        case true:
          // An anyOf schema allows null
          anyNullable = true;
          break;

        default:
          // An anyOf schema doesn't constrain propName
          anyUnconstrained = true;
          break;
      }
    }

    if (!anyNullable && !anyUnconstrained) {
      return false;
    }

    if (!anyUnconstrained) {
      constrained = true;
    }
  }

  if (Array.isArray(oneOf)) {
    let anyNullable = false;
    let anyUnconstrained = false;
    for (const oneSchema of oneOf) {
      switch (isPropNullable(oneSchema, propName, hasNullType, refNullable)) {
        case false:
          // A oneOf schema disallows null
          break;

        case true:
          // A oneOf schema allows null
          anyNullable = true;
          break;

        default:
          // A oneOf schema doesn't constrain propName
          anyUnconstrained = true;
          break;
      }
    }

    if (!anyNullable && !anyUnconstrained) {
      return false;
    }

    if (!anyUnconstrained) {
      constrained = true;
    }
  }

  return constrained ? true : undefined;
}

/**
 * Transformer to make properties which are nullable non-required to work around
 * partial support for x-nullable in Autorest.  Currently nullable data types
 * are used (e.g. Nullable<int>) but the Validate() method doesn't allow null.
 * See:  https://github.com/Azure/autorest/issues/3300
 */
export default class NullableNotRequiredTransformer
  extends OpenApiTransformerBase {
  hasNullType = undefined;

  refNullable = false;

  requireUnconstrained = false;

  constructor(options) {
    super();
    this.refNullable = Boolean(options?.refNullable);
    this.requireUnconstrained = Boolean(options?.requireUnconstrained);
  }

  transformSchema(schema) {
    const newSchema = super.transformSchema(schema);
    if (!Array.isArray(newSchema?.required)
      || newSchema.required.length === 0) {
      // No properties are required. None to remove.
      return newSchema;
    }

    const transformed = {
      ...newSchema,
      required: newSchema.required
        .filter(
          (reqName) => !(isPropNullable(
            newSchema,
            reqName,
            this.hasNullType,
            this.refNullable,
          ) ?? !this.requireUnconstrained),
        ),
    };

    if (transformed.required.length === 0) {
      delete transformed.required;
    }

    return transformed;
  }

  transformOpenApi(openApi) {
    const version = openApi?.openapi || openApi?.swagger;
    const verParts = version ? String(version).split('.').map(Number) : [];
    if (verParts[0] > 3 || (verParts[0] === 3 && verParts[1] >= 1)) {
      this.hasNullType = true;
    } else if (verParts[0] < 3 || (verParts[0] === 3 && verParts[1] < 1)) {
      this.hasNullType = false;
    }

    try {
      return super.transformOpenApi(openApi);
    } finally {
      this.hasNullType = undefined;
    }
  }
}