Skip to content

Commit

Permalink
feature: Added better support for testing with AJV having discriminat…
Browse files Browse the repository at this point in the history
…or 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
  • Loading branch information
heath-freenome authored Jul 27, 2024
1 parent 7f54d45 commit a2dc1cd
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 25 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ should change the heading of the (upcoming) version to include a major version b
-->

# 5.19.4

## @rjsf/utils

- Updated the `ValidatorType` interface to add an optional `reset?: () => void` prop that can be implemented to reset a validator back to initial constructed state
- Updated the `ParserValidator` to provide a `reset()` function that clears the schema map
- Also updated the default translatable string to use `Markdown` rather than HTML tags since we now render them with `Markdown`

## @rjsf/validator-ajv8

- Updated the `AJV8Validator` to implement the `reset()` function to remove cached schemas in the `ajv` instance

## Dev / docs / playground

- 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

# 5.19.3

## @rjsf/antd
Expand Down
2 changes: 2 additions & 0 deletions packages/playground/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import Playground, { PlaygroundProps } from './components';
const esV8Validator = customizeValidator({}, localize_es);
const AJV8_2019 = customizeValidator({ AjvClass: Ajv2019 });
const AJV8_2020 = customizeValidator({ AjvClass: Ajv2020 });
const AJV8_DISC = customizeValidator({ ajvOptionsOverrides: { discriminator: true } });

const validators: PlaygroundProps['validators'] = {
AJV8: v8Validator,
'AJV8 (discriminator)': AJV8_DISC,
AJV8_es: esV8Validator,
AJV8_2019,
AJV8_2020,
Expand Down
23 changes: 15 additions & 8 deletions packages/utils/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,25 @@ export enum TranslatableString {
/** Key label, where %1 will be replaced by the label as provided by WrapIfAdditionalTemplate */
KeyLabel = '%1 Key',
// Strings with replaceable parameters AND/OR that support markdown and html
/** Invalid object field configuration as provided by the ObjectField */
InvalidObjectField = 'Invalid "%1" object field configuration: <em>%2</em>.',
/** Invalid object field configuration as provided by the ObjectField.
* NOTE: Use markdown notation rather than html tags.
*/
InvalidObjectField = 'Invalid "%1" object field configuration: _%2_.',
/** Unsupported field schema, used by UnsupportedField */
UnsupportedField = 'Unsupported field schema.',
/** Unsupported field schema, where %1 will be replaced by the idSchema.$id as provided by UnsupportedField */
UnsupportedFieldWithId = 'Unsupported field schema for field <code>%1</code>.',
/** Unsupported field schema, where %1 will be replaced by the reason string as provided by UnsupportedField */
UnsupportedFieldWithReason = 'Unsupported field schema: <em>%1</em>.',
/** Unsupported field schema, where %1 will be replaced by the idSchema.$id as provided by UnsupportedField.
* NOTE: Use markdown notation rather than html tags.
*/
UnsupportedFieldWithId = 'Unsupported field schema for field `%1`.',
/** Unsupported field schema, where %1 will be replaced by the reason string as provided by UnsupportedField.
* NOTE: Use markdown notation rather than html tags.
*/
UnsupportedFieldWithReason = 'Unsupported field schema: _%1_.',
/** Unsupported field schema, where %1 and %2 will be replaced by the idSchema.$id and reason strings, respectively,
* as provided by UnsupportedField
* as provided by UnsupportedField.
* NOTE: Use markdown notation rather than html tags.
*/
UnsupportedFieldWithIdAndReason = 'Unsupported field schema for field <code>%1</code>: <em>%2</em>.',
UnsupportedFieldWithIdAndReason = 'Unsupported field schema for field `%1`: _%2_.',
/** File name, type and size info, where %1, %2 and %3 will be replaced by the file name, file type and file size as
* provided by FileWidget
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/utils/src/parser/ParserValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
this.addSchema(rootSchema, hashForSchema<S>(rootSchema));
}

/** Resets the internal AJV validator to clear schemas from it. Can be helpful for resetting the validator for tests.
*/
reset() {
this.schemaMap = {};
}

/** Adds the given `schema` to the `schemaMap` keyed by the `hash` or `ID_KEY` if present on the `schema`. If the
* schema does not have an `ID_KEY`, then the `hash` will be added as the `ID_KEY` to allow the schema to be
* associated with it's `hash` for future use (by a schema compiler).
Expand Down
4 changes: 4 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,10 @@ export interface ValidatorType<T = any, S extends StrictRJSFSchema = RJSFSchema,
* @param formData - The form data to validate
*/
rawValidation<Result = any>(schema: S, formData?: T): { errors?: Result[]; validationError?: Error };
/** An optional function that can be used to reset validator implementation. Useful for clear schemas in the AJV
* instance for tests.
*/
reset?: () => void;
}

/** The `SchemaUtilsType` interface provides a wrapper around the publicly exported APIs in the `@rjsf/utils/schema`
Expand Down
4 changes: 4 additions & 0 deletions packages/utils/test/parser/ParserValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,8 @@ describe('ParserValidator', () => {
JSON.stringify({ ...DUPLICATE_SCHEMA, [ID_KEY]: DUPLICATE_HASH }, null, 2)
);
});
it('reset clears the map', () => {
validator.reset();
expect(validator.schemaMap).toEqual({});
});
});
8 changes: 0 additions & 8 deletions packages/utils/test/schema/getClosestMatchingOptionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,6 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
},
discriminator: {
propertyName: 'code',
mapping: {
foo_coding: '#/definitions/Foo',
bar_coding: '#/definitions/Bar',
},
},
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
};
Expand Down Expand Up @@ -269,10 +265,6 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
},
discriminator: {
propertyName: 'code',
mapping: {
foo_coding: '#/definitions/Foo',
bar_coding: '#/definitions/Bar',
},
},
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
};
Expand Down
29 changes: 20 additions & 9 deletions packages/utils/test/schema/getFirstMatchingOptionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,9 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
},
discriminator: {
propertyName: 'code',
mapping: {
foo_coding: '#/definitions/Foo',
bar_coding: '#/definitions/Bar',
},
},
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
required: ['code'],
};
const options = [schema.definitions!.Foo, schema.definitions!.Bar] as RJSFSchema[];
expect(getFirstMatchingOption(testValidator, null, options, schema, 'code')).toEqual(0);
Expand Down Expand Up @@ -156,12 +153,9 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
},
discriminator: {
propertyName: 'code',
mapping: {
foo_coding: '#/definitions/Foo',
bar_coding: '#/definitions/Bar',
},
},
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
required: ['code'],
};
const formData = { code: 'bar_coding' };
const options = [schema.definitions!.Foo, schema.definitions!.Bar] as RJSFSchema[];
Expand All @@ -172,6 +166,7 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT

// simple in the sense of getOptionMatchingSimpleDiscriminator
it('should return Bar when schema has non-simple discriminator for bar', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn');
// Mock isValid to pass the second value
testValidator.setReturnValues({ isValid: [false, true] });
const schema: RJSFSchema = {
Expand All @@ -196,12 +191,28 @@ export default function getFirstMatchingOptionTest(testValidator: TestValidatorT
propertyName: 'code',
},
oneOf: [{ $ref: '#/definitions/Foo' }, { $ref: '#/definitions/Bar' }],
required: ['code'],
};
const formData = { code: ['bar_coding'] };
const options = [schema.definitions!.Foo, schema.definitions!.Bar] as RJSFSchema[];
// Use the schemaUtils to verify the discriminator prop gets passed
const schemaUtils = createSchemaUtils(testValidator, schema);
expect(schemaUtils.getFirstMatchingOption(formData, options, 'code')).toEqual(1);
const result = schemaUtils.getFirstMatchingOption(formData, options, 'code');
const wasWarned = consoleWarnSpy.mock.calls.length > 0;
if (wasWarned) {
// According to the docs https://ajv.js.org/json-schema.html#discriminator, with ajv8 discrimator turned on the
// schema in this test will fail because of the limitations of AJV implementation
expect(consoleWarnSpy).toHaveBeenCalledWith(
'Error encountered compiling schema:',
expect.objectContaining({
message: 'discriminator: "properties/code" must have "const" or "enum"',
})
);
expect(result).toEqual(0);
} else {
expect(result).toEqual(1);
}
consoleWarnSpy.mockRestore();
});
});
}
1 change: 1 addition & 0 deletions packages/utils/test/schema/retrieveSchemaTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) {
});
afterEach(() => {
consoleWarnSpy.mockClear();
testValidator.reset?.();
});
it('returns empty object when schema is not an object', () => {
expect(retrieveSchema(testValidator, [] as RJSFSchema)).toEqual({});
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/test/testUtils/getTestValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export default function getTestValidator<T = any>({
testValidator._errorList = errorList;
}
},
reset() {
testValidator._data = [];
testValidator._isValid = [];
testValidator._errorList = [];
},
},
};
return testValidator.validator;
Expand Down
1 change: 1 addition & 0 deletions packages/validator-ajv8/src/createAjvInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const AJV_CONFIG: Options = {
multipleOfPrecision: 8,
strict: false,
verbose: true,
discriminator: false, // TODO enable this in V6
} as const;
export const COLOR_FORMAT_REGEX =
/^(#?([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*\)))$/;
Expand Down
6 changes: 6 additions & 0 deletions packages/validator-ajv8/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
this.localizer = localizer;
}

/** Resets the internal AJV validator to clear schemas from it. Can be helpful for resetting the validator for tests.
*/
reset() {
this.ajv.removeSchema();
}

/** Converts an `errorSchema` into a list of `RJSFValidationErrors`
*
* @param errorSchema - The `ErrorSchema` instance to convert
Expand Down
3 changes: 3 additions & 0 deletions packages/validator-ajv8/test/utilsTests/getTestValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ export default function getTestValidator<T = any>(options: CustomValidatorOption
},
// This is intentionally a no-op as we are using the real validator here
setReturnValues() {},
reset() {
validator.reset?.();
},
};
}
16 changes: 16 additions & 0 deletions packages/validator-ajv8/test/utilsTests/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ sanitizeDataForNewSchemaTest(testValidator);
toIdSchemaTest(testValidator);
toPathSchemaTest(testValidator);

const testValidatorDiscriminated = getTestValidator({ ajvOptionsOverrides: { discriminator: true } });

// NOTE: to restrict which tests to run, you can temporarily comment out any tests you aren't needing
getDefaultFormStateTest(testValidatorDiscriminated);
getDisplayLabelTest(testValidatorDiscriminated);
getClosestMatchingOptionTest(testValidatorDiscriminated);
getFirstMatchingOptionTest(testValidatorDiscriminated);
isFilesArrayTest(testValidatorDiscriminated);
isMultiSelectTest(testValidatorDiscriminated);
isSelectTest(testValidatorDiscriminated);
mergeValidationDataTest(testValidatorDiscriminated);
retrieveSchemaTest(testValidatorDiscriminated);
sanitizeDataForNewSchemaTest(testValidatorDiscriminated);
toIdSchemaTest(testValidatorDiscriminated);
toPathSchemaTest(testValidatorDiscriminated);

const testValidator2019 = getTestValidator({ AjvClass: Ajv2019 });

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

0 comments on commit a2dc1cd

Please sign in to comment.