Skip to content

Commit 9f130b6

Browse files
committed
fixed exponential recursion in calculateIndexScore by removing recursive getClosestMatchingOption call for nested oneOf
1 parent 2fb9a34 commit 9f130b6

2 files changed

Lines changed: 53 additions & 28 deletions

File tree

packages/utils/src/schema/getClosestMatchingOption.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,9 @@ export const JUNK_OPTION: StrictRJSFSchema = {
3030
/** Recursive function that calculates the score of a `formData` against the given `schema`. The computation is fairly
3131
* simple. Initially the total score is 0. When `schema.properties` object exists, then all the `key/value` pairs within
3232
* the object are processed as follows after obtaining the formValue from `formData` using the `key`:
33-
* - If the `value` contains a `$ref`, `calculateIndexScore()` is called recursively with the formValue and the new
34-
* schema that is the result of the ref in the schema being resolved and that sub-schema's resulting score is added to
35-
* the total.
36-
* - If the `value` contains a `oneOf` and there is a formValue, then score based on the index returned from calling
37-
* `getClosestMatchingOption()` of that oneOf.
33+
* - If the `value` contains a `$ref`, it is resolved and scoring continues on the resolved schema.
34+
* - If the `value` contains a `oneOf`/`anyOf` and there is a formValue, the first matching option is found via
35+
* `getFirstMatchingOption()` and `calculateIndexScore()` is called recursively on that option.
3836
* - If the type of the `value` is 'object', `calculateIndexScore()` is called recursively with the formValue and the
3937
* `value` itself as the sub-schema, and the score is added to the total.
4038
* - If the type of the `value` matches the guessed-type of the `formValue`, the score is incremented by 1, UNLESS the
@@ -66,36 +64,33 @@ export function calculateIndexScore<T = any, S extends StrictRJSFSchema = RJSFSc
6664
return score;
6765
}
6866
if (has(value, REF_KEY)) {
69-
const newSchema = retrieveSchema<T, S, F>(
67+
value = retrieveSchema<T, S, F>(
7068
validator,
7169
value as S,
7270
rootSchema,
7371
formValue,
7472
experimental_customMergeAllOf,
7573
);
76-
return (
77-
score +
78-
calculateIndexScore<T, S, F>(
79-
validator,
80-
rootSchema,
81-
newSchema,
82-
formValue || {},
83-
experimental_customMergeAllOf,
84-
)
85-
);
8674
}
8775
if ((has(value, ONE_OF_KEY) || has(value, ANY_OF_KEY)) && formValue) {
8876
const key = has(value, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY;
77+
const options = get(value, key) as S[];
8978
const discriminator = getDiscriminatorFieldFromSchema<S>(value as S);
79+
const matched = getFirstMatchingOption<T, S, F>(validator, formValue, options, rootSchema, discriminator);
80+
const resolvedOption = retrieveSchema<T, S, F>(
81+
validator,
82+
options[matched] as S,
83+
rootSchema,
84+
formValue,
85+
experimental_customMergeAllOf,
86+
);
9087
return (
9188
score +
92-
getClosestMatchingOption<T, S, F>(
89+
calculateIndexScore<T, S, F>(
9390
validator,
9491
rootSchema,
92+
resolvedOption,
9593
formValue,
96-
get(value, key) as S[],
97-
-1,
98-
discriminator,
9994
experimental_customMergeAllOf,
10095
)
10196
);

packages/utils/test/schema/getClosestMatchingOptionTest.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
4545
it('returns 0 when formData is empty object', () => {
4646
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, {})).toEqual(0);
4747
});
48-
it('returns 1 for first option in oneOf schema', () => {
49-
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, ONE_OF_SCHEMA_DATA)).toEqual(1);
48+
it('returns 2 for first option in oneOf schema', () => {
49+
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, ONE_OF_SCHEMA_DATA)).toEqual(2);
5050
});
51-
it('returns 8 for second option in oneOf schema', () => {
52-
expect(calculateIndexScore(testValidator, oneOfSchema, secondOption, ONE_OF_SCHEMA_DATA)).toEqual(9);
51+
it('returns 10 for second option in oneOf schema', () => {
52+
expect(calculateIndexScore(testValidator, oneOfSchema, secondOption, ONE_OF_SCHEMA_DATA)).toEqual(10);
5353
});
5454
it('returns 1 for a schema that has a type matching the formData type', () => {
5555
expect(calculateIndexScore(testValidator, oneOfSchema, { type: 'boolean' }, true)).toEqual(1);
@@ -74,6 +74,34 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
7474
),
7575
).toEqual(0);
7676
});
77+
it('scores nested oneOf by finding the first matching option and recursing', () => {
78+
const schema: RJSFSchema = {
79+
properties: {
80+
choice: {
81+
oneOf: [
82+
{ properties: { day: { type: 'string' } } },
83+
{ properties: { night: { type: 'string' } } },
84+
],
85+
},
86+
},
87+
};
88+
testValidator.setReturnValues({ isValid: [true] });
89+
expect(calculateIndexScore(testValidator, oneOfSchema, schema, { choice: { day: 'monday' } })).toEqual(1);
90+
});
91+
it('scores nested anyOf by finding the first matching option and recursing', () => {
92+
const schema: RJSFSchema = {
93+
properties: {
94+
choice: {
95+
anyOf: [
96+
{ properties: { x: { type: 'number' } } },
97+
{ properties: { y: { type: 'number' } } },
98+
],
99+
},
100+
},
101+
};
102+
testValidator.setReturnValues({ isValid: [true] });
103+
expect(calculateIndexScore(testValidator, oneOfSchema, schema, { choice: { x: 42 } })).toEqual(1);
104+
});
77105
});
78106
describe('oneOfMatchingOption', () => {
79107
it('oneOfSchema, oneOfData data, no options, returns -1', () => {
@@ -161,9 +189,10 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
161189
},
162190
};
163191
const formData = { ipsum: { night: 'nicht' } };
164-
// Mock to return true for the last of the second one-ofs
192+
// Mock: first 3 calls (JUNK for lorem, lorem itself, JUNK for ipsum) fail;
193+
// 4th call (ipsum with oneOf stripped to {}) succeeds, giving a unique valid match
165194
testValidator.setReturnValues({
166-
isValid: [false, false, false, false, false, false, false, true],
195+
isValid: [false, false, false, true],
167196
});
168197
expect(getClosestMatchingOption(testValidator, schema, formData, get(schema, 'items.oneOf'))).toEqual(1);
169198
});
@@ -207,9 +236,10 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
207236
},
208237
};
209238
const formData = { ipsum: { night: 'nicht' } };
210-
// Mock to return true for the last of the second anyOfs
239+
// Mock: first 3 calls (JUNK for lorem, lorem itself, JUNK for ipsum) fail;
240+
// 4th call (ipsum with anyOf stripped to {}) succeeds, giving a unique valid match
211241
testValidator.setReturnValues({
212-
isValid: [false, false, false, false, false, false, false, true],
242+
isValid: [false, false, false, true],
213243
});
214244
expect(getClosestMatchingOption(testValidator, schema, formData, get(schema, 'items.anyOf'))).toEqual(1);
215245
});

0 commit comments

Comments
 (0)