/* ============================================================================ * Copyright (c) Palo Alto Networks * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * ========================================================================== */ import React, { useCallback } from "react"; import { translate } from "@docusaurus/Translate"; import { setSchemaSelection } from "@theme/ApiExplorer/SchemaSelection/slice"; import { useTypedDispatch } from "@theme/ApiItem/hooks"; import { ClosingArrayBracket, OpeningArrayBracket } from "@theme/ArrayBrackets"; import Details from "@theme/Details"; import DiscriminatorTabs from "@theme/DiscriminatorTabs"; import Markdown from "@theme/Markdown"; import SchemaItem from "@theme/SchemaItem"; import SchemaTabs from "@theme/SchemaTabs"; import TabItem from "@theme/TabItem"; import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds"; // eslint-disable-next-line import/no-extraneous-dependencies import { merge } from "allof-merge"; import clsx from "clsx"; import isEmpty from "lodash/isEmpty"; import { getQualifierMessage, getSchemaName } from "../../markdown/schema"; import type { SchemaObject } from "../../types.d"; // eslint-disable-next-line import/no-extraneous-dependencies // const jsonSchemaMergeAllOf = require("json-schema-merge-allof"); const mergeAllOf = (allOf: any) => { const onMergeError = (msg: string) => { console.warn(msg); }; const mergedSchemas = merge(allOf, { onMergeError }); return mergedSchemas ?? {}; }; /** * Recursively searches for a property in a schema, including nested * oneOf, anyOf, and allOf structures. This is needed for discriminators * where the property definition may be in a nested schema. */ const findProperty = ( schema: SchemaObject, propertyName: string ): SchemaObject | undefined => { // Check direct properties first if (schema.properties?.[propertyName]) { return schema.properties[propertyName]; } // Search in oneOf schemas if (schema.oneOf) { for (const subschema of schema.oneOf) { const found = findProperty(subschema as SchemaObject, propertyName); if (found) return found; } } // Search in anyOf schemas if (schema.anyOf) { for (const subschema of schema.anyOf) { const found = findProperty(subschema as SchemaObject, propertyName); if (found) return found; } } // Search in allOf schemas if (schema.allOf) { for (const subschema of schema.allOf) { const found = findProperty(subschema as SchemaObject, propertyName); if (found) return found; } } return undefined; }; /** * Recursively searches for a discriminator in a schema, including nested * oneOf, anyOf, and allOf structures. */ const findDiscriminator = (schema: SchemaObject): any | undefined => { if (schema.discriminator) { return schema.discriminator; } if (schema.oneOf) { for (const subschema of schema.oneOf) { const found = findDiscriminator(subschema as SchemaObject); if (found) return found; } } if (schema.anyOf) { for (const subschema of schema.anyOf) { const found = findDiscriminator(subschema as SchemaObject); if (found) return found; } } if (schema.allOf) { for (const subschema of schema.allOf) { const found = findDiscriminator(subschema as SchemaObject); if (found) return found; } } return undefined; }; interface MarkdownProps { text: string | undefined; } // Renders string as markdown, useful for descriptions and qualifiers const MarkdownWrapper: React.FC = ({ text }) => { return (
{text}
); }; interface SummaryProps { name: string; schemaName: string | undefined; schema: { deprecated?: boolean; nullable?: boolean; }; required?: boolean | string[]; } const Summary: React.FC = ({ name, schemaName, schema, required, }) => { const { deprecated, nullable } = schema; const isRequired = Array.isArray(required) ? required.includes(name) : required === true; return ( {name} {schemaName} {(isRequired || deprecated || nullable) && ( )} {nullable && ( {translate({ id: OPENAPI_SCHEMA_ITEM.NULLABLE, message: "nullable", })} )} {isRequired && ( {translate({ id: OPENAPI_SCHEMA_ITEM.REQUIRED, message: "required", })} )} {deprecated && ( {translate({ id: OPENAPI_SCHEMA_ITEM.DEPRECATED, message: "deprecated", })} )} ); }; // Common props interface interface SchemaProps { schema: SchemaObject; schemaType: "request" | "response"; /** * Optional path identifier for tracking anyOf/oneOf selections. * When provided, tab selections will be dispatched to Redux state * to enable dynamic body example updates. */ schemaPath?: string; } const AnyOneOf: React.FC = ({ schema, schemaType, schemaPath, }) => { const key = schema.oneOf ? "oneOf" : "anyOf"; const schemaArray = schema[key]; // Empty oneOf/anyOf arrays are valid in OpenAPI specs but would cause the // Tabs component to throw "requires at least one TabItem". Return null instead. if (!schemaArray || !Array.isArray(schemaArray) || schemaArray.length === 0) { return null; } const type = schema.oneOf ? translate({ id: OPENAPI_SCHEMA_ITEM.ONE_OF, message: "oneOf" }) : translate({ id: OPENAPI_SCHEMA_ITEM.ANY_OF, message: "anyOf" }); // Generate a unique ID for this anyOf/oneOf to prevent tab value collisions const uniqueId = React.useMemo( () => Math.random().toString(36).substring(7), [] ); // Try to get Redux dispatch - will be undefined if not inside a Provider let dispatch: ReturnType | undefined; try { // eslint-disable-next-line react-hooks/rules-of-hooks dispatch = useTypedDispatch(); } catch { // Not inside a Redux Provider, which is fine for response schemas dispatch = undefined; } // Handle tab change - dispatch to Redux if schemaPath is provided const handleTabChange = useCallback( (index: number) => { if (schemaPath && dispatch) { dispatch(setSchemaSelection({ path: schemaPath, index })); } }, [schemaPath, dispatch] ); return ( <> {type} {schema[key]?.map((anyOneSchema: any, index: number) => { // Use getSchemaName to include format info (e.g., "string") const computedSchemaName = getSchemaName(anyOneSchema); // Determine label for the tab // Prefer explicit title, then computed schema name, then raw type let label = anyOneSchema.title || computedSchemaName || anyOneSchema.type; if (!label) { if (anyOneSchema.oneOf) { label = translate({ id: OPENAPI_SCHEMA_ITEM.ONE_OF, message: "oneOf", }); } else if (anyOneSchema.anyOf) { label = translate({ id: OPENAPI_SCHEMA_ITEM.ANY_OF, message: "anyOf", }); } else { label = `Option ${index + 1}`; } } // Build the nested schemaPath for child anyOf/oneOf const childSchemaPath = schemaPath ? `${schemaPath}.${index}` : undefined; return ( // @ts-ignore {/* Handle primitive types directly */} {(isPrimitive(anyOneSchema) || anyOneSchema.const) && ( )} {/* Handle empty object as a primitive type */} {anyOneSchema.type === "object" && !anyOneSchema.properties && !anyOneSchema.allOf && !anyOneSchema.oneOf && !anyOneSchema.anyOf && ( )} {/* Handle actual object types with properties or nested schemas */} {/* Note: In OpenAPI, properties implies type: object even if not explicitly set */} {(anyOneSchema.type === "object" || !anyOneSchema.type) && anyOneSchema.properties && ( )} {anyOneSchema.allOf && ( )} {anyOneSchema.oneOf && ( )} {anyOneSchema.anyOf && ( )} {anyOneSchema.items && ( )} ); })} ); }; const Properties: React.FC = ({ schema, schemaType, schemaPath, }) => { const discriminator = schema.discriminator; if (discriminator && !discriminator.mapping) { const anyOneOf = schema.oneOf ?? schema.anyOf ?? {}; const inferredMapping = {} as any; Object.entries(anyOneOf).map(([_, anyOneSchema]: [string, any]) => { // ensure discriminated property only renders once if ( schema.properties![discriminator.propertyName] && anyOneSchema.properties[discriminator.propertyName] ) delete anyOneSchema.properties[discriminator.propertyName]; return (inferredMapping[anyOneSchema.title] = anyOneSchema); }); discriminator["mapping"] = inferredMapping; } if (Object.keys(schema.properties as {}).length === 0) { // Hide placeholder only for discriminator cleanup artifacts; preserve // empty object rendering for schemas that intentionally define no properties. if (discriminator) { return null; } return ( ); } return ( <> {Object.entries(schema.properties as {}).map( ([key, val]: [string, any]) => ( ) )} ); }; const PropertyDiscriminator: React.FC = ({ name, schemaName, schema, schemaType, discriminator, required, }) => { if (!schema) { return null; } return ( <>
{name} {schemaName && ( {schemaName} )} {required && } {required && ( {translate({ id: OPENAPI_SCHEMA_ITEM.REQUIRED, message: "required", })} )}
{schema.description && ( )} {getQualifierMessage(discriminator) && ( )}
{Object.keys(discriminator.mapping).map((key, index) => ( // @ts-ignore ))}
{schema.properties && Object.entries(schema.properties as {}).map( ([key, val]: [string, any]) => key !== discriminator.propertyName && ( ) )} ); }; interface DiscriminatorNodeProps { discriminator: any; schema: SchemaObject; schemaType: "request" | "response"; } const DiscriminatorNode: React.FC = ({ discriminator, schema, schemaType, }) => { let discriminatedSchemas: any = {}; let inferredMapping: any = {}; // Search for the discriminator property in the schema, including nested structures const discriminatorProperty = findProperty(schema, discriminator.propertyName) ?? {}; if (schema.allOf) { const mergedSchemas = mergeAllOf(schema) as SchemaObject; if (mergedSchemas.oneOf || mergedSchemas.anyOf) { discriminatedSchemas = mergedSchemas.oneOf || mergedSchemas.anyOf; } } else if (schema.oneOf || schema.anyOf) { discriminatedSchemas = schema.oneOf || schema.anyOf; } // Handle case where no mapping is defined if (!discriminator.mapping) { Object.entries(discriminatedSchemas).forEach( ([_, subschema]: [string, any], index) => { inferredMapping[subschema.title ?? `PROP${index}`] = subschema; } ); discriminator.mapping = inferredMapping; } // Merge sub schema discriminator property with parent Object.keys(discriminator.mapping).forEach((key) => { const subSchema = discriminator.mapping[key]; // Handle discriminated schema with allOf let mergedSubSchema = {} as SchemaObject; if (subSchema.allOf) { mergedSubSchema = mergeAllOf(subSchema) as SchemaObject; } const subProperties = subSchema.properties || mergedSubSchema.properties; // Add a safeguard check to avoid referencing subProperties if it's undefined if (subProperties && subProperties[discriminator.propertyName]) { if (schema.properties) { schema.properties![discriminator.propertyName] = { ...schema.properties![discriminator.propertyName], ...subProperties[discriminator.propertyName], }; if (subSchema.required && !schema.required) { schema.required = subSchema.required; } // Avoid duplicating property delete subProperties[discriminator.propertyName]; } else { schema.properties = {}; schema.properties[discriminator.propertyName] = subProperties[discriminator.propertyName]; // Avoid duplicating property delete subProperties[discriminator.propertyName]; } } }); const name = discriminator.propertyName; const schemaName = getSchemaName(discriminatorProperty); // Default case for discriminator without oneOf/anyOf/allOf return ( ); }; const AdditionalProperties: React.FC = ({ schema, schemaType, }) => { const additionalProperties = schema.additionalProperties; if (!additionalProperties) return null; // Handle free-form objects if (additionalProperties === true || isEmpty(additionalProperties)) { return ( ); } // Handle objects, arrays, complex schemas if ( additionalProperties.properties || additionalProperties.items || additionalProperties.allOf || additionalProperties.additionalProperties || additionalProperties.oneOf || additionalProperties.anyOf ) { const title = additionalProperties.title || getSchemaName(additionalProperties); const required = schema.required || false; return ( ); } // Handle primitive types if ( additionalProperties.type === "string" || additionalProperties.type === "boolean" || additionalProperties.type === "integer" || additionalProperties.type === "number" || additionalProperties.type === "object" ) { const schemaName = getSchemaName(additionalProperties); return ( ); } // Unknown type return null; }; const SchemaNodeDetails: React.FC = ({ name, schemaName, schema, required, schemaType, schemaPath, }) => { return (
} >
{schema.description && } {getQualifierMessage(schema) && ( )}
); }; const Items: React.FC<{ schema: any; schemaType: "request" | "response"; schemaPath?: string; }> = ({ schema, schemaType, schemaPath }) => { // Process schema.items to handle allOf merging let itemsSchema = schema.items; if (schema.items?.allOf) { itemsSchema = mergeAllOf(schema.items) as SchemaObject; } // Handle complex schemas with multiple schema types const hasOneOfAnyOf = itemsSchema?.oneOf || itemsSchema?.anyOf; const hasProperties = itemsSchema?.properties; const hasAdditionalProperties = itemsSchema?.additionalProperties; // Build the items schema path const itemsSchemaPath = schemaPath ? `${schemaPath}.items` : undefined; if (hasOneOfAnyOf || hasProperties || hasAdditionalProperties) { return ( <> {hasOneOfAnyOf && ( )} {hasProperties && ( )} {hasAdditionalProperties && ( )} ); } // Handles basic types (string, number, integer, boolean, object) if ( itemsSchema?.type === "string" || itemsSchema?.type === "number" || itemsSchema?.type === "integer" || itemsSchema?.type === "boolean" || itemsSchema?.type === "object" ) { return (
); } // Handles fallback case (use createEdges logic) return ( <> {Object.entries(itemsSchema || {}).map(([key, val]: [string, any]) => ( ))} ); }; interface SchemaEdgeProps { name: string; schemaName?: string; schema: SchemaObject; required?: boolean | string[]; nullable?: boolean | undefined; discriminator?: any; schemaType: "request" | "response"; schemaPath?: string; } const SchemaEdge: React.FC = ({ name, schema, required, discriminator, schemaType, schemaPath, }) => { if ( (schemaType === "request" && schema.readOnly) || (schemaType === "response" && schema.writeOnly) ) { return null; } const schemaName = getSchemaName(schema); if (discriminator && discriminator.propertyName === name) { return ( ); } if (schema.oneOf || schema.anyOf) { // return ; return ( ); } if (schema.properties) { return ( ); } if (schema.additionalProperties) { return ( ); } if (schema.items?.properties) { return ( ); } if (schema.items?.anyOf || schema.items?.oneOf || schema.items?.allOf) { return ( ); } if (schema.allOf) { // handle circular properties if ( schema.allOf && schema.allOf.length && schema.allOf.length === 1 && typeof schema.allOf[0] === "string" ) { return ( ); } const mergedSchemas = mergeAllOf(schema) as SchemaObject; if ( (schemaType === "request" && mergedSchemas.readOnly) || (schemaType === "response" && mergedSchemas.writeOnly) ) { return null; } const mergedSchemaName = getSchemaName(mergedSchemas); if (mergedSchemas.oneOf || mergedSchemas.anyOf) { return ( ); } if (mergedSchemas.properties !== undefined) { return ( ); } if (mergedSchemas.items?.properties) { return ( ); } return ( ); } return ( ); }; function renderChildren( schema: SchemaObject, schemaType: "request" | "response", schemaPath?: string ) { return ( <> {schema.oneOf && ( )} {schema.anyOf && ( )} {schema.properties && ( )} {schema.additionalProperties && ( )} {schema.items && ( )} ); } const SchemaNode: React.FC = ({ schema, schemaType, schemaPath, }) => { if ( (schemaType === "request" && schema.readOnly) || (schemaType === "response" && schema.writeOnly) ) { return null; } // Resolve discriminator recursively so nested oneOf/anyOf/allOf compositions // can still render discriminator tabs. let workingSchema = schema; const resolvedDiscriminator = schema.discriminator ?? findDiscriminator(schema); if (schema.allOf && !schema.discriminator && resolvedDiscriminator) { workingSchema = mergeAllOf(schema) as SchemaObject; } if (!workingSchema.discriminator && resolvedDiscriminator) { workingSchema.discriminator = resolvedDiscriminator; } if (workingSchema.discriminator) { const { discriminator } = workingSchema; return ( ); } // Handle allOf, oneOf, anyOf without discriminators if (schema.allOf) { // Check if allOf contains multiple oneOf/anyOf items that should be rendered separately const oneOfItems = schema.allOf.filter( (item: any) => item.oneOf || item.anyOf ); const hasMultipleChoices = oneOfItems.length > 1; if (hasMultipleChoices) { // Render each oneOf/anyOf constraint first, then shared properties const mergedSchemas = mergeAllOf(schema) as SchemaObject; if ( (schemaType === "request" && mergedSchemas.readOnly) || (schemaType === "response" && mergedSchemas.writeOnly) ) { return null; } return (
{/* Render all oneOf/anyOf constraints first */} {schema.allOf.map((item: any, index: number) => { if (item.oneOf || item.anyOf) { const itemSchemaPath = schemaPath ? `${schemaPath}.allOf.${index}` : undefined; return (
); } return null; })} {/* Then render shared properties from the merge */} {mergedSchemas.properties && ( )} {mergedSchemas.items && ( )}
); } // For other allOf cases, use standard merge behavior const mergedSchemas = mergeAllOf(schema) as SchemaObject; if ( (schemaType === "request" && mergedSchemas.readOnly) || (schemaType === "response" && mergedSchemas.writeOnly) ) { return null; } return (
{mergedSchemas.oneOf && ( )} {mergedSchemas.anyOf && ( )} {mergedSchemas.properties && ( )} {mergedSchemas.items && ( )}
); } // Handle primitives if ( schema.type && !schema.oneOf && !schema.anyOf && !schema.properties && !schema.allOf && !schema.items && !schema.additionalProperties ) { const schemaName = getSchemaName(schema); return ( ); } return renderChildren(schema, schemaType, schemaPath); }; export default SchemaNode; type PrimitiveSchemaType = | Exclude, "object" | "array"> | "null"; const PRIMITIVE_TYPES: Record = { string: true, number: true, integer: true, boolean: true, null: true, } as const; const isPrimitive = (schema: SchemaObject) => { // Enum-only schemas (without explicit type) should be treated as primitives // This is valid JSON Schema where enum values define the constraints if (schema.enum && !schema.type) { return true; } return PRIMITIVE_TYPES[schema.type as PrimitiveSchemaType]; };