Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "feat: improve deepEquals performance (#4292)" #4300

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 5.21.1

## @rjsf/utils

- Revert of updating `deepEquals()` from [#4292]

## @validator-ajv8

- Revert of using `deepEquals()` instead of `lodash.isEqual()` from [#4292]

# 5.21.0

## @rjsf/core
Expand Down
10 changes: 0 additions & 10 deletions package-lock.json

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

1 change: 0 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"react": "^16.14.0 || >=17"
},
"dependencies": {
"fast-equals": "^5.0.1",
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
"lodash": "^4.17.21",
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
ValidatorType,
} from './types';
import {
getClosestMatchingOption,
getDefaultFormState,
getDisplayLabel,
getClosestMatchingOption,
getFirstMatchingOption,
getMatchingOption,
isFilesArray,
Expand Down
47 changes: 10 additions & 37 deletions packages/utils/src/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
import { createCustomEqual, State } from 'fast-equals';
import isEqualWith from 'lodash/isEqualWith';

/** Check if all parameters are typeof function.
*
* @param a - The first element to check typeof
* @param b - The second element to check typeof
* @returns - if typeof a and b are equal to function return true, otherwise false
*/
function isFunctions(a: any, b: any) {
return typeof a === 'function' && typeof b === 'function';
}

/** Implements a deep equals using the `fast-equal.createCustomEqual` function, that provides a customized comparator that
* assumes all functions in objects are equivalent.
*
* @param a - The first element to compare
* @param b - The second element to compare
* @returns - True if the `a` and `b` are deeply equal, false otherwise
*/
const customDeepEqual = createCustomEqual({
createInternalComparator: (comparator: (a: any, b: any, state: State<any>) => boolean) => {
return (a: any, b: any, _idxA: any, _idxB: any, _parentA: any, _parentB: any, state: State<any>) => {
if (isFunctions(a, b)) {
// Assume all functions are equivalent
// see https://github.com/rjsf-team/react-jsonschema-form/issues/255
return true;
}

return comparator(a, b, state);
};
},
});

/** Implements a deep equals using the `fast-equal.createCustomEqual` function, that provides a customized comparator that
/** Implements a deep equals using the `lodash.isEqualWith` function, that provides a customized comparator that
* assumes all functions are equivalent.
*
* @param a - The first element to compare
* @param b - The second element to compare
* @returns - True if the `a` and `b` are deeply equal, false otherwise
*/
export default function deepEquals(a: any, b: any): boolean {
if (isFunctions(a, b)) {
return true;
}
return customDeepEqual(a, b);
return isEqualWith(a, b, (obj: any, other: any) => {
if (typeof obj === 'function' && typeof other === 'function') {
// Assume all functions are equivalent
// see https://github.com/rjsf-team/react-jsonschema-form/issues/255
return true;
}
return undefined; // fallback to default isEquals behavior
});
}
7 changes: 4 additions & 3 deletions packages/utils/src/enumOptionsDeselectValue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import isEqual from 'lodash/isEqual';

import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
import enumOptionsValueForIndex from './enumOptionsValueForIndex';
import deepEquals from './deepEquals';

/** Removes the enum option value at the `valueIndex` from the currently `selected` (list of) value(s). If `selected` is
* a list, then that list is updated to remove the enum option value with the `valueIndex` in `allEnumOptions`. If it is
Expand All @@ -21,7 +22,7 @@ export default function enumOptionsDeselectValue<S extends StrictRJSFSchema = RJ
): EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][] | undefined {
const value = enumOptionsValueForIndex<S>(valueIndex, allEnumOptions);
if (Array.isArray(selected)) {
return selected.filter((v) => !deepEquals(v, value));
return selected.filter((v) => !isEqual(v, value));
}
return deepEquals(value, selected) ? undefined : selected;
return isEqual(value, selected) ? undefined : selected;
}
7 changes: 4 additions & 3 deletions packages/utils/src/enumOptionsIsSelected.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import deepEquals from './deepEquals';
import isEqual from 'lodash/isEqual';

import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';

/** Determines whether the given `value` is (one of) the `selected` value(s).
Expand All @@ -12,7 +13,7 @@ export default function enumOptionsIsSelected<S extends StrictRJSFSchema = RJSFS
selected: EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][]
) {
if (Array.isArray(selected)) {
return selected.some((sel) => deepEquals(sel, value));
return selected.some((sel) => isEqual(sel, value));
}
return deepEquals(selected, value);
return isEqual(selected, value);
}
6 changes: 3 additions & 3 deletions packages/utils/src/parser/ParserValidator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';

import { ID_KEY } from '../constants';
import hashForSchema from '../hashForSchema';
Expand All @@ -14,7 +15,6 @@ import {
ValidationData,
ValidatorType,
} from '../types';
import deepEquals from '../deepEquals';

/** The type of the map of schema hash to schema
*/
Expand Down Expand Up @@ -67,7 +67,7 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
const existing = this.schemaMap[key];
if (!existing) {
this.schemaMap[key] = identifiedSchema;
} else if (!deepEquals(existing, identifiedSchema)) {
} else if (!isEqual(existing, identifiedSchema)) {
console.error('existing schema:', JSON.stringify(existing, null, 2));
console.error('new schema:', JSON.stringify(identifiedSchema, null, 2));
throw new Error(
Expand All @@ -91,7 +91,7 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
* @throws - Error when the given `rootSchema` differs from the root schema provided during construction
*/
isValid(schema: S, _formData: T, rootSchema: S): boolean {
if (!deepEquals(rootSchema, this.rootSchema)) {
if (!isEqual(rootSchema, this.rootSchema)) {
throw new Error('Unexpectedly calling isValid() with a rootSchema that differs from the construction rootSchema');
}
this.addSchema(schema, hashForSchema<S>(schema));
Expand Down
8 changes: 4 additions & 4 deletions packages/utils/src/parser/schemaParser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import forEach from 'lodash/forEach';
import isEqual from 'lodash/isEqual';

import { FormContextType, RJSFSchema, StrictRJSFSchema } from '../types';
import { ITEMS_KEY, PROPERTIES_KEY } from '../constants';
import { PROPERTIES_KEY, ITEMS_KEY } from '../constants';
import ParserValidator, { SchemaMap } from './ParserValidator';
import { resolveAnyOrOneOfSchemas, retrieveSchemaInternal } from '../schema/retrieveSchema';
import deepEquals from '../deepEquals';
import { retrieveSchemaInternal, resolveAnyOrOneOfSchemas } from '../schema/retrieveSchema';

/** Recursive function used to parse the given `schema` belonging to the `rootSchema`. The `validator` is used to
* capture the sub-schemas that the `isValid()` function is called with. For each schema returned by the
Expand All @@ -24,7 +24,7 @@ function parseSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
) {
const schemas = retrieveSchemaInternal<T, S, F>(validator, schema, rootSchema, undefined, true);
schemas.forEach((schema) => {
const sameSchemaIndex = recurseList.findIndex((item) => deepEquals(item, schema));
const sameSchemaIndex = recurseList.findIndex((item) => isEqual(item, schema));
if (sameSchemaIndex === -1) {
recurseList.push(schema);
const allOptions = resolveAnyOrOneOfSchemas<T, S, F>(validator, schema, rootSchema, true);
Expand Down
13 changes: 5 additions & 8 deletions packages/utils/src/schema/retrieveSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import set from 'lodash/set';
import times from 'lodash/times';
import transform from 'lodash/transform';
Expand All @@ -14,10 +15,10 @@ import {
ANY_OF_KEY,
DEPENDENCIES_KEY,
IF_KEY,
ITEMS_KEY,
ONE_OF_KEY,
PROPERTIES_KEY,
REF_KEY,
PROPERTIES_KEY,
ITEMS_KEY,
} from '../constants';
import findSchemaDefinition, { splitKeyElementFromObject } from '../findSchemaDefinition';
import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema';
Expand All @@ -26,7 +27,6 @@ import isObject from '../isObject';
import mergeSchemas from '../mergeSchemas';
import { FormContextType, GenericObjectType, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types';
import getFirstMatchingOption from './getFirstMatchingOption';
import deepEquals from '../deepEquals';

/** Retrieves an expanded schema that has had all of its conditions, additional properties, references and dependencies
* resolved and merged into the `schema` given a `validator`, `rootSchema` and `rawFormData` that is used to do the
Expand Down Expand Up @@ -196,10 +196,7 @@ export function resolveSchema<T = any, S extends StrictRJSFSchema = RJSFSchema,
)
);
const allPermutations = getAllPermutationsOfXxxOf<S>(allOfSchemaElements);
return allPermutations.map((permutation) => ({
...schema,
allOf: permutation,
}));
return allPermutations.map((permutation) => ({ ...schema, allOf: permutation }));
}
// No $ref or dependencies or allOf attribute was found, returning the original schema.
return [schema];
Expand Down Expand Up @@ -296,7 +293,7 @@ export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
};
}

return deepEquals(schema, resolvedSchema) ? schema : resolvedSchema;
return isEqual(schema, resolvedSchema) ? schema : resolvedSchema;
}

/** Creates new 'properties' items for each key in the `formData`
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/schema/toIdSchema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';

import { ALL_OF_KEY, DEPENDENCIES_KEY, ID_KEY, ITEMS_KEY, PROPERTIES_KEY, REF_KEY } from '../constants';
import isObject from '../isObject';
import { FormContextType, GenericObjectType, IdSchema, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types';
import retrieveSchema from './retrieveSchema';
import getSchemaType from '../getSchemaType';
import deepEquals from '../deepEquals';

/** An internal helper that generates an `IdSchema` object for the `schema`, recursively with protection against
* infinite recursion
Expand All @@ -32,7 +32,7 @@ function toIdSchemaInternal<T = any, S extends StrictRJSFSchema = RJSFSchema, F
): IdSchema<T> {
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
const sameSchemaIndex = _recurseList.findIndex((item) => deepEquals(item, _schema));
const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema));
if (sameSchemaIndex === -1) {
return toIdSchemaInternal<T, S, F>(
validator,
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/schema/toPathSchema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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,
Expand All @@ -17,7 +18,6 @@ import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema'
import { FormContextType, GenericObjectType, PathSchema, RJSFSchema, StrictRJSFSchema, ValidatorType } from '../types';
import getClosestMatchingOption from './getClosestMatchingOption';
import retrieveSchema from './retrieveSchema';
import deepEquals from '../deepEquals';

/** An internal helper that generates an `PathSchema` object for the `schema`, recursively with protection against
* infinite recursion
Expand All @@ -40,7 +40,7 @@ function toPathSchemaInternal<T = any, S extends StrictRJSFSchema = RJSFSchema,
): PathSchema<T> {
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
const sameSchemaIndex = _recurseList.findIndex((item) => deepEquals(item, _schema));
const sameSchemaIndex = _recurseList.findIndex((item) => isEqual(item, _schema));
if (sameSchemaIndex === -1) {
return toPathSchemaInternal<T, S, F>(
validator,
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/test/deepEquals.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { deepEquals } from '../src';

describe('deepEquals()', () => {
// Note: deepEquals implementation uses fast-equal.createCustomEqual, so we focus on the behavioral differences we introduced.
// Note: deepEquals implementation uses isEqualWith, so we focus on the behavioral differences we introduced.
it('should assume functions are always equivalent', () => {
expect(
deepEquals(
Expand Down
8 changes: 4 additions & 4 deletions packages/validator-ajv8/src/precompiledValidator.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { ErrorObject } from 'ajv';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import {
CustomValidator,
deepEquals,
ErrorSchema,
ErrorTransformer,
FormContextType,
hashForSchema,
ID_KEY,
JUNK_OPTION_ID,
retrieveSchema,
RJSFSchema,
StrictRJSFSchema,
toErrorList,
UiSchema,
ValidationData,
ValidatorType,
retrieveSchema,
} from '@rjsf/utils';

import { CompiledValidateFunction, Localizer, ValidatorFunctions } from './types';
Expand Down Expand Up @@ -92,10 +92,10 @@ export default class AJV8PrecompiledValidator<
* @param [formData] - The form data to validate if any
*/
ensureSameRootSchema(schema: S, formData?: T) {
if (!deepEquals(schema, this.rootSchema)) {
if (!isEqual(schema, this.rootSchema)) {
// Resolve the root schema with the passed in form data since that may affect the resolution
const resolvedRootSchema = retrieveSchema(this, this.rootSchema, this.rootSchema, formData);
if (!deepEquals(schema, resolvedRootSchema)) {
if (!isEqual(schema, resolvedRootSchema)) {
throw new Error(
'The schema associated with the precompiled validator differs from the rootSchema provided for validation'
);
Expand Down