Skip to content

Refactored schema to support running schema tests from other packages #2899

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

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
11 changes: 0 additions & 11 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
isMultiSelect,
isSelect,
retrieveSchema,
stubExistingAdditionalProperties,
toIdSchema,
toPathSchema,
} from './schema';
Expand Down Expand Up @@ -104,16 +103,6 @@ class SchemaUtils<T = any> implements SchemaUtilsType<T> {
return retrieveSchema<T>(this.validator, schema, this.rootSchema, rawFormData);
}

/** Creates new 'properties' items for each key in the `formData`
*
* @param schema - The schema for which the existing additional properties is desired
* @param [formData] - The current formData, if any, to assist retrieving a schema
* @returns - The updated schema with additional properties stubbed
*/
stubExistingAdditionalProperties(schema: RJSFSchema, formData?: T) {
return stubExistingAdditionalProperties<T>(this.validator, schema, this.rootSchema, formData);
}

/** Generates an `IdSchema` object for the `schema`, recursively
*
* @param schema - The schema for which the display label flag is desired
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/getWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,17 @@ export default function getWidget<T = any, F = any>(
throw new Error(`Unsupported widget definition: ${typeof widget}`);
}

if (registeredWidgets.hasOwnProperty(widget)) {
if (widget in registeredWidgets) {
const registeredWidget = registeredWidgets[widget];
return getWidget<T, F>(schema, registeredWidget, registeredWidgets);
}

if (typeof type === 'string') {
if (!widgetMap.hasOwnProperty(type)) {
if (!(type in widgetMap)) {
throw new Error(`No widget for type '${type}'`);
}

if (widgetMap[type].hasOwnProperty(widget)) {
if (widget in widgetMap[type]) {
const registeredWidget = registeredWidgets[widgetMap[type][widget]];
return getWidget<T, F>(schema, registeredWidget, registeredWidgets);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/isConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ import { RJSFSchema } from './types';
* @returns - True if the `schema` has a single constant value, false otherwise
*/
export default function isConstant(schema: RJSFSchema) {
return (Array.isArray(schema.enum) && schema.enum.length === 1) || schema.hasOwnProperty(CONST_KEY);
return (Array.isArray(schema.enum) && schema.enum.length === 1) || CONST_KEY in schema;
}
2 changes: 1 addition & 1 deletion packages/utils/src/mergeObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function mergeObjects(obj1: GenericObjectType, obj2: GenericObjec
return Object.keys(obj2).reduce((acc, key) => {
const left = obj1 ? obj1[key] : {},
right = obj2[key];
if (obj1 && obj1.hasOwnProperty(key) && isObject(right)) {
if (obj1 && key in obj1 && isObject(right)) {
acc[key] = mergeObjects(left, right, concatArrays);
} else if (concatArrays && Array.isArray(left) && Array.isArray(right)) {
acc[key] = left.concat(right);
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/mergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import union from 'lodash/union';

import getSchemaType from './getSchemaType';
import isObject from './isObject';
import { GenericObjectType } from 'types';
import { GenericObjectType } from './types';

/** Recursively merge deeply nested schemas. The difference between mergeSchemas and mergeObjects is that mergeSchemas
* only concats arrays for values under the 'required' keyword, and when it does, it doesn't include duplicate values.
Expand All @@ -16,7 +16,7 @@ export default function mergeSchemas(obj1: GenericObjectType, obj2: GenericObjec
return Object.keys(obj2).reduce((acc, key) => {
const left = obj1 ? obj1[key] : {},
right = obj2[key];
if (obj1 && obj1.hasOwnProperty(key) && isObject(right)) {
if (obj1 && key in obj1 && isObject(right)) {
acc[key] = mergeSchemas(left, right);
} else if (
obj1 &&
Expand Down
2 changes: 0 additions & 2 deletions packages/utils/src/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import isFilesArray from './isFilesArray';
import isMultiSelect from './isMultiSelect';
import isSelect from './isSelect';
import retrieveSchema from './retrieveSchema';
import stubExistingAdditionalProperties from './stubExistingAdditionalProperties';
import toIdSchema from './toIdSchema';
import toPathSchema from './toPathSchema';

Expand All @@ -17,7 +16,6 @@ export {
isMultiSelect,
isSelect,
retrieveSchema,
stubExistingAdditionalProperties,
toIdSchema,
toPathSchema,
};
78 changes: 70 additions & 8 deletions packages/utils/src/schema/retrieveSchema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import get from 'lodash/get';
import set from 'lodash/set';
import mergeAllOf from 'json-schema-merge-allof';

import { ADDITIONAL_PROPERTIES_KEY, ALL_OF_KEY, DEPENDENCIES_KEY, REF_KEY } from '../constants';
import {
ADDITIONAL_PROPERTIES_KEY,
ADDITIONAL_PROPERTY_FLAG,
ALL_OF_KEY,
DEPENDENCIES_KEY,
REF_KEY
} from '../constants';
import findSchemaDefinition, { splitKeyElementFromObject } from '../findSchemaDefinition';
import guessType from '../guessType';
import isObject from '../isObject';
import mergeSchemas from '../mergeSchemas';
import { GenericObjectType, RJSFSchema, RJSFSchemaDefinition, ValidatorType } from '../types';
import getMatchingOption from './getMatchingOption';
import stubExistingAdditionalProperties from './stubExistingAdditionalProperties';

/** Resolves a conditional block (if/else/then) by removing the condition and merging the appropriate conditional branch
* with the rest of the schema
Expand Down Expand Up @@ -58,14 +65,14 @@ export function resolveCondition<T = any>(
export function resolveSchema<T = any>(
validator: ValidatorType, schema: RJSFSchema, rootSchema: RJSFSchema = {}, formData?: T
): RJSFSchema {
if (schema.hasOwnProperty(REF_KEY)) {
if (REF_KEY in schema) {
return resolveReference<T>(validator, schema, rootSchema, formData);
}
if (schema.hasOwnProperty(DEPENDENCIES_KEY)) {
if (DEPENDENCIES_KEY in schema) {
const resolvedSchema = resolveDependencies<T>(validator, schema, rootSchema, formData);
return retrieveSchema<T>(validator, resolvedSchema, rootSchema, formData);
}
if (schema.hasOwnProperty(ALL_OF_KEY)) {
if (ALL_OF_KEY in schema) {
return {
...schema,
allOf: schema.allOf!.map((allOfSubschema) =>
Expand Down Expand Up @@ -101,6 +108,61 @@ export function resolveReference<T = any>(
);
}

/** Creates new 'properties' items for each key in the `formData`
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param theSchema - The schema for which the existing additional properties is desired
* @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @param validator
* @param [aFormData] - The current formData, if any, to assist retrieving a schema
* @returns - The updated schema with additional properties stubbed
*/
export function stubExistingAdditionalProperties<T = any>(
validator: ValidatorType,
theSchema: RJSFSchema,
rootSchema?: RJSFSchema,
aFormData?: T
): RJSFSchema {
// Clone the schema so we don't ruin the consumer's original
const schema = {
...theSchema,
properties: { ...theSchema.properties },
};

// make sure formData is an object
const formData: GenericObjectType = aFormData && isObject(aFormData) ? aFormData : {};
Object.keys(formData).forEach((key) => {
if (key in schema.properties) {
// No need to stub, our schema already has the property
return;
}

let additionalProperties: RJSFSchema = {};
if (typeof schema.additionalProperties !== 'boolean') {
if (REF_KEY in schema.additionalProperties!) {
additionalProperties = retrieveSchema<T>(
validator,
{ $ref: get(schema.additionalProperties, [REF_KEY]) },
rootSchema,
formData as T
);
} else if ('type' in schema.additionalProperties!) {
additionalProperties = { ...schema.additionalProperties };
} else {
additionalProperties = { type: guessType(get(formData, [key])) };
}
} else {
additionalProperties = { type: guessType(get(formData, [key])) };
}

// The type of our new key should match the additionalProperties value;
schema.properties[key] = additionalProperties;
// Set our additional property flag so we know it was dynamically added
set(schema.properties, [key, ADDITIONAL_PROPERTY_FLAG], true);
});

return schema;
}

/** 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
* potentially recursive resolution.
Expand All @@ -119,7 +181,7 @@ export default function retrieveSchema<T = any>(
}
let resolvedSchema = resolveSchema<T>(validator, schema, rootSchema, rawFormData);

if (schema.hasOwnProperty('if')) {
if ('if' in schema) {
return resolveCondition<T>(validator, schema, rootSchema, rawFormData as T);
}

Expand Down Expand Up @@ -165,7 +227,7 @@ export default function retrieveSchema<T = any>(
}
}
const hasAdditionalProperties =
resolvedSchema.hasOwnProperty(ADDITIONAL_PROPERTIES_KEY) &&
ADDITIONAL_PROPERTIES_KEY in resolvedSchema &&
resolvedSchema.additionalProperties !== false;
if (hasAdditionalProperties) {
return stubExistingAdditionalProperties<T>(
Expand Down Expand Up @@ -309,7 +371,7 @@ export function withDependentSchema<T>(
}
// Resolve $refs inside oneOf.
const resolvedOneOf = oneOf.map((subschema) => {
if (typeof subschema === 'boolean' || !subschema.hasOwnProperty(REF_KEY)) {
if (typeof subschema === 'boolean' || !(REF_KEY in subschema)) {
return subschema;
}
return resolveReference<T>(validator, subschema as RJSFSchema, rootSchema, formData);
Expand Down
62 changes: 0 additions & 62 deletions packages/utils/src/schema/stubExistingAdditionalProperties.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/utils/src/schema/toPathSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export default function toPathSchema<T = any>(
$name: name.replace(/^\./, ''),
} as PathSchema;

if (schema.hasOwnProperty(ADDITIONAL_PROPERTIES_KEY)) {
if (ADDITIONAL_PROPERTIES_KEY in schema) {
set(pathSchema, '__rjsf_additionalProperties', true);
}

if (schema.hasOwnProperty(ITEMS_KEY) && Array.isArray(formData)) {
if (ITEMS_KEY in schema && Array.isArray(formData)) {
formData.forEach((element, i: number) => {
pathSchema[i] = toPathSchema<T>(
validator,
Expand All @@ -51,7 +51,7 @@ export default function toPathSchema<T = any>(
element
);
});
} else if (schema.hasOwnProperty(PROPERTIES_KEY)) {
} else if (PROPERTIES_KEY in schema) {
for (const property in schema.properties) {
const field = get(schema, [PROPERTIES_KEY, property]);
pathSchema[property] = toPathSchema<T>(
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/toConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function toConstant(schema: RJSFSchema) {
if (Array.isArray(schema.enum) && schema.enum.length === 1) {
return schema.enum[0];
}
if (schema.hasOwnProperty(CONST_KEY)) {
if (CONST_KEY in schema) {
return schema.const;
}
throw new Error('schema cannot be inferred as a constant');
Expand Down
1 change: 0 additions & 1 deletion packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,6 @@ export interface SchemaUtilsType<T = any> {
isMultiSelect(schema: RJSFSchema): boolean;
isSelect(schema: RJSFSchema): boolean;
retrieveSchema(schema: RJSFSchema, formData: T): RJSFSchema;
stubExistingAdditionalProperties(schema: RJSFSchema, formData: T): RJSFSchema;
toIdSchema(schema: RJSFSchema, id?: string, formData?: T, idPrefix?: string, idSeparator?: string): IdSchema<T>;
toPathSchema(schema: RJSFSchema, name?: string, formData?: T): PathSchema<T>;
}
24 changes: 24 additions & 0 deletions packages/utils/test/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import getTestValidator from './testUtils/getTestValidator';
import {
getDefaultFormStateTest,
getDisplayLabelTest,
getMatchingOptionTest,
isFilesArrayTest,
isMultiSelectTest,
isSelectTest,
retrieveSchemaTest,
toIdSchemaTest,
toPathSchemaTest,
} from './schema';

const testValidator = getTestValidator({});

getDefaultFormStateTest(testValidator);
getDisplayLabelTest(testValidator);
getMatchingOptionTest(testValidator);
isFilesArrayTest(testValidator);
isMultiSelectTest(testValidator);
isSelectTest(testValidator);
retrieveSchemaTest(testValidator);
toIdSchemaTest(testValidator);
toPathSchemaTest(testValidator);
Loading