Skip to content

Commit a2dc1cd

Browse files
feature: Added better support for testing with AJV having discriminator option turned on (#4257)
* feature: Added better support for testing with AJV having discriminator option turned on A recent issue made it clear that we didn't make it easy for users to turn on `discriminator` support from AJV - In `@rjsf/utils` improved support for `discriminator` as follows: - Updated the `ValidatorType` to add support for an optional `reset()` method - Updated the `ParserValidator` to implement `reset()` to clear the schema map, including a test to verify that - Updated the tests that used `discriminator` to remove the `mapping` block that AJV doesn't support - Updated the `getFirstMatchingOption()` test to deal with the situation where AJV doesn't support discriminator for array types - Updated the `retrieveSchema()` test to call `reset()` on validators that have it in an `afterEach()` - Updated the `getTestValidator()` implementation to implement a `reset()` that empties the arrays - In `@rjsf/validator-ajv8` improved support for `discriminator` as follows: - Updated the `createAjvInstance()` function to denote that we want to make `discriminator: true` the default in v6 - Updated the `AJV8Validator` to make reset do `ajv.removeSchema()` to clear the cached schemas - Updated the `getTestValidator() implementation to call `reset()` on the validator if it exists - Updated the `schema.test.ts` file to run a set of test with `discriminator: true` set on the `AJV8Validator` - Updated the `CHANGELOG.md` file accordingly * - Added required for `code` to all of the schemas * - Switched the default Translatable strings to use Markdown
1 parent 7f54d45 commit a2dc1cd

14 files changed

+99
-25
lines changed

CHANGELOG.md

+16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19+
# 5.19.4
20+
21+
## @rjsf/utils
22+
23+
- Updated the `ValidatorType` interface to add an optional `reset?: () => void` prop that can be implemented to reset a validator back to initial constructed state
24+
- Updated the `ParserValidator` to provide a `reset()` function that clears the schema map
25+
- Also updated the default translatable string to use `Markdown` rather than HTML tags since we now render them with `Markdown`
26+
27+
## @rjsf/validator-ajv8
28+
29+
- Updated the `AJV8Validator` to implement the `reset()` function to remove cached schemas in the `ajv` instance
30+
31+
## Dev / docs / playground
32+
33+
- Updated the `Validator` dropdown to add `AJV8 (discriminator)` which sets the AJV validator [discriminator](https://ajv.js.org/json-schema.html#discriminator) option to `true` to support testing schemas with that option in them
34+
1935
# 5.19.3
2036

2137
## @rjsf/antd

packages/playground/src/app.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import Playground, { PlaygroundProps } from './components';
1919
const esV8Validator = customizeValidator({}, localize_es);
2020
const AJV8_2019 = customizeValidator({ AjvClass: Ajv2019 });
2121
const AJV8_2020 = customizeValidator({ AjvClass: Ajv2020 });
22+
const AJV8_DISC = customizeValidator({ ajvOptionsOverrides: { discriminator: true } });
2223

2324
const validators: PlaygroundProps['validators'] = {
2425
AJV8: v8Validator,
26+
'AJV8 (discriminator)': AJV8_DISC,
2527
AJV8_es: esV8Validator,
2628
AJV8_2019,
2729
AJV8_2020,

packages/utils/src/enums.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,25 @@ export enum TranslatableString {
5555
/** Key label, where %1 will be replaced by the label as provided by WrapIfAdditionalTemplate */
5656
KeyLabel = '%1 Key',
5757
// Strings with replaceable parameters AND/OR that support markdown and html
58-
/** Invalid object field configuration as provided by the ObjectField */
59-
InvalidObjectField = 'Invalid "%1" object field configuration: <em>%2</em>.',
58+
/** Invalid object field configuration as provided by the ObjectField.
59+
* NOTE: Use markdown notation rather than html tags.
60+
*/
61+
InvalidObjectField = 'Invalid "%1" object field configuration: _%2_.',
6062
/** Unsupported field schema, used by UnsupportedField */
6163
UnsupportedField = 'Unsupported field schema.',
62-
/** Unsupported field schema, where %1 will be replaced by the idSchema.$id as provided by UnsupportedField */
63-
UnsupportedFieldWithId = 'Unsupported field schema for field <code>%1</code>.',
64-
/** Unsupported field schema, where %1 will be replaced by the reason string as provided by UnsupportedField */
65-
UnsupportedFieldWithReason = 'Unsupported field schema: <em>%1</em>.',
64+
/** Unsupported field schema, where %1 will be replaced by the idSchema.$id as provided by UnsupportedField.
65+
* NOTE: Use markdown notation rather than html tags.
66+
*/
67+
UnsupportedFieldWithId = 'Unsupported field schema for field `%1`.',
68+
/** Unsupported field schema, where %1 will be replaced by the reason string as provided by UnsupportedField.
69+
* NOTE: Use markdown notation rather than html tags.
70+
*/
71+
UnsupportedFieldWithReason = 'Unsupported field schema: _%1_.',
6672
/** Unsupported field schema, where %1 and %2 will be replaced by the idSchema.$id and reason strings, respectively,
67-
* as provided by UnsupportedField
73+
* as provided by UnsupportedField.
74+
* NOTE: Use markdown notation rather than html tags.
6875
*/
69-
UnsupportedFieldWithIdAndReason = 'Unsupported field schema for field <code>%1</code>: <em>%2</em>.',
76+
UnsupportedFieldWithIdAndReason = 'Unsupported field schema for field `%1`: _%2_.',
7077
/** File name, type and size info, where %1, %2 and %3 will be replaced by the file name, file type and file size as
7178
* provided by FileWidget
7279
*/

packages/utils/src/parser/ParserValidator.ts

+6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
4848
this.addSchema(rootSchema, hashForSchema<S>(rootSchema));
4949
}
5050

51+
/** Resets the internal AJV validator to clear schemas from it. Can be helpful for resetting the validator for tests.
52+
*/
53+
reset() {
54+
this.schemaMap = {};
55+
}
56+
5157
/** Adds the given `schema` to the `schemaMap` keyed by the `hash` or `ID_KEY` if present on the `schema`. If the
5258
* schema does not have an `ID_KEY`, then the `hash` will be added as the `ID_KEY` to allow the schema to be
5359
* associated with it's `hash` for future use (by a schema compiler).

packages/utils/src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,10 @@ export interface ValidatorType<T = any, S extends StrictRJSFSchema = RJSFSchema,
10041004
* @param formData - The form data to validate
10051005
*/
10061006
rawValidation<Result = any>(schema: S, formData?: T): { errors?: Result[]; validationError?: Error };
1007+
/** An optional function that can be used to reset validator implementation. Useful for clear schemas in the AJV
1008+
* instance for tests.
1009+
*/
1010+
reset?: () => void;
10071011
}
10081012

10091013
/** The `SchemaUtilsType` interface provides a wrapper around the publicly exported APIs in the `@rjsf/utils/schema`

packages/utils/test/parser/ParserValidator.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,8 @@ describe('ParserValidator', () => {
9797
JSON.stringify({ ...DUPLICATE_SCHEMA, [ID_KEY]: DUPLICATE_HASH }, null, 2)
9898
);
9999
});
100+
it('reset clears the map', () => {
101+
validator.reset();
102+
expect(validator.schemaMap).toEqual({});
103+
});
100104
});

packages/utils/test/schema/getClosestMatchingOptionTest.ts

-8
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,6 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
236236
},
237237
discriminator: {
238238
propertyName: 'code',
239-
mapping: {
240-
foo_coding: '#/definitions/Foo',
241-
bar_coding: '#/definitions/Bar',
242-
},
243239
},
244240
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
245241
};
@@ -269,10 +265,6 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
269265
},
270266
discriminator: {
271267
propertyName: 'code',
272-
mapping: {
273-
foo_coding: '#/definitions/Foo',
274-
bar_coding: '#/definitions/Bar',
275-
},
276268
},
277269
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
278270
};

packages/utils/test/schema/getFirstMatchingOptionTest.ts

+20-9
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,9 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
121121
},
122122
discriminator: {
123123
propertyName: 'code',
124-
mapping: {
125-
foo_coding: '#/definitions/Foo',
126-
bar_coding: '#/definitions/Bar',
127-
},
128124
},
129125
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
126+
required: ['code'],
130127
};
131128
const options = [schema.definitions!.Foo, schema.definitions!.Bar] as RJSFSchema[];
132129
expect(getFirstMatchingOption(testValidator, null, options, schema, 'code')).toEqual(0);
@@ -156,12 +153,9 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
156153
},
157154
discriminator: {
158155
propertyName: 'code',
159-
mapping: {
160-
foo_coding: '#/definitions/Foo',
161-
bar_coding: '#/definitions/Bar',
162-
},
163156
},
164157
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
158+
required: ['code'],
165159
};
166160
const formData = { code: 'bar_coding' };
167161
const options = [schema.definitions!.Foo, schema.definitions!.Bar] as RJSFSchema[];
@@ -172,6 +166,7 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
172166

173167
// simple in the sense of getOptionMatchingSimpleDiscriminator
174168
it('should return Bar when schema has non-simple discriminator for bar', () => {
169+
const consoleWarnSpy = jest.spyOn(console, 'warn');
175170
// Mock isValid to pass the second value
176171
testValidator.setReturnValues({ isValid: [false, true] });
177172
const schema: RJSFSchema = {
@@ -196,12 +191,28 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
196191
propertyName: 'code',
197192
},
198193
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
194+
required: ['code'],
199195
};
200196
const formData = { code: ['bar_coding'] };
201197
const options = [schema.definitions!.Foo, schema.definitions!.Bar] as RJSFSchema[];
202198
// Use the schemaUtils to verify the discriminator prop gets passed
203199
const schemaUtils = createSchemaUtils(testValidator, schema);
204-
expect(schemaUtils.getFirstMatchingOption(formData, options, 'code')).toEqual(1);
200+
const result = schemaUtils.getFirstMatchingOption(formData, options, 'code');
201+
const wasWarned = consoleWarnSpy.mock.calls.length > 0;
202+
if (wasWarned) {
203+
// According to the docs https://ajv.js.org/json-schema.html#discriminator, with ajv8 discrimator turned on the
204+
// schema in this test will fail because of the limitations of AJV implementation
205+
expect(consoleWarnSpy).toHaveBeenCalledWith(
206+
'Error encountered compiling schema:',
207+
expect.objectContaining({
208+
message: 'discriminator: "properties/code" must have "const" or "enum"',
209+
})
210+
);
211+
expect(result).toEqual(0);
212+
} else {
213+
expect(result).toEqual(1);
214+
}
215+
consoleWarnSpy.mockRestore();
205216
});
206217
});
207218
}

packages/utils/test/schema/retrieveSchemaTest.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) {
3636
});
3737
afterEach(() => {
3838
consoleWarnSpy.mockClear();
39+
testValidator.reset?.();
3940
});
4041
it('returns empty object when schema is not an object', () => {
4142
expect(retrieveSchema(testValidator, [] as RJSFSchema)).toEqual({});

packages/utils/test/testUtils/getTestValidator.ts

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export default function getTestValidator<T = any>({
5858
testValidator._errorList = errorList;
5959
}
6060
},
61+
reset() {
62+
testValidator._data = [];
63+
testValidator._isValid = [];
64+
testValidator._errorList = [];
65+
},
6166
},
6267
};
6368
return testValidator.validator;

packages/validator-ajv8/src/createAjvInstance.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const AJV_CONFIG: Options = {
1010
multipleOfPrecision: 8,
1111
strict: false,
1212
verbose: true,
13+
discriminator: false, // TODO enable this in V6
1314
} as const;
1415
export const COLOR_FORMAT_REGEX =
1516
/^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/;

packages/validator-ajv8/src/validator.ts

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
4949
this.localizer = localizer;
5050
}
5151

52+
/** Resets the internal AJV validator to clear schemas from it. Can be helpful for resetting the validator for tests.
53+
*/
54+
reset() {
55+
this.ajv.removeSchema();
56+
}
57+
5258
/** Converts an `errorSchema` into a list of `RJSFValidationErrors`
5359
*
5460
* @param errorSchema - The `ErrorSchema` instance to convert

packages/validator-ajv8/test/utilsTests/getTestValidator.ts

+3
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,8 @@ export default function getTestValidator<T = any>(options: CustomValidatorOption
3838
},
3939
// This is intentionally a no-op as we are using the real validator here
4040
setReturnValues() {},
41+
reset() {
42+
validator.reset?.();
43+
},
4144
};
4245
}

packages/validator-ajv8/test/utilsTests/schema.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ sanitizeDataForNewSchemaTest(testValidator);
3333
toIdSchemaTest(testValidator);
3434
toPathSchemaTest(testValidator);
3535

36+
const testValidatorDiscriminated = getTestValidator({ ajvOptionsOverrides: { discriminator: true } });
37+
38+
// NOTE: to restrict which tests to run, you can temporarily comment out any tests you aren't needing
39+
getDefaultFormStateTest(testValidatorDiscriminated);
40+
getDisplayLabelTest(testValidatorDiscriminated);
41+
getClosestMatchingOptionTest(testValidatorDiscriminated);
42+
getFirstMatchingOptionTest(testValidatorDiscriminated);
43+
isFilesArrayTest(testValidatorDiscriminated);
44+
isMultiSelectTest(testValidatorDiscriminated);
45+
isSelectTest(testValidatorDiscriminated);
46+
mergeValidationDataTest(testValidatorDiscriminated);
47+
retrieveSchemaTest(testValidatorDiscriminated);
48+
sanitizeDataForNewSchemaTest(testValidatorDiscriminated);
49+
toIdSchemaTest(testValidatorDiscriminated);
50+
toPathSchemaTest(testValidatorDiscriminated);
51+
3652
const testValidator2019 = getTestValidator({ AjvClass: Ajv2019 });
3753

3854
// NOTE: to restrict which tests to run, you can temporarily comment out any tests you aren't needing

0 commit comments

Comments
 (0)