Skip to content

Commit

Permalink
Partially fixed issue where dependency errors do not show title or ui…
Browse files Browse the repository at this point in the history
…:title. This fix only applicable if we use an ajv-i18n localizer. Ref. rjsf-team#4402.
  • Loading branch information
chibacchie committed Dec 16, 2024
1 parent 5335d72 commit caa59ca
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 28 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b
-->

# 5.23.3

## @rjsf/validator-ajv8

- Partially fixed issue where dependency errors do not show `title` or `ui:title`. This fix only applicable if we use an ajv-i18n localizer. Ref. [#4402](https://github.com/rjsf-team/react-jsonschema-form/issues/4402).

# 5.23.2

## @rjsf/core
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/validator-ajv8/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@types/jest": "^29.5.12",
"@types/json-schema": "^7.0.15",
"@types/lodash": "^4.14.202",
"ajv-i18n": "^4.2.0",
"babel-jest": "^29.7.0",
"eslint": "^8.56.0",
"jest": "^29.7.0",
Expand Down
52 changes: 30 additions & 22 deletions packages/validator-ajv8/src/processRawValidationErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,33 @@ export function transformRJSFValidationErrors<
let { message = '' } = rest;
let property = instancePath.replace(/\//g, '.');
let stack = `${property} ${message}`.trim();

if ('missingProperty' in params) {
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
const currentProperty: string = params.missingProperty;
let uiSchemaTitle = getUiOptions(get(uiSchema, `${property.replace(/^\./, '')}`)).title;
if (uiSchemaTitle === undefined) {
const uiSchemaPath = schemaPath
.replace(/\/properties\//g, '/')
.split('/')
.slice(1, -1)
.concat([currentProperty]);
uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title;
}

if (uiSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`);
} else {
const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']);

if (parentSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`);
const rawPropertyNames: string[] = [
...(params.deps?.split(', ') || []),
params.missingProperty,
params.property,
].filter((item) => item);

if (rawPropertyNames.length > 0) {
rawPropertyNames.forEach((currentProperty) => {
const path = property ? `${property}.${currentProperty}` : currentProperty;
let uiSchemaTitle = getUiOptions(get(uiSchema, `${path.replace(/^\./, '')}`)).title;
if (uiSchemaTitle === undefined) {
const uiSchemaPath = schemaPath
.replace(/\/properties\//g, '/')
.split('/')
.slice(1, -1)
.concat([currentProperty]);
uiSchemaTitle = getUiOptions(get(uiSchema, uiSchemaPath)).title;
}
}
if (uiSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${uiSchemaTitle}'`);
} else {
const parentSchemaTitle = get(parentSchema, [PROPERTIES_KEY, currentProperty, 'title']);
if (parentSchemaTitle) {
message = message.replace(`'${currentProperty}'`, `'${parentSchemaTitle}'`);
}
}
});

stack = message;
} else {
Expand All @@ -75,6 +79,10 @@ export function transformRJSFValidationErrors<
}
}

if ('missingProperty' in params) {
property = property ? `${property}.${params.missingProperty}` : params.missingProperty;
}

// put data in expected format
return {
name: keyword,
Expand Down
28 changes: 22 additions & 6 deletions packages/validator-ajv8/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,35 @@ export default class AJV8Validator<T = any, S extends StrictRJSFSchema = RJSFSch
let errors;
if (compiledValidator) {
if (typeof this.localizer === 'function') {
// Missing properties need to be enclosed with quotes so that
// Properties need to be enclosed with quotes so that
// `AJV8Validator#transformRJSFValidationErrors` replaces property names
// with `title` or `ui:title`. See #4348, #4349, and #4387.
// with `title` or `ui:title`. See #4348, #4349, #4387, and #4402.
(compiledValidator.errors ?? []).forEach((error) => {
if (error.params?.missingProperty) {
error.params.missingProperty = `'${error.params.missingProperty}'`;
['missingProperty', 'property'].forEach((key) => {
if (error.params?.[key]) {
error.params[key] = `'${error.params[key]}'`;
}
});
if (error.params?.deps) {
error.params.deps = error.params.deps
.split(', ')
.map((v: string) => `'${v}'`)
.join(', ');
}
});
this.localizer(compiledValidator.errors);
// Revert to originals
(compiledValidator.errors ?? []).forEach((error) => {
if (error.params?.missingProperty) {
error.params.missingProperty = error.params.missingProperty.slice(1, -1);
['missingProperty', 'property'].forEach((key) => {
if (error.params?.[key]) {
error.params[key] = error.params[key].slice(1, -1);
}
});
if (error.params?.deps) {
error.params.deps = error.params.deps
.split(', ')
.map((v: string) => v.slice(1, -1))
.join(', ');
}
});
}
Expand Down
235 changes: 235 additions & 0 deletions packages/validator-ajv8/test/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UiSchema,
ValidatorType,
} from '@rjsf/utils';
import localize from 'ajv-i18n';

import AJV8Validator from '../src/validator';
import { Localizer } from '../src';
Expand Down Expand Up @@ -2252,6 +2253,240 @@ describe('AJV8Validator', () => {
});
});
});
describe('validating dependencies', () => {
beforeAll(() => {
validator = new AJV8Validator({ AjvClass: Ajv2019 }, localize.en as Localizer);
});
it('should return an error when a dependent is missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage = "must have property 'billingAddress' when property 'creditCard' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('billingAddress');
});
it('should return an error when multiple dependents are missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
holderName: {
type: 'string',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage = "must have properties 'holderName', 'billingAddress' when property 'creditCard' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
holderName: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
});
it('should return an error with title when a dependent is missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
title: 'Credit card',
},
billingAddress: {
type: 'string',
title: 'Billing address',
},
},
dependentRequired: {
creditCard: ['billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage = "must have property 'Billing address' when property 'Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('billingAddress');
});
it('should return an error with titles when multiple dependents are missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
title: 'Credit card',
},
holderName: {
type: 'string',
title: 'Holder name',
},
billingAddress: {
type: 'string',
title: 'Billing address',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema);
const errMessage =
"must have properties 'Holder name', 'Billing address' when property 'Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
holderName: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
});
it('should return an error with uiSchema title when a dependent is missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['billingAddress'],
},
};
const uiSchema: UiSchema = {
creditCard: {
'ui:title': 'uiSchema Credit card',
},
billingAddress: {
'ui:title': 'uiSchema Billing address',
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema);
const errMessage =
"must have property 'uiSchema Billing address' when property 'uiSchema Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('billingAddress');
});
it('should return an error with uiSchema titles when multiple dependents are missing', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
holderName: {
type: 'string',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const uiSchema: UiSchema = {
creditCard: {
'ui:title': 'uiSchema Credit card',
},
holderName: {
'ui:title': 'uiSchema Holder name',
},
billingAddress: {
'ui:title': 'uiSchema Billing address',
},
};
const errors = validator.validateFormData({ creditCard: 1234567890 }, schema, undefined, undefined, uiSchema);
const errMessage =
"must have properties 'uiSchema Holder name', 'uiSchema Billing address' when property 'uiSchema Credit card' is present";
expect(errors.errors[0].message).toEqual(errMessage);
expect(errors.errors[0].stack).toEqual(errMessage);
expect(errors.errorSchema).toEqual({
billingAddress: {
__errors: [errMessage],
},
holderName: {
__errors: [errMessage],
},
});
expect(errors.errors[0].params.deps).toEqual('holderName, billingAddress');
});
it('should handle the case when errors are not present', () => {
schema = {
type: 'object',
properties: {
creditCard: {
type: 'number',
},
holderName: {
type: 'string',
},
billingAddress: {
type: 'string',
},
},
dependentRequired: {
creditCard: ['holderName', 'billingAddress'],
},
};
const errors = validator.validateFormData(
{
creditCard: 1234567890,
holderName: 'Alice',
billingAddress: 'El Camino Real',
},
schema
);
expect(errors.errors).toHaveLength(0);
});
});
});
describe('validator.validateFormData(), custom options, localizer and Ajv2020', () => {
let validator: AJV8Validator;
Expand Down

0 comments on commit caa59ca

Please sign in to comment.