diff --git a/packages/utils/src/schema/toPathSchema.ts b/packages/utils/src/schema/toPathSchema.ts index e0b2abb368..50447c02b9 100644 --- a/packages/utils/src/schema/toPathSchema.ts +++ b/packages/utils/src/schema/toPathSchema.ts @@ -1,11 +1,11 @@ -import get from 'lodash/get'; -import isEqual from 'lodash/isEqual'; -import set from 'lodash/set'; +import get from "lodash/get"; +import isEqual from "lodash/isEqual"; +import set from "lodash/set"; import { + ADDITIONAL_PROPERTIES_KEY, ALL_OF_KEY, ANY_OF_KEY, - ADDITIONAL_PROPERTIES_KEY, DEPENDENCIES_KEY, ITEMS_KEY, NAME_KEY, @@ -13,11 +13,18 @@ import { PROPERTIES_KEY, REF_KEY, RJSF_ADDITIONAL_PROPERTIES_FLAG, -} from '../constants'; -import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema'; -import { FormContextType, GenericObjectType, PathSchema, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types'; -import getClosestMatchingOption from './getClosestMatchingOption'; -import retrieveSchema from './retrieveSchema'; +} from "../constants"; +import getDiscriminatorFieldFromSchema from "../getDiscriminatorFieldFromSchema"; +import { + FormContextType, + GenericObjectType, + PathSchema, + RJSFSchema, + StrictRJSFSchema, + ValidatorType, +} from "../types"; +import getClosestMatchingOption from "./getClosestMatchingOption"; +import retrieveSchema from "./retrieveSchema"; /** An internal helper that generates an `PathSchema` object for the `schema`, recursively with protection against * infinite recursion @@ -30,103 +37,151 @@ import retrieveSchema from './retrieveSchema'; * @param [_recurseList=[]] - The list of retrieved schemas currently being recursed, used to prevent infinite recursion * @returns - The `PathSchema` object for the `schema` */ -function toPathSchemaInternal( +function toPathSchemaInternal< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>( validator: ValidatorType, schema: S, name: string, rootSchema?: S, formData?: T, - _recurseList: S[] = [] + _recurseList: S[] = [], ): PathSchema { - if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) { - const _schema = retrieveSchema(validator, schema, rootSchema, formData); - const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema)); - if (sameSchemaIndex === -1) { - return toPathSchemaInternal( + const firstState = { + schema, + name, + formData, + _recurseList, + pathSchema: {} as PathSchema, + }; + const stack = [firstState]; + + while (stack.length > 0) { + const { schema, formData, _recurseList, pathSchema, name } = stack.pop()!; + + if ( + REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema + ) { + const _schema = retrieveSchema( validator, - _schema, - name, + schema, rootSchema, formData, - _recurseList.concat(_schema) ); + const sameSchemaIndex = _recurseList.findIndex((item) => + isEqual(item, _schema) + ); + if (sameSchemaIndex === -1) { + stack.push({ + schema: _schema, + name, + formData, + _recurseList: _recurseList.concat(_schema), + pathSchema, + }); + continue; + } } - } - let pathSchema: PathSchema = { - [NAME_KEY]: name.replace(/^\./, ''), - } as PathSchema; + pathSchema[NAME_KEY] = name.replace(/^\./, ""); - if (ONE_OF_KEY in schema || ANY_OF_KEY in schema) { - const xxxOf: S[] = ONE_OF_KEY in schema ? (schema.oneOf as S[]) : (schema.anyOf as S[]); - const discriminator = getDiscriminatorFieldFromSchema(schema); - const index = getClosestMatchingOption(validator, rootSchema!, formData, xxxOf, 0, discriminator); - const _schema: S = xxxOf![index] as S; - pathSchema = { - ...pathSchema, - ...toPathSchemaInternal(validator, _schema, name, rootSchema, formData, _recurseList), - }; - } + if (ONE_OF_KEY in schema || ANY_OF_KEY in schema) { + const xxxOf: S[] = ONE_OF_KEY in schema + ? (schema.oneOf as S[]) + : (schema.anyOf as S[]); + const discriminator = getDiscriminatorFieldFromSchema(schema); + const index = getClosestMatchingOption( + validator, + rootSchema!, + formData, + xxxOf, + 0, + discriminator, + ); + const _schema: S = xxxOf![index] as S; + stack.push({ + schema: _schema, + name, + formData, + _recurseList, + pathSchema, + }); + } - if (ADDITIONAL_PROPERTIES_KEY in schema && schema[ADDITIONAL_PROPERTIES_KEY] !== false) { - set(pathSchema, RJSF_ADDITIONAL_PROPERTIES_FLAG, true); - } + if ( + ADDITIONAL_PROPERTIES_KEY in schema && + schema[ADDITIONAL_PROPERTIES_KEY] !== false + ) { + set(pathSchema, RJSF_ADDITIONAL_PROPERTIES_FLAG, true); + } - if (ITEMS_KEY in schema && Array.isArray(formData)) { - const { items: schemaItems, additionalItems: schemaAdditionalItems } = schema; + if (ITEMS_KEY in schema && Array.isArray(formData)) { + const { items: schemaItems, additionalItems: schemaAdditionalItems } = + schema; - if (Array.isArray(schemaItems)) { - formData.forEach((element, i: number) => { - if (schemaItems[i]) { - (pathSchema as PathSchema)[i] = toPathSchemaInternal( - validator, - schemaItems[i] as S, - `${name}.${i}`, - rootSchema, - element, - _recurseList - ); - } else if (schemaAdditionalItems) { - (pathSchema as PathSchema)[i] = toPathSchemaInternal( - validator, - schemaAdditionalItems as S, - `${name}.${i}`, - rootSchema, - element, - _recurseList - ); - } else { - console.warn(`Unable to generate path schema for "${name}.${i}". No schema defined for it`); - } - }); - } else { - formData.forEach((element, i: number) => { - (pathSchema as PathSchema)[i] = toPathSchemaInternal( - validator, - schemaItems as S, - `${name}.${i}`, - rootSchema, - element, - _recurseList - ); - }); - } - } else if (PROPERTIES_KEY in schema) { - for (const property in schema.properties) { - const field = get(schema, [PROPERTIES_KEY, property]); - (pathSchema as PathSchema)[property] = toPathSchemaInternal( - validator, - field, - `${name}.${property}`, - rootSchema, - // It's possible that formData is not an object -- this can happen if an - // array item has just been added, but not populated with data yet - get(formData, [property]), - _recurseList - ); + if (Array.isArray(schemaItems)) { + formData.forEach((element, i: number) => { + if (schemaItems[i]) { + const newPathSchema = {} as PathSchema; + (pathSchema as PathSchema)[i] = newPathSchema; + stack.push({ + schema: schemaItems[i] as S, + name: `${name}.${i}`, + formData: element, + _recurseList, + pathSchema: newPathSchema, + }); + } else if (schemaAdditionalItems) { + const newPathSchema = {} as PathSchema; + (pathSchema as PathSchema)[i] = newPathSchema; + stack.push({ + schema: schemaAdditionalItems as S, + name: `${name}.${i}`, + formData: element, + _recurseList, + pathSchema: newPathSchema, + }); + } else { + console.warn( + `Unable to generate path schema for "${name}.${i}". No schema defined for it`, + ); + } + }); + } else { + formData.forEach((element, i: number) => { + const newPathSchema = {} as PathSchema; + (pathSchema as PathSchema)[i] = newPathSchema; + stack.push({ + schema: schemaItems as S, + name: `${name}.${i}`, + formData: element, + _recurseList, + pathSchema: newPathSchema, + }); + }); + } + } else if (PROPERTIES_KEY in schema) { + for (const property in schema.properties) { + const field = get(schema, [PROPERTIES_KEY, property]); + + const newPathSchema = {} as PathSchema; + (pathSchema as PathSchema)[property] = newPathSchema; + stack.push({ + schema: field, + name: `${name}.${property}`, + // It's possible that formData is not an object -- this can happen if an + // array item has just been added, but not populated with data yet + formData: get(formData, [property]), + _recurseList, + pathSchema: newPathSchema, + }); + } } } - return pathSchema; + + return firstState.pathSchema; } /** Generates an `PathSchema` object for the `schema`, recursively @@ -138,12 +193,16 @@ function toPathSchemaInternal( +export default function toPathSchema< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>( validator: ValidatorType, schema: S, - name = '', + name = "", rootSchema?: S, - formData?: T + formData?: T, ): PathSchema { return toPathSchemaInternal(validator, schema, name, rootSchema, formData); }