diff --git a/CHANGELOG_v6.md b/CHANGELOG_v6.md index 844368cd37..5c56fde4e1 100644 --- a/CHANGELOG_v6.md +++ b/CHANGELOG_v6.md @@ -48,6 +48,7 @@ should change the heading of the (upcoming) version to include a major version b - BREAKING CHANGE: Moved the addition of `Bootstrap 3` classes from the `SchemaField` to the `WrapIfAdditionalTemplate`, thereby affecting all the other themes, fixing [#2280](https://github.com/rjsf-team/react-jsonschema-form/issues/2280) - BREAKING CHANGE: Added `rjsf-` prefix onto the following marker classes used in the fields and templates: - `field`, `field-`, `field-error`, `field-hidden`, `field-array`, `field-array-of-`, `field-array-fixed-items`, `array-item`, `config-error`, `array-item-add`, `array-item-copy`, `array-item-move-down`, `array-item-move-up`, `array-item-remove`, `object-property-expand` +- Added support for `patternProperties` [#1944](https://github.com/rjsf-team/react-jsonschema-form/issues/1944) ## @rjsf/daisyui @@ -111,6 +112,7 @@ should change the heading of the (upcoming) version to include a major version b - BREAKING CHANGE: Removed the deprecated `toErrorList()` function from the `ValidatorType` interface - BREAKING CHANGE: Removed the deprecated `RJSF_ADDITONAL_PROPERTIES_FLAG` constant - Updated the `WrapIfAdditionalTemplateProps` to include `hideError` and `rawErrors` in support of moving `Bootstrap 3` marker classes out of `SchemaField` +- Added support for `patternProperties` [#1944](https://github.com/rjsf-team/react-jsonschema-form/issues/1944) ## @rjsf/validator-ajv6 @@ -130,6 +132,7 @@ should change the heading of the (upcoming) version to include a major version b - Updated the `playground` to add a `Layout Grid` example and made the selected example now be part of the shared export - Replaced Lerna with Nx, updated all lerna commands to use the Nx CLI - BREAKING CHANGE: Updated all `peerDependencies` to change minimal `React` support to `>=18` +- Added documentation and playground example for `patternProperties` # 6.0.0-alpha.0 diff --git a/packages/core/src/components/fields/ObjectField.tsx b/packages/core/src/components/fields/ObjectField.tsx index eedbb7ceb5..0a77d5bbe1 100644 --- a/packages/core/src/components/fields/ObjectField.tsx +++ b/packages/core/src/components/fields/ObjectField.tsx @@ -195,36 +195,40 @@ class ObjectField () => { - if (!schema.additionalProperties) { + if (!(schema.additionalProperties || schema.patternProperties)) { return; } const { formData, onChange, registry } = this.props; const newFormData = { ...formData } as T; - - let type: RJSFSchema['type'] = undefined; - let constValue: RJSFSchema['const'] = undefined; - let defaultValue: RJSFSchema['default'] = undefined; - if (isObject(schema.additionalProperties)) { - type = schema.additionalProperties.type; - constValue = schema.additionalProperties.const; - defaultValue = schema.additionalProperties.default; - let apSchema = schema.additionalProperties; - if (REF_KEY in apSchema) { - const { schemaUtils } = registry; - apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[REF_KEY] } as S, formData); - type = apSchema.type; - constValue = apSchema.const; - defaultValue = apSchema.default; - } - if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) { - type = 'object'; + const newKey = this.getAvailableKey('newKey', newFormData); + if (schema.patternProperties) { + // Cast this to make the `set` work properly + set(newFormData as GenericObjectType, newKey, null); + } else { + let type: RJSFSchema['type'] = undefined; + let constValue: RJSFSchema['const'] = undefined; + let defaultValue: RJSFSchema['default'] = undefined; + if (isObject(schema.additionalProperties)) { + type = schema.additionalProperties.type; + constValue = schema.additionalProperties.const; + defaultValue = schema.additionalProperties.default; + let apSchema = schema.additionalProperties; + if (REF_KEY in apSchema) { + const { schemaUtils } = registry; + apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[REF_KEY] } as S, formData); + type = apSchema.type; + constValue = apSchema.const; + defaultValue = apSchema.default; + } + if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) { + type = 'object'; + } } - } - const newKey = this.getAvailableKey('newKey', newFormData); - const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type); - // Cast this to make the `set` work properly - set(newFormData as GenericObjectType, newKey, newValue); + const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type); + // Cast this to make the `set` work properly + set(newFormData as GenericObjectType, newKey, newValue); + } onChange(newFormData); }; diff --git a/packages/docs/docs/advanced-customization/custom-templates.md b/packages/docs/docs/advanced-customization/custom-templates.md index e02351bd59..835fbd8b6f 100644 --- a/packages/docs/docs/advanced-customization/custom-templates.md +++ b/packages/docs/docs/advanced-customization/custom-templates.md @@ -14,7 +14,7 @@ In version 5, all existing `templates` were consolidated into a new `TemplatesTy They can also be overloaded globally on the `Form` via the `templates` prop as well as globally or per-field through the `uiSchema`. Further, many new templates were added or repurposed from existing `widgets` and `fields` in an effort to simplify the effort needed by theme authors to build new and/or maintain current themes. These new templates can also be overridden by individual users to customize the specific needs of their application. -A special category of templates, `ButtonTemplates`, were also added to support the easy replacement of the `Submit` button on the form, the `Add` and `Remove` buttons associated with `additionalProperties` on objects and elements of arrays, as well as the `Move up` and `Move down` buttons used for reordering arrays. +A special category of templates, `ButtonTemplates`, were also added to support the easy replacement of the `Submit` button on the form, the `Add` and `Remove` buttons associated with `additionalProperties` and `patternProperties` on objects and elements of arrays, as well as the `Move up` and `Move down` buttons used for reordering arrays. This category, unlike the others, can only be overridden globally via the `templates` prop on `Form`. Below is the table that lists all the `templates`, their props interface, their `uiSchema` name and from where they originated in the previous version of RJSF: @@ -461,7 +461,7 @@ The following props are passed to the `BaseInputTemplate`: - `multiple`: A boolean value stating if the widget can accept multiple values; - `onChange`: The value change event handler; call it with the new value every time it changes; - `onChangeOverride`: A `BaseInputTemplate` implements a default `onChange` handler that it passes to the HTML input component to handle the `ChangeEvent`. Sometimes a widget may need to handle the `ChangeEvent` using custom logic. If that is the case, that widget should provide its own handler via this prop; -- `onKeyChange`: The key change event handler (only called for fields with `additionalProperties`); pass the new value every time it changes; +- `onKeyChange`: The key change event handler (only called for fields with `additionalProperties` and `patternProperties`); pass the new value every time it changes; - `onBlur`: The input blur event handler; call it with the widget id and value; - `onFocus`: The input focus event handler; call it with the widget id and value; - `options`: A map of options passed as a prop to the component (see [Custom widget options](./custom-widgets-fields.md#custom-widget-options)). @@ -807,7 +807,7 @@ The following props are passed to each `ObjectFieldTemplate` as defined by the ` - `description`: A string value containing the description for the object. - `disabled`: A boolean value stating if the object is disabled. - `properties`: An array of object representing the properties in the object. Each of the properties represent a child with properties described below. -- `onAddClick: (schema: RJSFSchema) => () => void`: Returns a function that adds a new property to the object (to be used with additionalProperties) +- `onAddClick: (schema: RJSFSchema) => () => void`: Returns a function that adds a new property to the object (to be used with additionalProperties and patternProperties) - `readonly`: A boolean value stating if the object is read-only. - `required`: A boolean value stating if the object is required. - `hideError`: A boolean value stating if the field is hiding its errors. @@ -908,8 +908,8 @@ The following props are passed to each `UnsupportedFieldTemplate`: ## WrapIfAdditionalTemplate -The `WrapIfAdditionalTemplate` is used by the `FieldTemplate` to conditionally render additional controls if `additionalProperties` is present in the schema. -You may customize `WrapIfAdditionalTemplate` if you wish to change the layout or behavior of user-controlled `additionalProperties`. +The `WrapIfAdditionalTemplate` is used by the `FieldTemplate` to conditionally render additional controls if `additionalProperties` or `patternProperties` are present in the schema. +You may customize `WrapIfAdditionalTemplate` if you wish to change the layout or behavior of user-controlled `additionalProperties` and `patternProperties`. ```tsx import { RJSFSchema, WrapIfAdditionalTemplateProps } from '@rjsf/utils'; @@ -987,7 +987,7 @@ Each button template (except for the `SubmitButton`) accepts, as props, the stan ### AddButton -The `AddButton` is used to render an add action on a `Form` for both a new `additionalProperties` element for an object or a new element in an array. +The `AddButton` is used to render an add action on a `Form` for both a new `additionalProperties` or `patternProperties` element for an object or a new element in an array. You can customize the `AddButton` to render something other than the icon button that is provided by a theme as follows: ```tsx @@ -1077,7 +1077,7 @@ render( ### RemoveButton -The `RemoveButton` is used to render a remove action on a `Form` for both a existing `additionalProperties` element for an object or an existing element in an array. +The `RemoveButton` is used to render a remove action on a `Form` for both a existing `additionalProperties` or `patternProperties` element for an object or an existing element in an array. You can customize the `RemoveButton` to render something other than the icon button that is provided by a theme as follows: ```tsx diff --git a/packages/docs/docs/api-reference/form-props.md b/packages/docs/docs/api-reference/form-props.md index 3ed9a49c1b..07c4bc4a19 100644 --- a/packages/docs/docs/api-reference/form-props.md +++ b/packages/docs/docs/api-reference/form-props.md @@ -464,7 +464,7 @@ Sometimes you may want to trigger events or modify external state when a field h If you plan on being notified every time the form data are updated, you can pass an `onChange` handler, which will receive the same first argument as `onSubmit` any time a value is updated in the form. It will also receive, as the second argument, the `id` of the field which experienced the change. Generally, this will be the `id` of the field for which input data is modified. -In the case of adding/removing of new fields in arrays or objects with `additionalProperties` and the rearranging of items in arrays, the `id` will be that of the array or object itself, rather than the item/field being added, removed or moved. +In the case of adding/removing of new fields in arrays or objects with `additionalProperties` or `patternProperties` and the rearranging of items in arrays, the `id` will be that of the array or object itself, rather than the item/field being added, removed or moved. ## onError diff --git a/packages/docs/docs/api-reference/utility-functions.md b/packages/docs/docs/api-reference/utility-functions.md index 79979a1348..9ce8ba4307 100644 --- a/packages/docs/docs/api-reference/utility-functions.md +++ b/packages/docs/docs/api-reference/utility-functions.md @@ -96,7 +96,7 @@ The UI for the field can expand if it has additional properties, is not forced a #### Returns -- boolean: True if the schema element has additionalProperties, is expandable, and not at the maxProperties limit +- boolean: True if the schema element has additionalProperties or patternProperties keywords, is expandable, and not at the maxProperties limit ### createErrorHandler() @@ -392,6 +392,7 @@ If the type is not explicitly defined, then an attempt is made to infer it from - schema.enum: Returns `string` - schema.properties: Returns `object` - schema.additionalProperties: Returns `object` +- schema.patternProperties: Returns `object` - type is an array with a length of 2 and one type is 'null': Returns the other type #### Parameters diff --git a/packages/docs/docs/json-schema/objects.md b/packages/docs/docs/json-schema/objects.md index c7672ff7dc..9d31bce731 100644 --- a/packages/docs/docs/json-schema/objects.md +++ b/packages/docs/docs/json-schema/objects.md @@ -87,7 +87,7 @@ const uiSchema: UiSchema = { }; ``` -## Additional properties +## Additional and pattern properties The `additionalProperties` keyword allows the user to add properties with arbitrary key names. Set this keyword equal to a schema object: @@ -116,9 +116,36 @@ In this way, an add button for new properties is shown by default. You can also define `uiSchema` options for `additionalProperties` by setting the `additionalProperties` attribute in the `uiSchema`. +The `patternProperties` keyword allows the user to add properties with names that match one or more of the specified regular expressions + +```tsx +import { Form } from '@rjsf/core'; +import { RJSFSchema } from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; + +const schema: RJSFSchema = { + type: 'object', + properties: { + name: { + type: 'string', + }, + }, + patternProperties: { + '^foo+$': { + type: 'number', + enum: [1, 2, 3], + }, + }, +}; + +render(
, document.getElementById('app')); +``` + +Also in this case, an add button for new properties is shown by default. + ### `expandable` option -You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`: +You can turn support for `additionalProperties` and `patternProperties` off with the `expandable` option in `uiSchema`: ```ts import { UiSchema } from '@rjsf/utils'; diff --git a/packages/playground/src/samples/index.ts b/packages/playground/src/samples/index.ts index 619f522864..7f6f5828de 100644 --- a/packages/playground/src/samples/index.ts +++ b/packages/playground/src/samples/index.ts @@ -34,6 +34,7 @@ import customField from './customField'; import layoutGrid from './layoutGrid'; import { Sample } from './Sample'; import deepFreeze from 'deep-freeze-es6'; +import patternProperties from './patternProperties'; export type { Sample }; @@ -61,6 +62,7 @@ const _samples: Record = { 'Property dependencies': propertyDependencies, 'Schema dependencies': schemaDependencies, 'Additional Properties': additionalProperties, + 'Pattern Properties': patternProperties, 'Any Of': anyOf, 'Any Of with Custom Field': customFieldAnyOf, 'One Of': oneOf, diff --git a/packages/playground/src/samples/patternProperties.ts b/packages/playground/src/samples/patternProperties.ts new file mode 100644 index 0000000000..e0e746db40 --- /dev/null +++ b/packages/playground/src/samples/patternProperties.ts @@ -0,0 +1,38 @@ +import { Sample } from './Sample'; + +const patternProperties: Sample = { + schema: { + title: 'A customizable registration form', + description: 'A simple form with pattern properties example.', + type: 'object', + required: ['firstName', 'lastName'], + properties: { + firstName: { + type: 'string', + title: 'First name', + }, + lastName: { + type: 'string', + title: 'Last name', + }, + }, + patternProperties: { + '^[a-z][a-zA-Z]+$': { + type: 'string', + }, + }, + }, + uiSchema: { + firstName: { + 'ui:autofocus': true, + 'ui:emptyValue': '', + }, + }, + formData: { + firstName: 'Chuck', + lastName: 'Norris', + assKickCount: 'infinity', + }, +}; + +export default patternProperties; diff --git a/packages/utils/src/canExpand.ts b/packages/utils/src/canExpand.ts index 528562f948..65d5874df5 100644 --- a/packages/utils/src/canExpand.ts +++ b/packages/utils/src/canExpand.ts @@ -15,7 +15,7 @@ export default function canExpand = {}, formData?: T, ) { - if (!schema.additionalProperties) { + if (!(schema.additionalProperties || schema.patternProperties)) { return false; } const { expandable = true } = getUiOptions(uiSchema); diff --git a/packages/utils/src/constants.ts b/packages/utils/src/constants.ts index 3114d3d2a4..ae93063bdd 100644 --- a/packages/utils/src/constants.ts +++ b/packages/utils/src/constants.ts @@ -19,6 +19,7 @@ export const ITEMS_KEY = 'items'; export const JUNK_OPTION_ID = '_$junk_option_schema_id$_'; export const NAME_KEY = '$name'; export const ONE_OF_KEY = 'oneOf'; +export const PATTERN_PROPERTIES_KEY = 'patternProperties'; export const PROPERTIES_KEY = 'properties'; export const READONLY_KEY = 'readonly'; export const REQUIRED_KEY = 'required'; diff --git a/packages/utils/src/getSchemaType.ts b/packages/utils/src/getSchemaType.ts index f20dfa9074..6918b56315 100644 --- a/packages/utils/src/getSchemaType.ts +++ b/packages/utils/src/getSchemaType.ts @@ -7,6 +7,7 @@ import { RJSFSchema, StrictRJSFSchema } from './types'; * - schema.enum: Returns `string` * - schema.properties: Returns `object` * - schema.additionalProperties: Returns `object` + * - schema.patternProperties: Returns `object` * - type is an array with a length of 2 and one type is 'null': Returns the other type * * @param schema - The schema for which to get the type @@ -25,7 +26,7 @@ export default function getSchemaType( return 'string'; } - if (!type && (schema.properties || schema.additionalProperties)) { + if (!type && (schema.properties || schema.additionalProperties || schema.patternProperties)) { return 'object'; } diff --git a/packages/utils/src/schema/retrieveSchema.ts b/packages/utils/src/schema/retrieveSchema.ts index 9575f90343..058f6597ee 100644 --- a/packages/utils/src/schema/retrieveSchema.ts +++ b/packages/utils/src/schema/retrieveSchema.ts @@ -16,6 +16,7 @@ import { IF_KEY, ITEMS_KEY, ONE_OF_KEY, + PATTERN_PROPERTIES_KEY, PROPERTIES_KEY, REF_KEY, } from '../constants'; @@ -34,6 +35,7 @@ import { } from '../types'; import getFirstMatchingOption from './getFirstMatchingOption'; import deepEquals from '../deepEquals'; +import isEmpty from 'lodash/isEmpty'; /** 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 @@ -186,6 +188,27 @@ export function getAllPermutationsOfXxxOf( + schema: S, + key: string, +): S['patternProperties'] { + return Object.keys(schema.patternProperties!) + .filter((pattern) => RegExp(pattern).test(key)) + .reduce( + (obj, pattern) => { + obj[pattern] = schema.patternProperties![pattern]; + return obj; + }, + {} as S['patternProperties'], + ); +} + /** Resolves references and dependencies within a schema and its 'allOf' children. Passes the `expandAllBranches` flag * down to the `retrieveSchemaInternal()`, `resolveReference()` and `resolveDependencies()` helper calls. If * `expandAllBranches` is true, then all possible dependencies and/or allOf branches are returned. @@ -395,35 +418,55 @@ export function stubExistingAdditionalProperties< // No need to stub, our schema already has the property return; } - - let additionalProperties: S['additionalProperties'] = {}; - if (typeof schema.additionalProperties !== 'boolean') { - if (REF_KEY in schema.additionalProperties!) { - additionalProperties = retrieveSchema( + if (PATTERN_PROPERTIES_KEY in schema) { + const matchingProperties = getMatchingPatternProperties(schema, key); + if (!isEmpty(matchingProperties)) { + schema.properties[key] = retrieveSchema( validator, - { $ref: get(schema.additionalProperties, [REF_KEY]) } as S, + { allOf: Object.values(matchingProperties) } as S, rootSchema, formData as T, experimental_customMergeAllOf, ); - } else if ('type' in schema.additionalProperties!) { - additionalProperties = { ...schema.additionalProperties }; - } else if (ANY_OF_KEY in schema.additionalProperties! || ONE_OF_KEY in schema.additionalProperties!) { - additionalProperties = { - type: 'object', - ...schema.additionalProperties, - }; + set(schema.properties, [key, ADDITIONAL_PROPERTY_FLAG], true); + return; + } + } + if (ADDITIONAL_PROPERTIES_KEY in schema && schema.additionalProperties !== false) { + let additionalProperties: S['additionalProperties'] = {}; + if (typeof schema.additionalProperties !== 'boolean') { + if (REF_KEY in schema.additionalProperties!) { + additionalProperties = retrieveSchema( + validator, + { $ref: get(schema.additionalProperties, [REF_KEY]) } as S, + rootSchema, + formData as T, + experimental_customMergeAllOf, + ); + } else if ('type' in schema.additionalProperties!) { + additionalProperties = { ...schema.additionalProperties }; + } else if (ANY_OF_KEY in schema.additionalProperties! || ONE_OF_KEY in schema.additionalProperties!) { + additionalProperties = { + type: 'object', + ...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); } else { - additionalProperties = { type: guessType(get(formData, [key])) }; + // Invalid property + schema.properties[key] = { type: 'null' }; + // Set our additional property flag so we know it was dynamically added + set(schema.properties, [key, ADDITIONAL_PROPERTY_FLAG], true); } - - // 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; @@ -516,8 +559,30 @@ export function retrieveSchemaInternal< return resolvedSchemaWithoutAllOf as S; } } + if (PROPERTIES_KEY in resolvedSchema && PATTERN_PROPERTIES_KEY in resolvedSchema) { + resolvedSchema = Object.keys(resolvedSchema.properties!).reduce( + (schema, key) => { + const matchingProperties = getMatchingPatternProperties(schema, key); + if (!isEmpty(matchingProperties)) { + schema.properties[key] = retrieveSchema( + validator, + { allOf: [schema.properties[key], ...Object.values(matchingProperties)] } as S, + rootSchema, + rawFormData as T, + experimental_customMergeAllOf, + ); + } + return schema; + }, + { + ...resolvedSchema, + properties: { ...resolvedSchema.properties }, + }, + ); + } const hasAdditionalProperties = - ADDITIONAL_PROPERTIES_KEY in resolvedSchema && resolvedSchema.additionalProperties !== false; + PATTERN_PROPERTIES_KEY in resolvedSchema || + (ADDITIONAL_PROPERTIES_KEY in resolvedSchema && resolvedSchema.additionalProperties !== false); if (hasAdditionalProperties) { return stubExistingAdditionalProperties( validator, diff --git a/packages/utils/test/canExpand.test.ts b/packages/utils/test/canExpand.test.ts index b37f7f6697..1fb7f63d8c 100644 --- a/packages/utils/test/canExpand.test.ts +++ b/packages/utils/test/canExpand.test.ts @@ -1,7 +1,7 @@ import { RJSFSchema, canExpand } from '../src'; describe('canExpand()', () => { - it('no additional properties', () => { + it('no additional or pattern properties', () => { expect(canExpand({}, {}, {})).toBe(false); }); it('has additional properties', () => { @@ -12,6 +12,16 @@ describe('canExpand()', () => { }; expect(canExpand(schema)).toBe(true); }); + it('has pattern properties', () => { + const schema: RJSFSchema = { + patternProperties: { + '^foo': { + type: 'string', + }, + }, + }; + expect(canExpand(schema)).toBe(true); + }); it('has uiSchema expandable false', () => { const schema: RJSFSchema = { additionalProperties: { diff --git a/packages/utils/test/getSchemaType.test.ts b/packages/utils/test/getSchemaType.test.ts index 4d90ae9d55..dcd2304116 100644 --- a/packages/utils/test/getSchemaType.test.ts +++ b/packages/utils/test/getSchemaType.test.ts @@ -65,6 +65,10 @@ const cases: { schema: object; expected: string | undefined }[] = [ schema: { additionalProperties: {} }, expected: 'object', }, + { + schema: { patternProperties: { '^foo': {} } }, + expected: 'object', + }, { schema: { enum: ['foo'] }, expected: 'string', diff --git a/packages/utils/test/schema/retrieveSchemaTest.ts b/packages/utils/test/schema/retrieveSchemaTest.ts index 0c32350338..6545a3ba0e 100644 --- a/packages/utils/test/schema/retrieveSchemaTest.ts +++ b/packages/utils/test/schema/retrieveSchemaTest.ts @@ -1280,6 +1280,43 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) { expect(withExactlyOneSubschema(testValidator, schema, schema, 'bar', oneOf, false, [])).toEqual([schema]); }); }); + describe('withPatternProperties()', () => { + it('merges all subschemas that match the patternProperties regex', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + foo: { type: 'number' }, + baz: { type: 'boolean' }, + }, + patternProperties: { + '^foo': { + minimum: 10, + }, + '^foo.*': { + maximum: 20, + }, + '^bar': { + multipleOf: 2, + }, + }, + }; + const rootSchema: RJSFSchema = { definitions: {} }; + const formData = {}; + expect(retrieveSchema(testValidator, schema, rootSchema, formData)).toEqual({ + ...schema, + properties: { + foo: { + type: 'number', + minimum: 10, + maximum: 20, + }, + baz: { + type: 'boolean', + }, + }, + }); + }); + }); describe('stubExistingAdditionalProperties()', () => { it('deals with undefined formData', () => { const schema: RJSFSchema = { type: 'string' }; @@ -1396,6 +1433,143 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) { }, }); }); + it('has property keys that does not match patternProperties, no additionalProperties', () => { + const schema: RJSFSchema = { + patternProperties: { + '^foo': { + type: 'string', + }, + '^bar': { + type: 'number', + }, + }, + }; + const formData = { baz: 1 }; + expect(stubExistingAdditionalProperties(testValidator, schema, undefined, formData)).toEqual({ + ...schema, + properties: { + baz: { + type: 'null', + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); + it('has property keys that match patternProperties', () => { + const schema: RJSFSchema = { + patternProperties: { + '^foo': { + type: 'string', + }, + '^bar': { + type: 'number', + minimum: 10, + }, + }, + }; + const formData = { bar: 1 }; + expect(stubExistingAdditionalProperties(testValidator, schema, undefined, formData)).toEqual({ + ...schema, + properties: { + bar: { + type: 'number', + minimum: 10, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); + it('has property keys that match multiple patternProperties', () => { + const schema: RJSFSchema = { + patternProperties: { + '^foo': { + type: 'string', + }, + '^bar': { + type: 'number', + minimum: 10, + }, + '^ba.*': { + type: 'number', + maximum: 20, + }, + }, + }; + const formData = { bar: 1 }; + expect(stubExistingAdditionalProperties(testValidator, schema, undefined, formData)).toEqual({ + ...schema, + properties: { + bar: { + type: 'number', + minimum: 10, + maximum: 20, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); + it('has property keys that match patternProperties, additionalProperties is boolean', () => { + const schema: RJSFSchema = { + patternProperties: { + '^foo': { + type: 'string', + }, + '^bar': { + type: 'number', + minimum: 10, + }, + }, + additionalProperties: true, + }; + const formData = { bar: 1, baz: true }; + expect(stubExistingAdditionalProperties(testValidator, schema, undefined, formData)).toEqual({ + ...schema, + properties: { + bar: { + type: 'number', + minimum: 10, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + baz: { + type: 'boolean', + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); + it('has property keys that match patternProperties, additionalProperties is object', () => { + const schema: RJSFSchema = { + patternProperties: { + '^foo': { + type: 'string', + }, + '^bar': { + type: 'number', + minimum: 10, + }, + }, + additionalProperties: { + type: 'number', + maximum: 20, + }, + }; + const formData = { bar: 1, baz: 2 }; + expect(stubExistingAdditionalProperties(testValidator, schema, undefined, formData)).toEqual({ + ...schema, + properties: { + bar: { + type: 'number', + minimum: 10, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + baz: { + type: 'number', + maximum: 20, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); }); describe('getAllPermutationsOfXxxOf()', () => { it('returns a single permutation when there are only one version of each row', () => {