Skip to content

Commit

Permalink
Packages/utils/schema (#2877)
Browse files Browse the repository at this point in the history
* Implemented the utils package
- Refactored the `packages/core/src/utils.js` file into separate typescript files in the new `packages/utils/src`

* - More changes

* - Added all of the non-validation-based utilities and many of the tests

* - removed schema related files temporarily

* - revert `package-lock.json` for antd, core and playground

* - The schema utilities that require validation

* - More changes

* - completed conversion to Typescript with some changes that were needed

* - Updated various files to make some parameters optional, to add generics typing where missed
- Created an `index.ts` in the `schema` subdirectory that exports all of the schema related functions
- Updated `types.ts` to add the new `SchemaUtilsType` interface
- Implemented a `createSchemaUtils()` function that returns a `SchemaUtilsType` interface given a `ValidatorType` and a `rootSchema`

* - Implemented createSchemaUtils
- Began testing the schema-based utils

* - More fixes and adding missing generics

* - Converted all the schema based tests over still missing 100% coverage

* - Completed 100% unit testing

* - Added documentation for most of the files and cleaned up some optionality on some arguments

* - Completed all of the documentation of the schema-based utils

* - Changed the `_NAME` constants to `_KEY`

* - Added missing generics on a few types

* - Added `ERRORS_KEY` constant for use in validation

* - Fixed types based on implementation of validator

* - Added missing generic to the `ValidationData` type for `ErrorSchema`

* - Fixed tests for the missing generic type

* - Incorporated #2876 by making props for UISchemaSubmitButtonOptions optional

* - Added generic `<T>` to the return value of `toIdSchema()` and `toPathSchema()`
  • Loading branch information
heath-freenome authored Jun 24, 2022
1 parent 78688c0 commit ff277f9
Show file tree
Hide file tree
Showing 32 changed files with 5,161 additions and 126 deletions.
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"jest-expect-message"
],
"testMatch": [
"**/test/**/*.test.[jt]s?(x)"
"**/test/**/*.test.ts?(x)"
],
"coverageDirectory": "<rootDir>/coverage/",
"collectCoverage": true,
Expand Down
26 changes: 15 additions & 11 deletions packages/utils/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
export const ADDITIONAL_PROPERTY_FLAG = '__additional_property';
export const ADDITIONAL_PROPERTIES_NAME = 'additionalProperties';
export const ALL_OF_NAME = 'allOf';
export const CONST_NAME = 'const';
export const DEPENDENCIES_NAME = 'dependencies';
export const ITEMS_NAME = 'items';
export const PROPERTIES_NAME = 'properties';
export const SUBMIT_BTN_OPTIONS_NAME = 'submitButtonOptions';
export const REF_NAME = '$ref';
export const UI_FIELD_NAME = 'ui:field';
export const UI_WIDGET_NAME = 'ui:widget';
export const UI_OPTIONS_NAME = 'ui:options';
export const ADDITIONAL_PROPERTIES_KEY = 'additionalProperties';
export const ALL_OF_KEY = 'allOf';
export const ANY_OF_KEY = 'anyOf';
export const CONST_KEY = 'const';
export const DEFAULT_KEY = 'default';
export const DEPENDENCIES_KEY = 'dependencies';
export const ERRORS_KEY = '__errors';
export const ITEMS_KEY = 'items';
export const ONE_OF_KEY = 'oneOf';
export const PROPERTIES_KEY = 'properties';
export const SUBMIT_BTN_OPTIONS_KEY = 'submitButtonOptions';
export const REF_KEY = '$ref';
export const UI_FIELD_KEY = 'ui:field';
export const UI_WIDGET_KEY = 'ui:widget';
export const UI_OPTIONS_KEY = 'ui:options';
154 changes: 154 additions & 0 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { IdSchema, PathSchema, RJSFSchema, SchemaUtilsType, UiSchema, ValidatorType } from './types';
import {
getDefaultFormState,
getDisplayLabel,
getMatchingOption,
isFilesArray,
isMultiSelect,
isSelect,
retrieveSchema,
stubExistingAdditionalProperties,
toIdSchema,
toPathSchema,
} from './schema';

/** The `SchemaUtils` class provides a wrapper around the publically exported APIs in the `utils/schema` directory such
* that one does not have to explicitly pass the `validator` or `rootSchema` to each method. Since both the `validator`
* and `rootSchema` generally does not change across a `Form`, this allows for providing a simplified set of APIs to the
* `@rjsf/core` components and the various themes as well. This class implements the `SchemaUtilsType` interface.
*/
class SchemaUtils<T = any> implements SchemaUtilsType<T> {
rootSchema: RJSFSchema;
validator: ValidatorType;

/** Constructs the `SchemaUtils` instance with the given `validator` and `rootSchema` stored as instance variables
*
* @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs
* @param rootSchema - The root schema that will be forwarded to all the APIs
*/
constructor(validator: ValidatorType, rootSchema: RJSFSchema) {
this.rootSchema = rootSchema;
this.validator = validator;
}

/** Returns the superset of `formData` that includes the given set updated to include any missing fields that have
* computed to have defaults provided in the `schema`.
*
* @param schema - The schema for which the default state is desired
* @param [formData] - The current formData, if any, onto which to provide any missing defaults
* @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults
* @returns - The resulting `formData` with all the defaults provided
*/
getDefaultFormState(schema: RJSFSchema, formData?: T, includeUndefinedValues = false): T | T[] | undefined {
return getDefaultFormState<T>(this.validator, schema, formData, this.rootSchema, includeUndefinedValues);
}

/** Determines whether the combination of `schema` and `uiSchema` properties indicates that the label for the `schema`
* should be displayed in a UI.
*
* @param schema - The schema for which the display label flag is desired
* @param uiSchema - The UI schema from which to derive potentially displayable information
* @returns - True if the label should be displayed or false if it should not
*/
getDisplayLabel<F = any>(schema: RJSFSchema, uiSchema: UiSchema<T, F>) {
return getDisplayLabel<T, F>(this.validator, schema, uiSchema, this.rootSchema);
}

/** Given the `formData` and list of `options`, attempts to find the index of the option that best matches the data.
*
* @param formData - The current formData, if any, onto which to provide any missing defaults
* @param options - The list of options to find a matching options from
* @returns - The index of the matched option or 0 if none is available
*/
getMatchingOption(formData: T, options: RJSFSchema[]) {
return getMatchingOption<T>(this.validator, formData, options, this.rootSchema);
}

/** Checks to see if the `schema` and `uiSchema` combination represents an array of files
*
* @param schema - The schema for which check for array of files flag is desired
* @param uiSchema - The UI schema from which to check the widget
* @returns - True if schema/uiSchema contains an array of files, otherwise false
*/
isFilesArray<F = any>(schema: RJSFSchema, uiSchema: UiSchema<T, F>) {
return isFilesArray<T, F>(this.validator, schema, uiSchema, this.rootSchema);
}

/** Checks to see if the `schema` combination represents a multi-select
*
* @param schema - The schema for which check for a multi-select flag is desired
* @returns - True if schema contains a multi-select, otherwise false
*/
isMultiSelect(schema: RJSFSchema) {
return isMultiSelect<T>(this.validator, schema, this.rootSchema);
}

/** Checks to see if the `schema` combination represents a select
*
* @param schema - The schema for which check for a select flag is desired
* @returns - True if schema contains a select, otherwise false
*/
isSelect(schema: RJSFSchema) {
return isSelect<T>(this.validator, schema, this.rootSchema);
}

/** Retrieves an expanded schema that has had all of its conditions, additional properties, references and
* dependencies resolved and merged into the `schema` given a `rawFormData` that is used to do the potentially
* recursive resolution.
*
* @param schema - The schema for which retrieving a schema is desired
* @param [rawFormData] - The current formData, if any, to assist retrieving a schema
* @returns - The schema having its conditions, additional properties, references and dependencies resolved
*/
retrieveSchema(schema: RJSFSchema, rawFormData: 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
* @param [id] - The base id for the schema
* @param [formData] - The current formData, if any, onto which to provide any missing defaults
* @param [idPrefix='root'] - The prefix to use for the id
* @param [idSeparator='_'] - The separator to use for the path segments in the id
* @returns - The `IdSchema` object for the `schema`
*/
toIdSchema(
schema: RJSFSchema, id?: string | null, formData?: T, idPrefix = 'root', idSeparator = '_'
): IdSchema<T> {
return toIdSchema<T>(this.validator, schema, id, this.rootSchema, formData, idPrefix, idSeparator);
}

/** Generates an `PathSchema` object for the `schema`, recursively
*
* @param schema - The schema for which the display label flag is desired
* @param [name] - The base name for the schema
* @param [formData] - The current formData, if any, onto which to provide any missing defaults
* @returns - The `PathSchema` object for the `schema`
*/
toPathSchema(schema: RJSFSchema, name?: string, formData?: T): PathSchema<T> {
return toPathSchema<T>(this.validator, schema, name, this.rootSchema, formData);
}
}

/** Creates a `SchemaUtilsType` interface that is based around the given `validator` and `rootSchema` parameters. The
* resulting interface implementation will forward the `validator` and `rootSchema` to all the wrapped APIs.
*
* @param validator - an implementation of the `ValidatorType` interface that will be forwarded to all the APIs
* @param rootSchema - The root schema that will be forwarded to all the APIs
*/
export default function createSchemaUtils<T = any>(
validator: ValidatorType, rootSchema: RJSFSchema
): SchemaUtilsType<T> {
return new SchemaUtils<T>(validator, rootSchema);
}
27 changes: 21 additions & 6 deletions packages/utils/src/findSchemaDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import jsonpointer from 'jsonpointer';
import omit from 'lodash/omit';

import { REF_NAME } from './constants';
import { RJSFSchema } from './types';
import { REF_KEY } from './constants';
import { GenericObjectType, RJSFSchema } from './types';

/** Splits out the value at the `key` in `object` from the `object`, returning an array that contains in the first
* location, the `object` minus the `key: value` and in the second location the `value`.
*
* @param key - The key from the object to extract
* @param object - The object from which to extract the element
* @returns - An array with the first value being the object minus the `key` element and the second element being the
* value from `object[key]`
*/
export function splitKeyElementFromObject(key: string, object: GenericObjectType) {
const value = object[key];
const remaining = omit(object, [key]);
return [remaining, value];
}

/** Given the name of a `$ref` from within a schema, using the `rootSchema`, look up and return the sub-schema using the
* path provided by that reference. If `#` is not the first character of the reference, or the path does not exist in
Expand All @@ -24,10 +38,11 @@ export default function findSchemaDefinition($ref?: string, rootSchema: RJSFSche
if (current === undefined) {
throw new Error(`Could not find a definition for ${$ref}.`);
}
if (current[REF_NAME]) {
const subSchema = findSchemaDefinition(current[REF_NAME]!, rootSchema);
if (Object.keys(current).length > 1) {
return { ...omit(current, [REF_NAME]), ...subSchema };
if (current[REF_KEY]) {
const [remaining, theRef] = splitKeyElementFromObject(REF_KEY, current);
const subSchema = findSchemaDefinition(theRef, rootSchema);
if (Object.keys(remaining).length > 0) {
return { ...remaining, ...subSchema };
}
return subSchema;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/getSubmitButtonOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SUBMIT_BTN_OPTIONS_NAME } from './constants';
import { SUBMIT_BTN_OPTIONS_KEY } from './constants';
import getUiOptions from './getUiOptions';
import { UiSchema, UISchemaSubmitButtonOptions } from './types';

Expand All @@ -19,8 +19,8 @@ export const DEFAULT_OPTIONS = {
*/
export default function getSubmitButtonOptions<T = any, F = any>(uiSchema: UiSchema<T, F>) {
const uiOptions = getUiOptions<T, F>(uiSchema);
if (uiOptions && uiOptions[SUBMIT_BTN_OPTIONS_NAME]) {
const options = uiOptions[SUBMIT_BTN_OPTIONS_NAME] as UISchemaSubmitButtonOptions;
if (uiOptions && uiOptions[SUBMIT_BTN_OPTIONS_KEY]) {
const options = uiOptions[SUBMIT_BTN_OPTIONS_KEY] as UISchemaSubmitButtonOptions;
return { ...DEFAULT_OPTIONS, ...options };
}

Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/getUiOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UI_OPTIONS_NAME, UI_WIDGET_NAME } from './constants';
import { UI_OPTIONS_KEY, UI_WIDGET_KEY } from './constants';
import isObject from './isObject';
import { UIOptionsType, UiSchema } from './types';

Expand All @@ -13,11 +13,11 @@ export default function getUiOptions<T = any, F = any>(uiSchema: UiSchema<T, F>)
.filter(key => key.indexOf('ui:') === 0)
.reduce((options, key) => {
const value = uiSchema[key];
if (key === UI_WIDGET_NAME && isObject(value)) {
if (key === UI_WIDGET_KEY && isObject(value)) {
console.error('Setting options via ui:widget object is no longer supported, use ui:options instead');
return options;
}
if (key === UI_OPTIONS_NAME && isObject(value)) {
if (key === UI_OPTIONS_KEY && isObject(value)) {
return { ...options, ...value };
}
return { ...options, [key.substring(3)]: value };
Expand Down
83 changes: 4 additions & 79 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import allowAdditionalItems from './allowAdditionalItems';
import asNumber from './asNumber';
import canExpand from './canExpand';
import createSchemaUtils from './createSchemaUtils';
import dataURItoBlob from './dataURItoBlob';
import deepEquals from './deepEquals';
import findSchemaDefinition from './findSchemaDefinition';
Expand Down Expand Up @@ -29,92 +30,16 @@ import toConstant from './toConstant';
import toDateString from './toDateString';
import utcToLocal from './utcToLocal';

import {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
CustomValidator,
DateObject,
DescriptionFieldProps,
ErrorListProps,
ErrorSchema,
ErrorTransformer,
Field,
FieldError,
FieldErrors,
FieldId,
FieldPath,
FieldProps,
FieldTemplateProps,
FieldValidation,
FormValidation,
GenericObjectType,
IChangeEvent,
IdSchema,
ObjectFieldTemplatePropertyType,
ObjectFieldTemplateProps,
PathSchema,
RangeSpecType,
Registry,
RegistryFieldsType,
RegistryWidgetsType,
RJSFSchema,
RJSFValidationError,
TitleFieldProps,
UiSchema,
UIOptionsType,
UISchemaSubmitButtonOptions,
ValidationData,
ValidatorType,
Widget,
WidgetProps,
} from './types';

export type {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
CustomValidator,
DateObject,
DescriptionFieldProps,
ErrorListProps,
ErrorSchema,
ErrorTransformer,
Field,
FieldError,
FieldErrors,
FieldId,
FieldPath,
FieldProps,
FieldTemplateProps,
FieldValidation,
FormValidation,
GenericObjectType,
IChangeEvent,
IdSchema,
ObjectFieldTemplatePropertyType,
ObjectFieldTemplateProps,
PathSchema,
RangeSpecType,
Registry,
RegistryFieldsType,
RegistryWidgetsType,
RJSFSchema,
RJSFValidationError,
TitleFieldProps,
UiSchema,
UIOptionsType,
UISchemaSubmitButtonOptions,
ValidationData,
ValidatorType,
Widget,
WidgetProps,
};
export * from './types';

export * from './constants';
export * from './schema';

export {
allowAdditionalItems,
asNumber,
canExpand,
createSchemaUtils,
dataURItoBlob,
deepEquals,
findSchemaDefinition,
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/isConstant.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CONST_NAME } from './constants';
import { CONST_KEY } from './constants';
import { RJSFSchema } from './types';

/**
Expand All @@ -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_NAME);
return (Array.isArray(schema.enum) && schema.enum.length === 1) || schema.hasOwnProperty(CONST_KEY);
}
Loading

0 comments on commit ff277f9

Please sign in to comment.