UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

1,955 lines • 65.8 kB
/**
 * Generates a deterministic sample string matching common regex patterns in OpenAPI schemas.
 * Falls back to the pattern string if an unexpected error occurs.
 */
export function patternSampleGenerator(pattern) {
  if (!pattern || typeof pattern !== "string") {
    return "";
  }

  try {
    let p = pattern.trim();
    if (p.startsWith("^")) p = p.slice(1);
    if (p.endsWith("$") && !p.endsWith("\\$")) p = p.slice(0, -1);

    // Resolve simple group alternations: (opt1|opt2|opt3) -> opt1
    p = p.replace(/\((?:\?:)?([^()|]+)(?:\|[^()]+)*\)/g, "$1");

    // Tokenizer matching
    const tokenRegex =
      /(\[[^\]]+\]|\\[dwsDWS]|\\[^]|\.|[^\\[\]{}()+*?|])(?:\{(\d+)(?:,\d*)?\}|([+*?]))?/g;

    let result = "";
    let match;

    while ((match = tokenRegex.exec(p)) !== null) {
      const token = match[1];
      const count = match[2];
      const quantifier = match[3];

      let rep = 1;
      if (count !== undefined) {
        rep = parseInt(count, 10);
      } else if (quantifier === "+") {
        rep = 1;
      } else if (quantifier === "*" || quantifier === "?") {
        rep = 0;
      }

      rep = Math.min(Math.max(0, rep), 50);

      let sampleChar = "a";
      if (token === "\\d") {
        sampleChar = "0";
      } else if (token === "\\D") {
        sampleChar = "a";
      } else if (token === "\\w") {
        sampleChar = "a";
      } else if (token === "\\W") {
        sampleChar = "_";
      } else if (token === "\\s") {
        sampleChar = " ";
      } else if (token === "\\S") {
        sampleChar = "a";
      } else if (token === ".") {
        sampleChar = "a";
      } else if (token.startsWith("\\")) {
        sampleChar = token.slice(1);
      } else if (token.startsWith("[")) {
        const inner = token.slice(1, -1);
        if (inner.startsWith("^")) {
          sampleChar = inner.includes("a") ? "0" : "a";
        } else if (/[1-9]/.test(inner) && !inner.includes("0")) {
          sampleChar = "1";
        } else if (/[0-9]/.test(inner) && !/[a-zA-Z]/.test(inner)) {
          sampleChar = "0";
        } else if (/[0-9]/.test(inner) && /[a-fA-F]/.test(inner) && !/[g-zG-Z]/.test(inner)) {
          sampleChar = "0";
        } else if (/[A-Z]/.test(inner) && !/[a-z]/.test(inner)) {
          sampleChar = "A";
        } else if (/[a-z]/.test(inner)) {
          sampleChar = "a";
        } else if (inner.length > 0) {
          sampleChar = inner[0];
        }
      } else {
        sampleChar = token;
      }

      result += sampleChar.repeat(rep);
    }

    return result || pattern;
  } catch {
    return pattern;
  }
}

// Takes a value as input and provides a printable string to replresent null values, spaces, blankstring etc
export function getPrintableVal(val) {
  if (val === undefined) {
    return "";
  }
  if (val === null) {
    return "null";
  }
  if (val === "") {
    return "∅";
  }
  if (typeof val === "boolean" || typeof val === "number") {
    return `${val}`;
  }
  if (Array.isArray(val)) {
    return val
      .map((v) =>
        v === null
          ? "null"
          : v === ""
            ? "∅"
            : v.toString().replace(/^ +| +$/g, (m) => "●".repeat(m.length)) || "",
      )
      .join(", ");
  }
  if (typeof val === "object") {
    const keys = Object.keys(val);
    return `{ ${keys[0]}:${val[keys[0]]}${keys.length > 1 ? "," : ""} ... }`;
  }
  return val.toString().replace(/^ +| +$/g, (m) => "●".repeat(m.length)) || "";
}

/* Helper to detect binary file fields across OpenAPI 3.0 and 3.1 */
export function isBinaryFileField(schema) {
  if (!schema) {
    return false;
  }
  return (
    schema.format === "binary" ||
    schema.contentEncoding === "binary" ||
    (Boolean(schema.contentMediaType) && schema.contentEncoding !== "base64")
  );
}

// Keywords that only annotate a schema, without telling anything about the type of the value
const ANNOTATION_KEYWORDS = new Set([
  "title",
  "description",
  "example",
  "examples",
  "default",
  "deprecated",
  "readOnly",
  "writeOnly",
  "externalDocs",
]);

function isAnnotationOnly(schema) {
  return Object.keys(schema).every((key) => ANNOTATION_KEYWORDS.has(key) || key.startsWith("x-"));
}

/* Generates an schema object containing type and constraint info */
export function getTypeInfo(schema) {
  if (!schema) {
    return;
  }
  let dataType = "";
  let constrain = "";
  // let examples;

  if (schema.$ref) {
    const n = schema.$ref.lastIndexOf("/");
    const schemaNode = schema.$ref.substring(n + 1);
    dataType = `{recursive: ${schemaNode}} `;
  } else if (schema.type) {
    dataType = Array.isArray(schema.type) ? schema.type.join("┃") : schema.type;
    if (schema.format || schema.enum || schema.const !== undefined) {
      dataType = dataType.replace(
        "string",
        schema.enum ? "enum" : schema.const !== undefined ? "const" : schema.format,
      );
    }
    if (schema.nullable) {
      dataType += "┃null";
    }
  } else if (schema.const !== undefined) {
    dataType = "const";
  } else if (schema.anyOf || schema.oneOf) {
    const subSchemas = (schema.anyOf || schema.oneOf).filter(Boolean);
    const subTypes = [];
    subSchemas.forEach((s) => {
      const sInfo = getTypeInfo(s);
      if (sInfo?.type && sInfo.type !== "{missing-type-info}") {
        sInfo.type.split("┃").forEach((t) => {
          const trimmed = t.trim();
          if (trimmed && !subTypes.includes(trimmed)) {
            subTypes.push(trimmed);
          }
        });
      }
    });
    dataType = subTypes.length > 0 ? subTypes.join("┃") : "{missing-type-info}";
  } else if (isAnnotationOnly(schema)) {
    dataType = "any";
  } else {
    dataType = "{missing-type-info}";
  }

  const effectiveSchema =
    schema.anyOf || schema.oneOf
      ? {
          ...((schema.anyOf || schema.oneOf).find((s) => s && s.type && s.type !== "null") ||
            (schema.anyOf || schema.oneOf)[0]),
          ...schema,
        }
      : schema;

  const info = {
    type: dataType,
    format: schema.format || effectiveSchema.format || "",
    contentMediaType: schema.contentMediaType || effectiveSchema.contentMediaType || "",
    contentEncoding: schema.contentEncoding || effectiveSchema.contentEncoding || "",
    contentSchema: schema.contentSchema || effectiveSchema.contentSchema || null,
    pattern:
      (schema.pattern || effectiveSchema.pattern) && !schema.enum && !effectiveSchema.enum
        ? schema.pattern || effectiveSchema.pattern
        : "",
    readOrWriteOnly: schema.readOnly
      ? "🆁"
      : schema.writeOnly
        ? "🆆"
        : effectiveSchema.readOnly
          ? "🆁"
          : effectiveSchema.writeOnly
            ? "🆆"
            : "",
    deprecated: schema.deprecated || effectiveSchema.deprecated ? "❌" : "",
    examples:
      schema.examples || schema.example || effectiveSchema.examples || effectiveSchema.example,
    default: getPrintableVal(
      schema.default !== undefined ? schema.default : effectiveSchema.default,
    ),
    description: schema.description || effectiveSchema.description || "",
    constrain: "",
    allowedValues: "",
    arrayType: "",
    html: "",
  };

  if (info.type === "{missing-type-info}" || info.type === "any") {
    info.description = info.description || "";
  }
  // Set Allowed Values
  info.allowedValues =
    schema.const !== undefined
      ? getPrintableVal(schema.const)
      : Array.isArray(schema.enum)
        ? schema.enum.map((v) => getPrintableVal(v)).join("┃")
        : effectiveSchema.const !== undefined
          ? getPrintableVal(effectiveSchema.const)
          : Array.isArray(effectiveSchema.enum)
            ? effectiveSchema.enum.map((v) => getPrintableVal(v)).join("┃")
            : "";

  if (!info.allowedValues && (schema.anyOf || schema.oneOf)) {
    const subValues = [];
    (schema.anyOf || schema.oneOf).forEach((s) => {
      const sVal =
        s.const !== undefined
          ? getPrintableVal(s.const)
          : Array.isArray(s.enum)
            ? s.enum.map((v) => getPrintableVal(v)).join("┃")
            : "";
      if (sVal && !subValues.includes(sVal)) {
        subValues.push(sVal);
      }
    });
    if (subValues.length > 0) {
      info.allowedValues = subValues.join("┃");
    }
  }

  const itemsSchema = schema.items || effectiveSchema.items;
  if ((dataType === "array" || dataType.split("┃").includes("array")) && itemsSchema) {
    const arrayItemType =
      itemsSchema.format ||
      itemsSchema.contentMediaType ||
      (Array.isArray(itemsSchema?.type) ? itemsSchema.type.join("┃") : itemsSchema?.type) ||
      "";
    const arrayItemDefault = getPrintableVal(itemsSchema.default);

    info.arrayType = arrayItemType ? `array of ${arrayItemType}` : `${schema.type || "array"}`;
    if (!info.default) {
      info.default = arrayItemDefault;
    }
    if (!info.allowedValues) {
      info.allowedValues =
        itemsSchema.const !== undefined
          ? getPrintableVal(itemsSchema.const)
          : Array.isArray(itemsSchema?.enum)
            ? itemsSchema.enum.map((v) => getPrintableVal(v)).join("┃")
            : "";
    }
  }
  if (dataType.match(/integer|number/g)) {
    const minVal = schema.minimum !== undefined ? schema.minimum : effectiveSchema.minimum;
    const excMinVal =
      schema.exclusiveMinimum !== undefined
        ? schema.exclusiveMinimum
        : effectiveSchema.exclusiveMinimum;
    if (minVal !== undefined || excMinVal !== undefined) {
      constrain += minVal !== undefined ? `Min ${minVal}` : `More than ${excMinVal}`;
    }
    const maxVal = schema.maximum !== undefined ? schema.maximum : effectiveSchema.maximum;
    const excMaxVal =
      schema.exclusiveMaximum !== undefined
        ? schema.exclusiveMaximum
        : effectiveSchema.exclusiveMaximum;
    if (maxVal !== undefined || excMaxVal !== undefined) {
      constrain +=
        maxVal !== undefined
          ? `${constrain ? "┃" : ""}Max ${maxVal}`
          : `${constrain ? "┃" : ""}Less than ${excMaxVal}`;
    }
    const multVal =
      schema.multipleOf !== undefined ? schema.multipleOf : effectiveSchema.multipleOf;
    if (multVal !== undefined) {
      constrain += `${constrain ? "┃" : ""} multiple of ${multVal}`;
    }
  }
  // dataType shows the format or enum in place of 'string' (e.g. 'email', 'uri'), so check the declared type too
  const declaredTypes = [].concat(effectiveSchema.type || []);
  if (declaredTypes.includes("string") || dataType.match(/string/g)) {
    const minLen = schema.minLength !== undefined ? schema.minLength : effectiveSchema.minLength;
    const maxLen = schema.maxLength !== undefined ? schema.maxLength : effectiveSchema.maxLength;
    if (minLen !== undefined && maxLen !== undefined) {
      constrain += `${constrain ? "┃" : ""}${minLen} to ${maxLen} chars`;
    } else if (minLen !== undefined) {
      constrain += `${constrain ? "┃" : ""}Min ${minLen} chars`;
    } else if (maxLen !== undefined) {
      constrain += `${constrain ? "┃" : ""}Max ${maxLen} chars`;
    }
  }
  info.constrain = constrain;
  info.html = `${info.type}~|~${info.readOrWriteOnly}~|~${info.constrain}~|~${info.default}~|~${info.allowedValues}~|~${info.pattern}~|~${info.description}~|~${schema.title || ""}~|~${info.deprecated ? "deprecated" : ""}`;
  return info;
}

/**
 *
 * @param {*} ex  if the value
 *  - Is an Object with 'value' property  like
 *      { 'value': 'example_val1', 'description': 'some description' }
 *    Returns >>>
 *      {
 *        'Example': { 'value' : 'example_val1', 'description': 'some description' },
 *      }
 *  - Is an object where each key represents a valid example object (i,e has a value property)
 *      {
 *        'example1': { 'value' : 'example_val1', 'description': 'some description' },
 *        'example2': { 'value' : 'example_val2', 'description': 'some other description' },
 *        'invalid':  { 'description': 'invalid example object without any value property' }
 *      }
 *    Returns >>>
 *      {
 *        'example1': { 'value' : 'example_val1', 'description': 'some description' },
 *        'example2': { 'value' : 'example_val2', 'description': 'some other description' }
 *      }
 *      if none of the keys represents an object with 'value' property then return undefined
 *  - Is an array of premitive values
 *      ['example_val1', 'example_val2']
 *    Returns >>>
 *      {
 *         'Example1': {value:'value1'}
 *         'Example2': {value:'value2'}
 *      }
 *  - Is a premitive value
 *      'example_val1'
 *    Returns >>>
 *      {
 *        'Example': { 'value': 'example_val1' }
 *      }
 *  - Is undefined
 *    returns undefined
 * @returns
 */

export function standardizeExample(ex) {
  if (typeof ex === "object" && ex !== null && !Array.isArray(ex)) {
    if (ex.value !== undefined) {
      // Case 1: Single object with 'value' property
      return { Example: { ...ex } };
    }
    // Case 2: Object where each key is an object with a 'value' property
    const filteredEntries = Object.entries(ex).filter(([_, obj]) => obj?.value !== undefined); // oxlint-disable-line no-unused-vars
    // If no valid entries found, return JSON.stringify of the input
    if (filteredEntries.length === 0) {
      return undefined;
    }
    return Object.fromEntries(filteredEntries);
  }
  if (Array.isArray(ex)) {
    // Case 3: Array of primitive values
    return ex.reduce((acc, value, index) => {
      acc[`Example${index + 1}`] = { value };
      return acc;
    }, {});
  }
  // Case 4: Single primitive value
  return ex ? { Example: { value: ex } } : undefined;
}

/**
 *  Normalize example object in the following format (List of object which is used to render example links and fill the input boxes)
 *  [{
 *     exampleVal  : 'value to be rendered on the input control (text-box)',
 *     exampleList : [
 *       value         : '',
 *       printableValue: '',
 *       summary       : '',
 *       description   : ''
 *     ]
 *  }]
 * */
export function normalizeExamples(examples, dataType = "string") {
  if (!examples) {
    return {
      exampleVal: "",
      exampleList: [],
    };
  }
  if (examples.constructor === Object) {
    const exampleList = Object.values(examples)
      .filter((v) => v["x-example-show-value"] !== false)
      .map((v) => ({
        value:
          typeof v.value === "boolean" || typeof v.value === "number"
            ? `${v.value}`
            : v.value || "",
        printableValue: getPrintableVal(v.value),
        summary: v.summary || "",
        description: v.description || "",
      }));
    const exampleVal = exampleList.length > 0 ? exampleList[0].value : "";
    return { exampleVal, exampleList };
  }

  // This is non-standard way to provide example but will support for now
  if (!Array.isArray(examples)) {
    examples = examples ? [examples] : [];
  }

  if (examples.length === 0) {
    return {
      exampleVal: "",
      exampleList: [],
    };
  }

  if (dataType === "array") {
    const [exampleVal] = examples;
    const exampleList = examples.map((v) => ({
      value: v,
      printableValue: getPrintableVal(v),
    }));
    return { exampleVal, exampleList };
  }

  const exampleVal = examples[0].toString();
  const exampleList = examples.map((v) => ({
    value: v.toString(),
    printableValue: getPrintableVal(v),
  }));
  return { exampleVal, exampleList };
}

export function anyExampleWithSummaryOrDescription(examples) {
  return examples.some((x) => x.summary?.length > 0 || x.description?.length > 0);
}

/**
 * The 'example' property was deprecated in 3.1.0 in favor of the JSON Schema 'examples' keyword. However RapiDoc supports both. in the following order:
 * - examples
 * - example
 *
 * Returns the first example in the given schema.
 * Returns `undefined` if `schema` is undefined or if there are no examples at the top level of the `schema`.
 */
function getFirstExample(schema) {
  let firstExample;
  if (schema) {
    if (schema.examples && schema.examples.length >= 1) {
      firstExample = schema.examples[0];
    } else {
      firstExample = schema.example;
    }
  }
  return firstExample;
}

export function getSampleValueByType(schemaObj) {
  const example = getFirstExample(schemaObj);
  if (example === "") {
    return "";
  }
  if (example === null) {
    return null;
  }
  if (example === 0) {
    return 0;
  }
  if (example === false) {
    return false;
  }
  if (example instanceof Date) {
    switch (schemaObj.format.toLowerCase()) {
      case "date":
        return example.toISOString().split("T")[0];
      case "time":
        return example.toISOString().split("T")[1];
      default:
        return example.toISOString();
    }
  }
  if (example) {
    return example;
  }

  if (Object.keys(schemaObj).length === 0) {
    return null;
  }
  if (schemaObj.$ref) {
    // Indicates a Circular ref
    return {};
  }
  if (
    schemaObj.const === false ||
    schemaObj.const === 0 ||
    schemaObj.const === null ||
    schemaObj.const === ""
  ) {
    return schemaObj.const;
  }
  if (schemaObj.const) {
    return schemaObj.const;
  }
  if (schemaObj.default) {
    return schemaObj.default;
  }
  const typeValue = Array.isArray(schemaObj.type) ? schemaObj.type[0] : schemaObj.type;
  if (!typeValue) {
    return null;
  }
  if (typeValue.match(/^integer|^number/g)) {
    const multipleOf = Number.isNaN(Number(schemaObj.multipleOf))
      ? undefined
      : Number(schemaObj.multipleOf);
    const maximum = Number.isNaN(Number(schemaObj.maximum)) ? undefined : Number(schemaObj.maximum);
    const minimumPossibleVal = Number.isNaN(Number(schemaObj.minimum))
      ? Number.isNaN(Number(schemaObj.exclusiveMinimum))
        ? maximum || 0
        : Number(schemaObj.exclusiveMinimum) + (typeValue.startsWith("integer") ? 1 : 0.001)
      : Number(schemaObj.minimum);
    const finalVal = multipleOf
      ? multipleOf >= minimumPossibleVal
        ? multipleOf
        : minimumPossibleVal % multipleOf === 0
          ? minimumPossibleVal
          : Math.ceil(minimumPossibleVal / multipleOf) * multipleOf
      : minimumPossibleVal;
    return finalVal;
  }
  if (typeValue.match(/^boolean/g)) {
    return false;
  }
  if (typeValue.match(/^null/g)) {
    return null;
  }
  if (typeValue.match(/^string/g)) {
    if (schemaObj.enum) {
      return schemaObj.enum[0];
    }
    if (schemaObj.const) {
      return schemaObj.const;
    }
    if (schemaObj.pattern) {
      try {
        return patternSampleGenerator(schemaObj.pattern);
      } catch {
        return schemaObj.pattern;
      }
    }
    if (schemaObj.contentEncoding) {
      switch (schemaObj.contentEncoding.toLowerCase()) {
        case "base64":
          return "ZXhhbXBsZQ=="; // 'example' base64 encoded
        case "binary":
        case "7bit":
        case "8bit":
        case "quoted-printable":
          return "string";
      }
    }
    if (schemaObj.contentMediaType) {
      if (schemaObj.contentMediaType.toLowerCase() === "application/json") {
        if (schemaObj.contentSchema) {
          const sample = schemaToSampleObj(schemaObj.contentSchema);
          const sampleVal = sample?.["example-0"] ?? sample;
          return JSON.stringify(sampleVal);
        }
        return "{}";
      }
      if (schemaObj.contentMediaType.startsWith("text/")) {
        return "text content";
      }
    }
    if (schemaObj.format) {
      switch (schemaObj.format.toLowerCase()) {
        case "url":
        case "uri":
          return "http://example.com";
        case "date":
          return new Date(0).toISOString().split("T")[0];
        case "time":
          return new Date(0).toISOString().split("T")[1];
        case "date-time":
          return new Date(0).toISOString();
        case "duration":
          return "P3Y6M4DT12H30M5S"; // P=Period 3-Years 6-Months 4-Days 12-Hours 30-Minutes 5-Seconds
        case "email":
        case "idn-email":
          return "user@example.com";
        case "hostname":
        case "idn-hostname":
          return "www.example.com";
        case "ipv4":
          return "198.51.100.42";
        case "ipv6":
          return "2001:0db8:5b96:0000:0000:426f:8e17:642a";
        case "uuid":
          return "3fa85f64-5717-4562-b3fc-2c963f66afa6";
        case "byte":
        case "base64":
          return "ZXhhbXBsZQ=="; // 'example' base64 encoded. See https://spec.openapis.org/oas/v3.0.0#data-types
        case "binary":
          return "binary";
        default:
          return "";
      }
    } else {
      const minLength = Number.isNaN(schemaObj.minLength) ? undefined : Number(schemaObj.minLength);
      const maxLength = Number.isNaN(schemaObj.maxLength) ? undefined : Number(schemaObj.maxLength);
      const finalLength = minLength || (maxLength > 6 ? 6 : maxLength || undefined);
      return finalLength ? "A".repeat(finalLength) : "string";
    }
  }
  // If type cannot be determined
  return null;
}

/*
json2xml- TestCase
  {
    'prop1' : 'one',
    'prop2' : 'two',
    'prop3' : [ 'a', 'b', 'c' ],
    'prop4' : {
      'ob1' : 'val-1',
      'ob2' : 'val-2'
    }
  }
  <root>
    <prop1>simple</prop1>
    <prop2>
      <0>a</0>
      <1>b</1>
      <2>c</2>
    </prop2>
    <prop3>
      <ob1>val-1</ob1>
      <ob2>val-2</ob2>
    </prop3>
  </root>
*/
export function json2xml(obj, level = 1) {
  if (obj === null || obj === undefined) {
    return "";
  }
  const indent = "  ".repeat(level);
  let xmlText = "";
  if (typeof obj !== "object") {
    return `\n${indent}${obj.toString()}`;
  }
  for (const prop in obj) {
    if (prop.startsWith("::")) {
      continue;
    }
    const val = obj[prop];
    let tagName = prop;
    if (Array.isArray(val)) {
      tagName = val["::XML_TAG"] || val[0]?.["::XML_TAG"] || prop;
    } else if (val && typeof val === "object") {
      tagName = val["::XML_TAG"] || prop;
    } else if (Array.isArray(obj) && obj["::XML_TAG"]) {
      tagName = obj["::XML_TAG"];
    }

    if (val === null || val === undefined) {
      xmlText = `${xmlText}\n${indent}<${tagName}></${tagName}>`;
    } else if (Array.isArray(val)) {
      xmlText = `${xmlText}\n${indent}<${tagName}>${json2xml(val, level + 1)}\n${indent}</${tagName}>`;
    } else if (typeof val === "object") {
      xmlText = `${xmlText}\n${indent}<${tagName}>${json2xml(val, level + 1)}\n${indent}</${tagName}>`;
    } else {
      xmlText = `${xmlText}\n${indent}<${tagName}>${val.toString()}</${tagName}>`;
    }
  }
  return xmlText;
}

function addSchemaInfoToExample(schema, obj) {
  if (typeof obj !== "object" || obj === null) {
    return;
  }
  if (schema.title) {
    obj["::TITLE"] = schema.title;
  }
  if (schema.description) {
    obj["::DESCRIPTION"] = schema.description;
  }
  if (schema.xml?.name) {
    obj["::XML_TAG"] = schema.xml?.name;
  }
  if (schema.xml?.wrapped) {
    obj["::XML_WRAP"] = schema.xml?.wrapped.toString();
  }
}

function removeTitlesAndDescriptions(obj) {
  if (typeof obj !== "object" || obj === null) {
    return;
  }
  delete obj["::TITLE"];
  delete obj["::DESCRIPTION"];
  delete obj["::XML_TAG"];
  delete obj["::XML_WRAP"];
  for (const k in obj) {
    removeTitlesAndDescriptions(obj[k]);
  }
}

function mergePropertyExamples(obj, propertyName, propExamples) {
  if (!obj || typeof obj !== "object") {
    return obj;
  }
  const propVal =
    propExamples && typeof propExamples === "object" && "example-0" in propExamples
      ? propExamples["example-0"]
      : propExamples &&
          typeof propExamples === "object" &&
          Object.keys(propExamples).length > 0 &&
          !Array.isArray(propExamples)
        ? Object.values(propExamples)[0]
        : propExamples;

  for (const exampleKey in obj) {
    if (obj[exampleKey] && typeof obj[exampleKey] === "object") {
      obj[exampleKey][propertyName] = propVal;
    }
  }
  return obj;
}

/* For changing JSON-Schema to a Sample Object, as per the schema (to generate examples based on schema) */
export function schemaToSampleObj(schema, config = {}, level = 0) {
  let obj = {};
  if (!schema || level > 8) {
    return;
  }
  if (schema.allOf) {
    const mergedObj = {};

    if (schema.allOf.length === 1 && !schema.allOf[0]?.properties && !schema.allOf[0]?.items) {
      // If allOf has single item and the type is not an object or array, then its a primitive
      if (schema.allOf[0].$ref) {
        return { "example-0": {} };
      }
      if (schema.allOf[0].readOnly && config.includeReadOnly) {
        const tempSchema = schema.allOf[0];
        return { "example-0": getSampleValueByType(tempSchema) };
      }
      return;
    }

    schema.allOf.forEach((v) => {
      if (!v) return;
      if (v.type === "object" || v.properties || v.allOf || v.anyOf || v.oneOf) {
        const partialSamples = schemaToSampleObj(v, config, level + 1);
        const partialVal = partialSamples?.["example-0"] ?? partialSamples;
        if (partialVal && typeof partialVal === "object" && !Array.isArray(partialVal)) {
          Object.assign(mergedObj, partialVal);
        }
      } else if (v.type === "array" || v.items) {
        const partialSamples = schemaToSampleObj(v, config, level + 1);
        const partialVal = partialSamples?.["example-0"] ?? partialSamples;
        if (Array.isArray(partialVal)) {
          mergedObj[Object.keys(mergedObj).length] = partialVal;
        }
      } else if (v.type) {
        const prop = `prop${Object.keys(mergedObj).length}`;
        mergedObj[prop] = getSampleValueByType(v);
      }
    });

    obj = { "example-0": mergedObj };
    addSchemaInfoToExample(schema, obj["example-0"]);
  } else if (schema.oneOf) {
    // 1. First create example with schema.properties
    const objWithSchemaProps = {};
    if (schema.properties) {
      for (const propertyName in schema.properties) {
        const propSchema = schema.properties[propertyName];
        if (propSchema?.properties || propSchema?.items) {
          const propSamples = schemaToSampleObj(propSchema, config, level + 1);
          objWithSchemaProps[propertyName] = propSamples?.["example-0"] ?? propSamples;
        } else {
          objWithSchemaProps[propertyName] = getSampleValueByType(propSchema);
        }
      }
    }

    if (schema.oneOf.length > 0) {
      // If at root (level === 0), expand all oneOf variants as distinct examples.
      // If nested (level > 0), only pick the first oneOf variant to avoid combinatorial duplication.
      const variantsToProcess = level === 0 ? schema.oneOf : [schema.oneOf[0]];
      let i = 0;
      for (let k = 0; k < variantsToProcess.length; k++) {
        const variantSchema = variantsToProcess[k];
        const oneOfSamples = schemaToSampleObj(variantSchema, config, level + 1);
        for (const sampleKey in oneOfSamples) {
          let finalExample = oneOfSamples[sampleKey];
          if (
            Object.keys(objWithSchemaProps).length > 0 &&
            finalExample &&
            typeof finalExample === "object" &&
            !Array.isArray(finalExample)
          ) {
            finalExample = { ...objWithSchemaProps, ...finalExample };
          }
          obj[`example-${i}`] = finalExample;
          addSchemaInfoToExample(variantSchema, obj[`example-${i}`]);
          i++;
          if (level > 0) {
            break;
          }
        }
      }
    }
  } else if (schema.anyOf) {
    // First generate values for regular properties
    let commonObj = { "example-0": {} };
    if (schema.type === "object" || schema.properties) {
      for (const propertyName in schema.properties) {
        const propSchema = schema.properties[propertyName];
        if (propSchema?.deprecated && !config.includeDeprecated) continue;
        if (propSchema?.readOnly && !config.includeReadOnly) continue;
        if (propSchema?.writeOnly && !config.includeWriteOnly) continue;
        const propExample = getFirstExample(propSchema);
        if (propExample !== undefined) {
          commonObj["example-0"][propertyName] = propExample;
        } else {
          mergePropertyExamples(
            commonObj,
            propertyName,
            schemaToSampleObj(propSchema, config, level + 1),
          );
        }
      }
    }

    if (schema.anyOf.length > 0) {
      const variantsToProcess = level === 0 ? schema.anyOf : [schema.anyOf[0]];
      let i = 0;
      for (let k = 0; k < variantsToProcess.length; k++) {
        const variantSchema = variantsToProcess[k];
        const anyOfSamples = schemaToSampleObj(variantSchema, config, level + 1);
        for (const sampleKey in anyOfSamples) {
          const sampleVal = anyOfSamples[sampleKey];
          if (commonObj?.["example-0"] && Object.keys(commonObj["example-0"]).length > 0) {
            obj[`example-${i}`] = {
              ...commonObj["example-0"],
              ...(typeof sampleVal === "object" ? sampleVal : {}),
            };
          } else {
            obj[`example-${i}`] = sampleVal;
          }
          addSchemaInfoToExample(variantSchema, obj[`example-${i}`]);
          i++;
          if (level > 0) {
            break;
          }
        }
      }
    }
  } else if (schema.type === "object" || schema.properties) {
    obj["example-0"] = {};
    addSchemaInfoToExample(schema, obj["example-0"]);
    const firstExample = getFirstExample(schema);
    if (firstExample !== undefined) {
      obj["example-0"] = firstExample;
    } else {
      for (const propertyName in schema.properties) {
        const propSchema = schema.properties[propertyName];
        if (!propSchema) continue;
        if (propSchema.deprecated && !config.includeDeprecated) continue;
        if (propSchema.readOnly && !config.includeReadOnly) continue;
        if (propSchema.writeOnly && !config.includeWriteOnly) continue;

        if (propSchema.type === "array" || propSchema.items) {
          const propExample = getFirstExample(propSchema);
          if (propExample !== undefined) {
            obj["example-0"][propertyName] = propExample;
          } else if (getFirstExample(propSchema.items) !== undefined) {
            obj["example-0"][propertyName] = [getFirstExample(propSchema.items)];
          } else {
            const itemSamples = schemaToSampleObj(propSchema.items, config, level + 1);
            const itemSampleVal = itemSamples?.["example-0"] ?? itemSamples;
            if (config.useXmlTagForProp) {
              const xmlTagName = propSchema.xml?.name || propertyName;
              if (propSchema.xml?.wrapped) {
                obj["example-0"][xmlTagName] = { [xmlTagName]: itemSampleVal };
              } else {
                obj["example-0"][xmlTagName] = [itemSampleVal];
              }
            } else {
              obj["example-0"][propertyName] = [itemSampleVal];
            }
          }
          continue;
        }

        mergePropertyExamples(obj, propertyName, schemaToSampleObj(propSchema, config, level + 1));
      }

      if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
        const propName = schema.additionalProperties["x-additionalPropertiesName"] || "property";
        mergePropertyExamples(
          obj,
          `${propName}1`,
          schemaToSampleObj(schema.additionalProperties, config, level + 1),
        );
        mergePropertyExamples(
          obj,
          `${propName}2`,
          schemaToSampleObj(schema.additionalProperties, config, level + 1),
        );
      }
    }
  } else if (schema.type === "array" || schema.items) {
    const firstExample = getFirstExample(schema);
    if (firstExample !== undefined) {
      obj["example-0"] = firstExample;
    } else if (schema.items) {
      const itemsExample = getFirstExample(schema.items);
      if (itemsExample !== undefined) {
        obj["example-0"] = [itemsExample];
      } else {
        const samples = schemaToSampleObj(schema.items, config, level + 1);
        const itemVal = samples?.["example-0"] ?? samples;
        obj["example-0"] = [itemVal];
        addSchemaInfoToExample(schema.items, obj["example-0"]);
      }
    } else {
      obj["example-0"] = [];
    }
  } else {
    return { "example-0": getSampleValueByType(schema) };
  }
  return obj;
}

function generateMarkdownForArrayAndObjectDescription(schema, level = 0) {
  let mainText = "";
  if (schema.title) {
    if (schema.description) {
      mainText = `<b>${schema.title}:</b> ${schema.description}`;
    } else {
      mainText = schema.title;
    }
  } else if (schema.description) {
    mainText = schema.description;
  }

  const limits = [];
  if (schema.minItems) {
    limits.push(
      `<span class="descr-label">Min Items:</span> <span class="descr-value">${schema.minItems}</span>`,
    );
  }
  if (schema.maxItems) {
    limits.push(
      `<span class="descr-label">Max Items:</span> <span class="descr-value">${schema.maxItems}</span>`,
    );
  }
  if (schema.uniqueItems === true) {
    limits.push(`<span class="descr-label">Must have unique items</span>`);
  }

  const parts = mainText ? [mainText.trim()] : [];
  if (limits.length > 0) {
    // Same wrapper as the constraint chips, so both sit the same distance under the description.
    // Kept on one line so markdown treats it as a single block.
    parts.push(`<div class="item-details">${limits.join(" ")}</div>`);
  }
  if (level > 0 && schema.items?.description) {
    let itemsMarkdown = "";
    if (schema.items.minProperties) {
      itemsMarkdown = `<span class="descr-label">Min Properties:</span> <span class="descr-value">${schema.items.minProperties}</span>`;
    }
    if (schema.items.maxProperties) {
      itemsMarkdown = `${itemsMarkdown} <span class="descr-label">Max Properties:</span> <span class="descr-value">${schema.items.maxProperties}</span>`;
    }
    parts.push(`⮕ ${itemsMarkdown} [ ${schema.items.description} ]`);
  }
  return parts.join("\n\n");
}
/**
 * For changing OpenAPI-Schema to an Object Notation,
 * This Object would further be an input to UI Components to generate an Object-Tree
 * @param {object} schema - Schema object from OpenAPI spec
 * @param {object} obj - recursivly pass this object to generate object notation
 * @param {number} level - recursion level
 * @param {string} suffix - used for suffixing property names to avoid duplicate props during object composion
 */
/**
 * Helper to populate schema properties into a target object notation.
 */
function populateProperties(target, schema, level) {
  if (schema.properties) {
    for (const key in schema.properties) {
      const propKey = schema.required && schema.required.includes(key) ? `${key}*` : key;
      target[propKey] = schemaInObjectNotation(schema.properties[key], {}, level + 1);
    }
  }
  if (schema.patternProperties) {
    for (const key in schema.patternProperties) {
      target[`[pattern: ${key}]`] = schemaInObjectNotation(
        schema.patternProperties[key],
        {},
        level + 1,
      );
    }
  }
  if (schema.additionalProperties) {
    target["[any-key]"] = schemaInObjectNotation(schema.additionalProperties, {}, level + 1);
  }
}

function handleObjectSchema(schema, baseObj = {}, level = 0) {
  const obj = Object.assign({}, baseObj);
  obj["::title"] = schema.title || "";
  obj["::description"] = generateMarkdownForArrayAndObjectDescription(schema, level);
  obj["::type"] = "object";
  if ((Array.isArray(schema.type) && schema.type.includes("null")) || schema.nullable) {
    obj["::dataTypeLabel"] = "object ┃ null";
    obj["::nullable"] = true;
  }
  obj["::deprecated"] = schema.deprecated || false;
  obj["::readwrite"] = schema.readOnly ? "readonly" : schema.writeOnly ? "writeonly" : "";
  populateProperties(obj, schema, level);
  return obj;
}

function handleArraySchema(schema, baseObj = {}, level = 0) {
  const obj = Object.assign({}, baseObj);
  obj["::title"] = schema.title || "";
  obj["::description"] = generateMarkdownForArrayAndObjectDescription(schema, level);
  obj["::type"] = "array";
  if ((Array.isArray(schema.type) && schema.type.includes("null")) || schema.nullable) {
    obj["::dataTypeLabel"] = "array ┃ null";
    obj["::nullable"] = true;
  }
  obj["::deprecated"] = schema.deprecated || false;
  obj["::readwrite"] = schema.readOnly ? "readonly" : schema.writeOnly ? "writeonly" : "";
  if (schema.items?.items) {
    obj["::array-type"] = schema.items.items.type;
  }
  obj["::props"] = schema.items ? schemaInObjectNotation(schema.items, {}, level + 1) : undefined;
  return obj;
}

function handleAllOfSchema(schema, baseObj = {}, level = 0) {
  if (schema.allOf.length === 1 && !schema.allOf[0].properties && !schema.allOf[0].items) {
    // If allOf has single item and the type is not an object or array, then its a primitive
    return `${getTypeInfo(schema.allOf[0]).html}`;
  }

  const objWithAllProps = Object.assign({}, baseObj);
  schema.allOf.forEach((v, i) => {
    if (v.type === "object" || v.properties || v.allOf || v.anyOf || v.oneOf) {
      const propSuffix = (v.anyOf || v.oneOf) && i > 0 ? `${i}` : "";
      const partialObj = schemaInObjectNotation(v, {}, level + 1, propSuffix);
      Object.assign(objWithAllProps, partialObj);
    } else if (v.type === "array" || v.items) {
      const partialObj = schemaInObjectNotation(v, {}, level + 1);
      Object.assign(objWithAllProps, partialObj);
    } else if (v.type) {
      const prop = `prop${Object.keys(objWithAllProps).length}`;
      const typeObj = getTypeInfo(v);
      objWithAllProps[prop] = `${typeObj.html}`;
    }
  });

  return objWithAllProps;
}

function handleAnyOrOneOfSchema(schema, baseObj = {}, level = 0, suffix = "") {
  const obj = Object.assign({}, baseObj);
  obj["::description"] = schema.description || "";

  // 1. First iterate the regular properties if defined
  if (schema.type === "object" || schema.properties) {
    obj["::type"] = "object";
    populateProperties(obj, schema, level);
  }

  // 2. Then build anyOf / oneOf option entries
  const objWithAnyOfProps = {};
  const xxxOf = schema.anyOf ? "anyOf" : "oneOf";
  schema[xxxOf].forEach((v, index) => {
    const optKey = `::OPTION~${index + 1}${v.title ? `~${v.title}` : ""}`;
    if (v.type === "object" || v.properties || v.allOf || v.anyOf || v.oneOf) {
      const partialObj = schemaInObjectNotation(v, {}, level + 1);
      objWithAnyOfProps[optKey] = partialObj;
      if (typeof partialObj === "object" && partialObj !== null) {
        partialObj["::readwrite"] = ""; // xxx-options cannot be read or write only
      }
      objWithAnyOfProps["::type"] = "xxx-of-option";
    } else if (v.type === "array" || v.items) {
      const partialObj = schemaInObjectNotation(v, {}, level + 1);
      objWithAnyOfProps[optKey] = partialObj;
      if (typeof partialObj === "object" && partialObj !== null) {
        partialObj["::readwrite"] = "";
      }
      objWithAnyOfProps["::type"] = "xxx-of-array";
    } else {
      objWithAnyOfProps[optKey] = `${getTypeInfo(v).html}`;
      objWithAnyOfProps["::type"] = "xxx-of-option";
    }
  });

  const operatorKey = schema.anyOf ? "::ANY~OF" : "::ONE~OF";
  const fullKey = suffix ? `${operatorKey} ${suffix}` : operatorKey;
  obj[fullKey] = objWithAnyOfProps;
  obj["::type"] = "object";

  return obj;
}

function handleMultiTypeSchema(schema, baseObj = {}, level = 0) {
  // Recognize OpenAPI 3.1 nullable object and array
  if (schema.type.length === 2 && schema.type.includes("null")) {
    if (schema.type.includes("object")) {
      return handleObjectSchema(schema, baseObj, level);
    }
    if (schema.type.includes("array")) {
      return handleArraySchema(schema, baseObj, level);
    }
  }

  const subSchema =
    typeof structuredClone === "function"
      ? structuredClone(schema)
      : JSON.parse(JSON.stringify(schema));
  const primitiveType = [];
  const complexTypes = [];

  subSchema.type.forEach((v) => {
    if (v.match(/integer|number|string|null|boolean/g)) {
      primitiveType.push(v);
    } else if (
      v === "array" &&
      typeof subSchema.items?.type === "string" &&
      subSchema.items?.type.match(/integer|number|string|null|boolean/g)
    ) {
      if (
        subSchema.items.type === "string" &&
        (subSchema.items.format || subSchema.items.contentMediaType)
      ) {
        primitiveType.push(`[${subSchema.items.format || subSchema.items.contentMediaType}]`);
      } else {
        primitiveType.push(`[${subSchema.items.type}]`);
      }
    } else {
      complexTypes.push(v);
    }
  });

  let multiPrimitiveTypes;
  if (primitiveType.length > 0) {
    subSchema.type = primitiveType.join("┃");
    multiPrimitiveTypes = getTypeInfo(subSchema);
    if (complexTypes.length === 0) {
      return `${multiPrimitiveTypes?.html || ""}`;
    }
  }

  if (complexTypes.length > 0) {
    const obj = Object.assign({}, baseObj);
    obj["::type"] = "object";
    const multiTypeOptions = {
      "::type": "xxx-of-option",
    };

    let optionIndex = 1;
    complexTypes.forEach((v) => {
      if (v === "object") {
        const objTypeOption = {
          "::title": schema.title || "",
          "::description": schema.description || "",
          "::type": "object",
          "::deprecated": schema.deprecated || false,
        };
        populateProperties(objTypeOption, schema, level);
        multiTypeOptions[`::OPTION~${optionIndex++}`] = objTypeOption;
      } else if (v === "array") {
        multiTypeOptions[`::OPTION~${optionIndex++}`] = {
          "::title": schema.title || "",
          "::description": schema.description || "",
          "::type": "array",
          "::props": schemaInObjectNotation(schema.items, {}, level + 1),
        };
      }
    });

    if (primitiveType.length > 0 && multiPrimitiveTypes?.html) {
      multiTypeOptions[`::OPTION~${optionIndex++}`] = multiPrimitiveTypes.html;
    }

    obj["::ONE~OF"] = multiTypeOptions;
    return obj;
  }

  return Object.assign({}, baseObj);
}

function createObjectAST(schema, level, name, isRequired) {
  const isNullable =
    schema.nullable || (Array.isArray(schema.type) && schema.type.includes("null")) || false;
  const properties = [];
  if (schema.properties) {
    for (const key in schema.properties) {
      const isPropReq = Array.isArray(schema.required) && schema.required.includes(key);
      const child = schemaToAST(schema.properties[key], level + 1, key, isPropReq);
      if (child) properties.push(child);
    }
  }

  const patternProperties = [];
  if (schema.patternProperties) {
    for (const key in schema.patternProperties) {
      const child = schemaToAST(
        schema.patternProperties[key],
        level + 1,
        `[pattern: ${key}]`,
        false,
      );
      if (child) patternProperties.push(child);
    }
  }

  let additionalProperties = null;
  if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
    additionalProperties = schemaToAST(schema.additionalProperties, level + 1, "[any-key]", false);
  }

  return {
    kind: "object",
    name,
    title: schema.title || "",
    description: generateMarkdownForArrayAndObjectDescription(schema, level),
    required: isRequired,
    deprecated: schema.deprecated || false,
    readOnly: schema.readOnly || false,
    writeOnly: schema.writeOnly || false,
    nullable: isNullable,
    dataTypeLabel: isNullable ? "object ┃ null" : "object",
    properties,
    patternProperties,
    additionalProperties,
  };
}

function createArrayAST(schema, level, name, isRequired) {
  const isNullable =
    schema.nullable || (Array.isArray(schema.type) && schema.type.includes("null")) || false;
  let itemsNode = null;
  if (schema.items) {
    itemsNode = schemaToAST(schema.items, level + 1, "", false);
  }

  let arrayType = "";
  if (schema.items?.items) {
    arrayType = schema.items.items.type || "";
  } else if (schema.items?.type && typeof schema.items.type === "string") {
    arrayType = schema.items.type;
  }

  return {
    kind: "array",
    name,
    title: schema.title || "",
    description: generateMarkdownForArrayAndObjectDescription(schema, level),
    required: isRequired,
    deprecated: schema.deprecated || false,
    readOnly: schema.readOnly || false,
    writeOnly: schema.writeOnly || false,
    nullable: isNullable,
    dataTypeLabel: isNullable ? "array ┃ null" : "array",
    arrayType,
    minItems: schema.minItems,
    maxItems: schema.maxItems,
    uniqueItems: schema.uniqueItems || false,
    items: itemsNode,
  };
}

function createPrimitiveAST(schema, name, isRequired) {
  const typeInfo = getTypeInfo(schema) || {};
  return {
    kind: "primitive",
    name,
    type: typeInfo.type || schema.type || "",
    format: typeInfo.format || schema.format || "",
    contentMediaType: typeInfo.contentMediaType || schema.contentMediaType || "",
    contentEncoding: typeInfo.contentEncoding || schema.contentEncoding || "",
    contentSchema: typeInfo.contentSchema || schema.contentSchema || null,
    pattern: typeInfo.pattern || schema.pattern || "",
    constraints: typeInfo.constrain || "",
    defaultValue:
      typeInfo.default !== undefined && typeInfo.default !== "" ? String(typeInfo.default) : "",
    allowedValues: typeInfo.allowedValues || "",
    description: (schema.description || typeInfo.description || "").trim(),
    title: (schema.title || "").trim(),
    required: isRequired,
    deprecated: schema.deprecated || !!typeInfo.deprecated,
    readOnly: schema.readOnly || typeInfo.readOrWriteOnly === "🆁",
    writeOnly: schema.writeOnly || typeInfo.readOrWriteOnly === "🆆",
    html: typeInfo.html || "",
  };
}

// Deepest JSON nesting level expanded; anything below an object or array at this level is truncated
const MAX_SCHEMA_DEPTH = 8;
// Largest tree built for one schema. A bigger tree (e.g. Stripe's expandable fields) is rebuilt one level shallower,
// down to MIN_SCHEMA_DEPTH, so the page stays responsive
const SCHEMA_NODE_BUDGET = 8000;
const MIN_SCHEMA_DEPTH = 4;

/**
 * Whether a schema opens a new JSON nesting level (an object or array), directly or through allOf.
 * Primitives and unions are not containers: primitives cost nothing to show, and union options are checked one by one.
 * @param {object} schema - OpenAPI/JSON Schema object
 * @returns {boolean}
 */
function isContainerSchema(schema) {
  const types = Array.isArray(schema.type) ? schema.type : [schema.type];
  return (
    types.includes("object") ||
    types.includes("array") ||
    !!(schema.properties || schema.patternProperties || schema.items) ||
    (!!schema.additionalProperties && typeof schema.additionalProperties === "object") ||
    !!schema.allOf?.some((sub) => sub && isContainerSchema(sub))
  );
}

// State of the schemaToAST() call in progress (conversion is synchronous)
// Schemas on the current path, to stop on cycles that only go through allOf/anyOf/oneOf
const schemasInProgress = new Set();
let maxDepth = MAX_SCHEMA_DEPTH;
let nodeCount = 0;

/**
 * Transforms an OpenAPI Schema into a strongly-typed Abstract Syntax Tree (AST) node.
 *
 * @param {object} schema - OpenAPI/JSON Schema object
 * @param {number} [level=0] - JSON nesting depth: grows through properties, items and additionalProperties, not through allOf/anyOf/oneOf
 * @param {string} [name=''] - Property or field name
 * @param {boolean} [isRequired=false] - Whether this property is required
 * @param {string} [suffix=''] - Suffix used for union composition
 * @returns {object|null} Typed SchemaNode AST
 */
export function schemaToAST(schema, level = 0, name = "", isRequired = false, suffix = "") {
  if (!schema) {
    return null;
  }
  if (schemasInProgress.size > 0) {
    return nodeToAST(schema, level, name, isRequired, suffix);
  }
  // Top-level call: expand as deep as allowed while the tree fits the budget
  for (maxDepth = MAX_SCHEMA_DEPTH; ; maxDepth -= 1) {
    nodeCount = 0;
    const ast = nodeToAST(schema, level, name, isRequired, suffix);
    if (nodeCount <= SCHEMA_NODE_BUDGET || maxDepth <= MIN_SCHEMA_DEPTH) {
      return ast;
    }
  }
}

function nodeToAST(schema, level, name, isRequired, suffix) {
  if (!schema) {
    return null;
  }
  nodeCount += 1;
  // Once over budget this attempt is discarded, so stop expanding to get to the next one quickly
  const overBudget = nodeCount > SCHEMA_NODE_BUDGET && maxDepth > MIN_SCHEMA_DEPTH;
  if (
    schemasInProgress.has(schema) ||
    ((level > maxDepth || overBudget) && isContainerSchema(schema))
  ) {
    const isArray =
      schema.type === "array" ||
      (Array.isArray(schema.type) && schema.type.includes("array")) ||
      !!schema.items;
    return {
      kind: isArray ? "array" : "object",
      name,
      type: isArray ? "array" : "object",
      title: schema.title || "",
      description: schema.description || "",
      required: isRequired,
      truncated: true,
    };
  }
  schemasInProgress.add(schema);
  try {
    return composeSchemaAST(schema, level, name, isRequired, suffix);
  } finally {
    schemasInProgress.delete(schema);
  }
}

function composeSchemaAST(schema, level, name, isRequired, suffix) {
  // 1. allOf
  if (schema.allOf) {
    if (schema.allOf.length === 1 && !schema.allOf[0].properties && !schema.allOf[0].items) {
      const mergedSchema = {
        ...schema.allOf[0],
        ...schema,
        title: schema.title || schema.allOf[0].title || "",
        description: schema.description || schema.allOf[0].description || "",
      };
      delete mergedSchema.allOf;
      return schemaToAST(mergedSchema, level, name, isRequired);
    }

    const mergedProps = [];
    const mergedPatternProps = [];
    let mergedAdditionalProps = null;
    const unionChildren = [];
    let mergedTitle = schema.title || "";
    let mergedDescription = schema.description || "";
    let mergedDeprecated = schema.deprecated || false;
    let mergedReadOnly = schema.readOnly || false;
    let mergedWriteOnly = schema.writeOnly || false;
    let mergedNullable = schema.nullable || false;

    schema.allOf.forEach((sub, i) => {
      if (sub.title && !mergedTitle) mergedTitle = sub.title;
      if (sub.description && !mergedDescription) mergedDescription = sub.description;
      if (sub.deprecated) mergedDeprecated = true;
      if (sub.readOnly) mergedReadOnly = true;
      if (sub.writeOnly) mergedWriteOnly = true;
      if (sub.nullable) mergedNullable = true;

      if (sub.type === "object" || sub.properties || sub.allOf || sub.anyOf || sub.oneOf) {
        const subSuffix = (sub.anyOf || sub.oneOf) && i > 0 ? `${i}` : "";
        const subAst = schemaToAST(sub, level, "", false, subSuffix);
        if (subAst) {
          if (subAst.kind === "object") {
            if (subAst.properties) mergedProps.push(...subAst.properties);
            if (subAst.patternProperties) mergedPatternProps.push(...subAst.patternProperties);
            if (subAst.additionalProperties) mergedAdditionalProps = subAst.additionalProperties;
          } else if (subAst.kind === "union") {
            unionChildren.push(subAst);
          }
        }
      } else if (sub.type === "array" || sub.items) {
        const subAst = schemaToAST(sub, level);
        if (subAst) {
          mergedProps.push(subAst);
        }
      } else if (sub.type) {
        const propName = `prop${mergedProps.length}`;
        mergedProps.push(schemaToAST(sub, level, propName, false));
      }
    });

    return {
      kind: "object",
      name,
      title: mergedTitle,
      description: generateMarkdownForArrayAndObjectDescription(
        { ...schema, title: mergedTitle, description: mergedDescription },
        level,
      ),
      required: isRequired,
      deprecated: mergedDeprecated,
      readOnly: mergedReadOnly,
      writeOnly: mergedWriteOnly,
      nullable: mergedNullable,
      dataTypeLabel: mergedNullable ? "object ┃ null" : "object",
      properties: mergedProps,
      patternProperties: mergedPatternProps,
      additionalProperties: mergedAdditionalProps,
      unions: unionChildren,
    };
  }

  // 2. anyOf / oneOf
  if (schema.anyOf || schema.oneOf) {
    const operator = schema.anyOf ? "anyOf" : "oneOf";
    const rawOptions = schema.anyOf || schema.oneOf;
    const options = rawOptions
      .map((opt, idx) => {
        const optTitle = opt.title || "";
        const optAst = schemaToAST(opt, level, optTitle || `Option ${idx + 1}`, false);
        if (optAst) {
          optAst.readOnly = false;
          optAst.writeOnly = false;
          optAst.optionIndex = idx + 1;
          optAst.optionTitle = optTitle;
          if (optAst.kind === "primitive" && !optTitle) {
            // The OPT n badge already names it, a row labelled "Option n" would only repeat it
            optAst.name = "";
          }
        }
        return optAst;
      })
      .filter(Boolean);

    let baseProps = [];
    if (schema.type === "object" || schema.properties) {
      if (schema.properties) {
        for (const key in schema.properties) {
          const isReq = Array.isArray(schema.required) && schema.required.includes(key);
          const propNode = schemaToAST(schema.properties[key], level + 1, key, isReq);
          if (propNode) baseProps.push(propNode);
        }
      }
    }

    return {
      kind: "union",
      operator,
      name,
      suffix,
      title: schema.title || "",
      description: schema.description || "",
      required: isRequired,
      properties: baseProps,
      options,
    };
  }

  // 3. Multi-type array (OpenAPI 3.1)
  if (Array.isArray(schema.type)) {
    if (schema.type.length === 2 && schema.type.includes("null")) {
      if (schema.type.includes("object")) {
        return createObjectAST(schema, level, name, isRequired);
      }
      if (schema.type.includes("array")) {
        return createArrayAST(schema, level, name, isRequired);
      }
    }

    const subSchema =
      typeof structuredClone === "function"
        ? structuredClone(schema)
        : JSON.parse(JSON.stringify(schema));
    const primitiveType = [];
    const complexTypes = [];

    subSchema.type.forEach((v) => {
      if (v.match(/integer|number|string|null|boolean/g)) {
        primitiveType.push(v);
      } else if (
        v === "array" &&
        typeof subSchema.items?.type === "string" &&
        subSchema.items?.type.match(/integer|number|string|null|boolean/g)
      ) {
        if (
          subSchema.items.type === "string" &&
          (subSchema.items.format || subSchema.items.contentMediaType)
        ) {
          primitiveType.push(`[${subSchema.items.format || subSchema.items.contentMediaType}]`);
        } else {
          primitiveType.push(`[${subSchema.items.type}]`);
        }
      } else {
        complexTypes.push(v);
      }
    });

    if (complexTypes.length === 0 && primitiveType.length > 0) {
      subSchema.type = primitiveType.join("┃");
      return createPrimitiveAST(subSchema, name, isRequired);
    }

    if (complexTypes.length > 0) {
      const options = [];
      let optionIndex = 1;
      complexTypes.forEach((v) => {
        if (v === "object") {
          const objNode = createObjectAST(
            schema,
            level,
            schema.title || `Option ${optionIndex}`,
            false,
          );
          objNode.optionIndex = optionIndex++;
          objNode.optionTitle = schema.title || "";
          options.push(objNode);
        } else if (v === "array") {
          const arrNode = createArrayAST(
            schema,
            level,
            schema.title || `Option ${optionIndex}`,
            false,
          );
          arrNode.optionIndex = optionIndex++;
          arrNode.optionTitle = schema.title || "";
          options.push(arrNode);
        }
      });

      if (primitiveType.length > 0) {
        subSchema.type = primitiveType.join("┃");
        const primNode = createPrimitiveAST(subSchema, `Option ${optionIndex}`, false);
        primNode.optionIndex = optionIndex++;
        options.push(primNode);
      }

      return {
        kind: "union",
        operator: "oneOf",
        name,
        title: schema.title || "",
        description: schema.description || "",
        required: isRequired,
        options,
      };
    }
  }

  // 4. Object
  if (schema.type === "object" || schema.properties) {
    return createObjectAST(schema, level, name, isRequired);
  }

  // 5. Array
  if (schema.type === "array" || schema.items) {
    return createArrayAST(schema, level, name, isRequired);
  }

  // 6. Primitive
  return createPrimitiveAST(schema, name, isRequired);
}

/**
 * For changing OpenAPI-Schema to an Object Notation,
 * This Object would further be an input to UI Components to generate an Object-Tree
 * @deprecated Use schemaToAST instead.
 * @param {object} schema - Schema object from OpenAPI spec
 * @param {object} [obj={}] - base object to populate (defaults to new object)
 * @param {number} [level=0] - recursion level
 * @param {string} [suffix=''] - used for suffixing property names to avoid duplicate props during object composition
 */
export function schemaInObjectNotation(schema, obj = {}, level = 0, suffix = "") {
  if (!schema) {
    return;
  }
  if (level > 8) {
    return {
      ...obj,
      "::type": schema.type || "object",
      "::description": schema.description || "",
    };
  }
  if (schema.allOf) {
    return handleAllOfSchema(schema, obj, level);
  }
  if (schema.anyOf || schema.oneOf) {
    return handleAnyOrOneOfSchema(schema, obj, level, suffix);
  }
  if (Array.isArray(schema.type)) {
    return handleMultiTypeSchema(schema, obj, level);
  }
  if (schema.type === "object" || schema.properties) {
    return handleObjectSchema(schema, obj, level);
  }
  if (schema.type === "array" || schema.items) {
    return handleArraySchema(schema, obj, level);
  }
  const typeObj = getTypeInfo(schema);
  if (typeObj?.html) {
    return `${typeObj.html}`;
  }
  return "";
}

/**
 * Helper to consistently format raw example values according to mimeType and outputType.
 */
function formatExampleContent(rawVal, mimeType = "", outputType = "json") {
  let content = rawVal;
  let format = "text";

  if (mimeType?.toLowerCase().includes("json")) {
    if (outputType === "text") {
      content = typeof rawVal === "string" ? rawVal : JSON.stringify(rawVal, undefined, 2);
      format = "text";
    } else if (typeof rawVal === "object" && rawVal !== null) {
      content = rawVal;
      format = "json";
    } else if (typeof rawVal === "string") {
      try {
        content = JSON.parse(rawVal);
        format = "json";
      } catch {
        content = rawVal;
        format = "text";
      }
    } else {
      content = rawVal;
      format = "json";
    }
  } else {
    content =
      typeof rawVal === "object" && rawVal !== null
        ? JSON.stringify(rawVal, undefined, 2)
        : String(rawVal ?? "");
    format = "text";
  }

  return { content, format };
}

/* Create Example object */
export function generateExample(
  schema,
  mimeType = "",
  examples = null,
  example = null,
  includeReadOnly = true,
  includeWriteOnly = true,
  outputType = "json",
  includeGeneratedExample = false,
) {
  const finalExamples = [];
  const isJson = mimeType?.toLowerCase().includes("json");
  const isXml = mimeType?.toLowerCase().includes("xml");

  // 1. Process multiple examples (examples map)
  if (examples && typeof examples === "object" && Object.keys(examples).length > 0) {
    for (const eg in examples) {
      const rawVal = examples[eg]?.value !== undefined ? examples[eg].value : examples[eg];
      const { content, format } = formatExampleContent(rawVal, mimeType, outputType);

      finalExamples.push({
        exampleId: eg,
        exampleSummary: examples[eg]?.summary || eg,
        exampleDescription: examples[eg]?.description || "",
        exampleType: mimeType,
        exampleValue: content,
        exampleFormat: format,
      });
    }
  }
  // 2. Process single example (when examples is absent or empty)
  else if (
    example !== null &&
    example !== undefined &&
    (typeof example !== "object" || Object.keys(example).length > 0)
  ) {
    const rawVal = example?.value !== undefined ? example.value : example;
    const { content, format } = formatExampleContent(rawVal, mimeType, outputType);

    finalExamples.push({
      exampleId: "Example",
      exampleSummary: example?.summary || "",
      exampleDescription: example?.description || "",
      exampleType: mimeType,
      exampleValue: content,
      exampleFormat: format,
    });
  }

  // 3. Fallback to schema-level example or schema-generated sample
  if (finalExamples.length === 0 || includeGeneratedExample === true) {
    if (schema) {
      const firstExample = getFirstExample(schema);
      if (firstExample !== undefined && firstExample !== null) {
        const { content, format } = formatExampleContent(firstExample, mimeType, outputType);
        finalExamples.push({
          exampleId: "Example",
          exampleSummary: "",
          exampleDescription: "",
          exampleType: mimeType,
          exampleValue: content,
          exampleFormat: format,
        });
      } else if (
        isJson ||
        isXml ||
        mimeType?.toLowerCase().includes("text") ||
        mimeType?.toLowerCase().includes("yaml") ||
        mimeType?.toLowerCase().includes("*/*")
      ) {
        let xmlRootStart = "";
        let xmlRootEnd = "";
        let exampleFormat = outputType;

        if (isXml) {
          xmlRootStart = schema.xml?.name
            ? `<${schema.xml.name}${schema.xml.namespace ? ` xmlns="${schema.xml.namespace}"` : ""}>`
            : "<root>";
          xmlRootEnd = schema.xml?.name ? `</${schema.xml.name}>` : "</root>";
          exampleFormat = "text";
        }

        const samples = schemaToSampleObj(schema, {
          includeReadOnly,
          includeWriteOnly,
          deprecated: true,
          useXmlTagForProp: isXml,
        });

        let i = 0;
        for (const samplesKey in samples) {
          if (!samples[samplesKey]) {
            continue;
          }
          const summary = samples[samplesKey]["::TITLE"] || `Example ${++i}`;
          const description = samples[samplesKey]["::DESCRIPTION"] || "";
          let exampleValue = "";

          if (isXml) {
            exampleValue = `<?xml version="1.0" encoding="UTF-8"?>\n${xmlRootStart}${json2xml(samples[samplesKey], 1)}\n${xmlRootEnd}`;
          } else {
            removeTitlesAndDescriptions(samples[samplesKey]);
            exampleValue =
              outputType === "text"
                ? JSON.stringify(samples[samplesKey], null, 2)
                : samples[samplesKey];
          }

          finalExamples.push({
            exampleId: samplesKey,
            exampleSummary: summary,
            exampleDescription: description,
            exampleType: mimeType,
            exampleFormat,
            exampleValue,
          });
        }
      } else if (mimeType?.toLowerCase().includes("jose")) {
        finalExamples.push({
          exampleId: "Example",
          exampleSummary: "Base64 Encoded",
          exampleDescription: "",
          exampleType: mimeType,
          exampleValue: schema.pattern || "bXJpbg==",
          exampleFormat: "text",
        });
      } else {
        finalExamples.push({
          exampleId: "Example",
          exampleSummary: "",
          exampleDescription: "",
          exampleType: mimeType,
          exampleValue: "",
          exampleFormat: "text",
        });
      }
    } else {
      // No Example or Schema provided
      finalExamples.push({
        exampleId: "Example",
        exampleSummary: "",
        exampleDescription: "",
        exampleType: mimeType,
        exampleValue: "",
        exampleFormat: "text",
      });
    }
  }

  return finalExamples;
}

function getSerializeStyleForContentType(contentType) {
  if (contentType === "application/json") {
    return "json";
  }
  if (contentType === "application/xml") {
    return "xml";
  }
  return null;
}

/**
 * Text that names an example in an examples dropdown: its summary, or its key when it has none.
 * A long summary is shown whole; the dropdown wraps it, or cuts it where the browser's select can't wrap.
 */
export function exampleLabel(example) {
  return example?.exampleSummary || example?.exampleId || "";
}

export function getSchemaFromParam(param) {
  if (param.schema) {
    return [param.schema, null, null];
  }
  if (param.content) {
    // we gonna use the first content-encoding
    for (const contentType of Object.keys(param.content)) {
      if (param.content[contentType].schema) {
        return [
          param.content[contentType].schema,
          getSerializeStyleForContentType(contentType),
          param.content[contentType],
        ];
      }
    }
  }
  return [null, null, null];
}

export const EMPTY_ARRAY = Object.freeze([]);

export function getMergedProperties(schema, visited = new Set()) {
  if (!schema || typeof schema !== "object" || visited.has(schema)) {
    return {};
  }
  visited.add(schema);
  const properties = { ...schema.properties };
  if (Array.isArray(schema.allOf)) {
    schema.allOf.forEach((sub) => {
      Object.assign(properties, getMergedProperties(sub, visited));
    });
  }
  return properties;
}

export function getMergedRequired(schema, visited = new Set()) {
  if (!schema || typeof schema !== "object" || visited.has(schema)) {
    return [];
  }
  visited.add(schema);
  const required = Array.isArray(schema.required) ? [...schema.required] : [];
  if (Array.isArray(schema.allOf)) {
    schema.allOf.forEach((sub) => {
      required.push(...getMergedRequired(sub, visited));
    });
  }
  return [...new Set(required)];
}