diff --git a/packages/core/docs/advanced-customization.md b/packages/core/docs/advanced-customization.md index e2d8e6f39b..82c66e34d4 100644 --- a/packages/core/docs/advanced-customization.md +++ b/packages/core/docs/advanced-customization.md @@ -515,12 +515,13 @@ A field component will always be passed the following props: #### The `registry` object -The `registry` is an object containing the registered custom fields and widgets as well as root schema definitions. +The `registry` is an object containing the registered custom fields and widgets as well as the root schema definitions. - `fields`: The [custom registered fields](#custom-field-components). By default this object contains the standard `SchemaField`, `TitleField` and `DescriptionField` components; - `widgets`: The [custom registered widgets](#custom-widget-components), if any; - - `definitions`: The root schema [definitions](#schema-definitions-and-references), if any. - - `formContext`: The [formContext](#the-formcontext-object) object. + - `rootSchema`: The root schema, which can contain referenced [definitions](#schema-definitions-and-references); + - `formContext`: The [formContext](#the-formcontext-object) object; + - `definitions` (deprecated since v2): Equal to `rootSchema.definitions`. The registry is passed down the component tree, so you can access it from your custom field and `SchemaField` components. diff --git a/packages/core/docs/definitions.md b/packages/core/docs/definitions.md index 65b89ed284..42072b6898 100644 --- a/packages/core/docs/definitions.md +++ b/packages/core/docs/definitions.md @@ -23,7 +23,4 @@ This library partially supports [inline schema definition dereferencing]( http:/ } ``` -*(Sample schema courtesy of the [Space Telescope Science Institute](http://spacetelescope.github.io/understanding-json-schema/structuring.html))* - -Note that it only supports local definition referencing; we do not plan on fetching foreign schemas over HTTP anytime soon. Basically, you can only reference a definition from the very schema object defining it. - +Note that this library only supports local definition referencing. The value in the `$ref` keyword should be a [JSON Pointer](https://tools.ietf.org/html/rfc6901) in URI fragment identifier format. \ No newline at end of file diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index b7190bf6d3..97244eed94 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -7787,6 +7787,11 @@ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", diff --git a/packages/core/package.json b/packages/core/package.json index 01a09d7e4b..673e8977a5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ "@babel/runtime-corejs2": "^7.4.5", "ajv": "^6.7.0", "core-js": "^2.5.7", + "jsonpointer": "^4.0.1", "json-schema-merge-allof": "^0.6.0", "lodash": "^4.17.15", "prop-types": "^15.7.2", diff --git a/packages/core/src/components/Form.js b/packages/core/src/components/Form.js index f6a08db83a..477ed8a869 100644 --- a/packages/core/src/components/Form.js +++ b/packages/core/src/components/Form.js @@ -60,9 +60,9 @@ export default class Form extends Component { const edit = typeof inputFormData !== "undefined"; const liveValidate = props.liveValidate || this.props.liveValidate; const mustValidate = edit && !props.noValidate && liveValidate; - const { definitions } = schema; - const formData = getDefaultFormState(schema, inputFormData, definitions); - const retrievedSchema = retrieveSchema(schema, definitions, formData); + const rootSchema = schema; + const formData = getDefaultFormState(schema, inputFormData, rootSchema); + const retrievedSchema = retrieveSchema(schema, rootSchema, formData); const customFormats = props.customFormats; const additionalMetaSchemas = props.additionalMetaSchemas; let { errors, errorSchema } = mustValidate @@ -78,7 +78,7 @@ export default class Form extends Component { const idSchema = toIdSchema( retrievedSchema, uiSchema["ui:rootFieldId"], - definitions, + rootSchema, formData, props.idPrefix ); @@ -105,8 +105,8 @@ export default class Form extends Component { customFormats = this.props.customFormats ) { const { validate, transformErrors } = this.props; - const { definitions } = this.getRegistry(); - const resolvedSchema = retrieveSchema(schema, definitions, formData); + const { rootSchema } = this.getRegistry(); + const resolvedSchema = retrieveSchema(schema, rootSchema, formData); return validateFormData( formData, resolvedSchema, @@ -185,13 +185,13 @@ export default class Form extends Component { if (this.props.omitExtraData === true && this.props.liveOmit === true) { const retrievedSchema = retrieveSchema( this.state.schema, - this.state.schema.definitions, + this.state.schema, formData ); const pathSchema = toPathSchema( retrievedSchema, "", - this.state.schema.definitions, + this.state.schema, formData ); @@ -250,13 +250,13 @@ export default class Form extends Component { if (this.props.omitExtraData === true) { const retrievedSchema = retrieveSchema( this.state.schema, - this.state.schema.definitions, + this.state.schema, newFormData ); const pathSchema = toPathSchema( retrievedSchema, "", - this.state.schema.definitions, + this.state.schema, newFormData ); @@ -317,6 +317,7 @@ export default class Form extends Component { ObjectFieldTemplate: this.props.ObjectFieldTemplate, FieldTemplate: this.props.FieldTemplate, definitions: this.props.schema.definitions || {}, + rootSchema: this.props.schema, formContext: this.props.formContext || {}, }; } diff --git a/packages/core/src/components/fields/ArrayField.js b/packages/core/src/components/fields/ArrayField.js index ffd6bd5def..69f778d629 100644 --- a/packages/core/src/components/fields/ArrayField.js +++ b/packages/core/src/components/fields/ArrayField.js @@ -270,12 +270,12 @@ class ArrayField extends Component { _getNewFormDataRow = () => { const { schema, registry = getDefaultRegistry() } = this.props; - const { definitions } = registry; + const { rootSchema } = registry; let itemSchema = schema.items; if (isFixedItems(schema) && allowAdditionalItems(schema)) { itemSchema = schema.additionalItems; } - return getDefaultFormState(itemSchema, undefined, definitions); + return getDefaultFormState(itemSchema, undefined, rootSchema); }; onAddClick = event => { @@ -425,7 +425,7 @@ class ArrayField extends Component { idSchema, registry = getDefaultRegistry(), } = this.props; - const { definitions } = registry; + const { rootSchema } = registry; if (!schema.hasOwnProperty("items")) { return ( { const { key, item } = keyedItem; - const itemSchema = retrieveSchema(schema.items, definitions, item); + const itemSchema = retrieveSchema(schema.items, rootSchema, item); const itemErrorSchema = errorSchema ? errorSchema[index] : undefined; const itemIdPrefix = idSchema.$id + "_" + index; const itemIdSchema = toIdSchema( itemSchema, itemIdPrefix, - definitions, + rootSchema, item, idPrefix ); @@ -541,8 +541,8 @@ class ArrayField extends Component { rawErrors, } = this.props; const items = this.props.formData; - const { widgets, definitions, formContext } = registry; - const itemsSchema = retrieveSchema(schema.items, definitions, formData); + const { widgets, rootSchema, formContext } = registry; + const itemsSchema = retrieveSchema(schema.items, rootSchema, formData); const enumOptions = optionsList(itemsSchema); const { widget = "select", ...options } = { ...getUiOptions(uiSchema), @@ -631,13 +631,13 @@ class ArrayField extends Component { } = this.props; const title = schema.title || name; let items = this.props.formData; - const { ArrayFieldTemplate, definitions, fields, formContext } = registry; + const { ArrayFieldTemplate, rootSchema, fields, formContext } = registry; const { TitleField } = fields; const itemSchemas = schema.items.map((item, index) => - retrieveSchema(item, definitions, formData[index]) + retrieveSchema(item, rootSchema, formData[index]) ); const additionalSchema = allowAdditionalItems(schema) - ? retrieveSchema(schema.additionalItems, definitions, formData) + ? retrieveSchema(schema.additionalItems, rootSchema, formData) : null; if (!items || items.length < itemSchemas.length) { @@ -657,13 +657,13 @@ class ArrayField extends Component { const { key, item } = keyedItem; const additional = index >= itemSchemas.length; const itemSchema = additional - ? retrieveSchema(schema.additionalItems, definitions, item) + ? retrieveSchema(schema.additionalItems, rootSchema, item) : itemSchemas[index]; const itemIdPrefix = idSchema.$id + "_" + index; const itemIdSchema = toIdSchema( itemSchema, itemIdPrefix, - definitions, + rootSchema, item, idPrefix ); diff --git a/packages/core/src/components/fields/MultiSchemaField.js b/packages/core/src/components/fields/MultiSchemaField.js index 1403c3592e..11ec9afe81 100644 --- a/packages/core/src/components/fields/MultiSchemaField.js +++ b/packages/core/src/components/fields/MultiSchemaField.js @@ -43,9 +43,9 @@ class AnyOfField extends Component { } getMatchingOption(formData, options) { - const { definitions } = this.props.registry; + const { rootSchema } = this.props.registry; - let option = getMatchingOption(formData, options, definitions); + let option = getMatchingOption(formData, options, rootSchema); if (option !== 0) { return option; } @@ -57,10 +57,10 @@ class AnyOfField extends Component { onOptionChange = option => { const selectedOption = parseInt(option, 10); const { formData, onChange, options, registry } = this.props; - const { definitions } = registry; + const { rootSchema } = registry; const newOption = retrieveSchema( options[selectedOption], - definitions, + rootSchema, formData ); @@ -89,7 +89,7 @@ class AnyOfField extends Component { } // Call getDefaultFormState to make sure defaults are populated on change. onChange( - getDefaultFormState(options[selectedOption], newFormData, definitions) + getDefaultFormState(options[selectedOption], newFormData, rootSchema) ); this.setState({ diff --git a/packages/core/src/components/fields/ObjectField.js b/packages/core/src/components/fields/ObjectField.js index ba15449f2f..c154dc188d 100644 --- a/packages/core/src/components/fields/ObjectField.js +++ b/packages/core/src/components/fields/ObjectField.js @@ -180,7 +180,7 @@ class ObjectField extends Component { const { registry = getDefaultRegistry() } = this.props; const refSchema = retrieveSchema( { $ref: schema.additionalProperties["$ref"] }, - registry.definitions, + registry.rootSchema, this.props.formData ); @@ -210,9 +210,9 @@ class ObjectField extends Component { registry = getDefaultRegistry(), } = this.props; - const { definitions, fields, formContext } = registry; + const { rootSchema, fields, formContext } = registry; const { SchemaField, TitleField, DescriptionField } = fields; - const schema = retrieveSchema(this.props.schema, definitions, formData); + const schema = retrieveSchema(this.props.schema, rootSchema, formData); // If this schema has a title defined, but the user has set a new key/label, retain their input. let title; diff --git a/packages/core/src/components/fields/SchemaField.js b/packages/core/src/components/fields/SchemaField.js index 689774cc72..6309793fc6 100644 --- a/packages/core/src/components/fields/SchemaField.js +++ b/packages/core/src/components/fields/SchemaField.js @@ -237,13 +237,13 @@ function SchemaFieldRender(props) { registry = getDefaultRegistry(), wasPropertyKeyModified = false, } = props; - const { definitions, fields, formContext } = registry; + const { rootSchema, fields, formContext } = registry; const FieldTemplate = uiSchema["ui:FieldTemplate"] || registry.FieldTemplate || DefaultTemplate; let idSchema = props.idSchema; - const schema = retrieveSchema(props.schema, definitions, formData); + const schema = retrieveSchema(props.schema, rootSchema, formData); idSchema = mergeObjects( - toIdSchema(schema, null, definitions, formData, idPrefix), + toIdSchema(schema, null, rootSchema, formData, idPrefix), idSchema ); const FieldComponent = getFieldComponent(schema, uiSchema, idSchema, fields); @@ -264,8 +264,8 @@ function SchemaFieldRender(props) { let { label: displayLabel = true } = uiOptions; if (schema.type === "array") { displayLabel = - isMultiSelect(schema, definitions) || - isFilesArray(schema, uiSchema, definitions); + isMultiSelect(schema, rootSchema) || + isFilesArray(schema, uiSchema, rootSchema); } if (schema.type === "object") { displayLabel = false; diff --git a/packages/core/src/types.js b/packages/core/src/types.js index 1393b17e91..eb2f17c466 100644 --- a/packages/core/src/types.js +++ b/packages/core/src/types.js @@ -5,6 +5,7 @@ export const registry = PropTypes.shape({ FieldTemplate: PropTypes.elementType, ObjectFieldTemplate: PropTypes.elementType, definitions: PropTypes.object.isRequired, + rootSchema: PropTypes.object, fields: PropTypes.objectOf(PropTypes.elementType).isRequired, formContext: PropTypes.object.isRequired, widgets: PropTypes.objectOf( diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index 4509920258..b07c392811 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -4,6 +4,7 @@ import mergeAllOf from "json-schema-merge-allof"; import fill from "core-js/library/fn/array/fill"; import validateFormData, { isValid } from "./validate"; import union from "lodash/union"; +import jsonpointer from "jsonpointer"; export const ADDITIONAL_PROPERTY_FLAG = "__additional_property"; @@ -64,6 +65,7 @@ export function getDefaultRegistry() { fields: require("./components/fields").default, widgets: require("./components/widgets").default, definitions: {}, + rootSchema: {}, formContext: {}, }; } @@ -154,7 +156,7 @@ export function hasWidget(schema, widget, registeredWidgets = {}) { function computeDefaults( _schema, parentDefaults, - definitions, + rootSchema, rawFormData = {}, includeUndefinedValues = false ) { @@ -171,20 +173,20 @@ function computeDefaults( defaults = schema.default; } else if ("$ref" in schema) { // Use referenced schema defaults for this node. - const refSchema = findSchemaDefinition(schema.$ref, definitions); + const refSchema = findSchemaDefinition(schema.$ref, rootSchema); return computeDefaults( refSchema, defaults, - definitions, + rootSchema, formData, includeUndefinedValues ); } else if ("dependencies" in schema) { - const resolvedSchema = resolveDependencies(schema, definitions, formData); + const resolvedSchema = resolveDependencies(schema, rootSchema, formData); return computeDefaults( resolvedSchema, defaults, - definitions, + rootSchema, formData, includeUndefinedValues ); @@ -193,17 +195,17 @@ function computeDefaults( computeDefaults( itemSchema, Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined, - definitions, + rootSchema, formData, includeUndefinedValues ) ); } else if ("oneOf" in schema) { schema = - schema.oneOf[getMatchingOption(undefined, schema.oneOf, definitions)]; + schema.oneOf[getMatchingOption(undefined, schema.oneOf, rootSchema)]; } else if ("anyOf" in schema) { schema = - schema.anyOf[getMatchingOption(undefined, schema.anyOf, definitions)]; + schema.anyOf[getMatchingOption(undefined, schema.anyOf, rootSchema)]; } // Not defaults defined for this node, fallback to generic typed ones. @@ -220,7 +222,7 @@ function computeDefaults( let computedDefault = computeDefaults( schema.properties[key], (defaults || {})[key], - definitions, + rootSchema, (formData || {})[key], includeUndefinedValues ); @@ -237,7 +239,7 @@ function computeDefaults( return computeDefaults( schema.items[idx] || schema.additionalItems || {}, item, - definitions + rootSchema ); }); } @@ -248,13 +250,13 @@ function computeDefaults( return computeDefaults( schema.items, (defaults || {})[idx], - definitions, + rootSchema, item ); }); } if (schema.minItems) { - if (!isMultiSelect(schema, definitions)) { + if (!isMultiSelect(schema, rootSchema)) { const defaultsLength = defaults ? defaults.length : 0; if (schema.minItems > defaultsLength) { const defaultEntries = defaults || []; @@ -264,7 +266,7 @@ function computeDefaults( : schema.items; const fillerEntries = fill( new Array(schema.minItems - defaultsLength), - computeDefaults(fillerSchema, fillerSchema.defaults, definitions) + computeDefaults(fillerSchema, fillerSchema.defaults, rootSchema) ); // then fill up the rest with either the item default or empty, up to minItems @@ -281,17 +283,17 @@ function computeDefaults( export function getDefaultFormState( _schema, formData, - definitions = {}, + rootSchema = {}, includeUndefinedValues = false ) { if (!isObject(_schema)) { throw new Error("Invalid schema: " + _schema); } - const schema = retrieveSchema(_schema, definitions, formData); + const schema = retrieveSchema(_schema, rootSchema, formData); const defaults = computeDefaults( schema, _schema.default, - definitions, + rootSchema, formData, includeUndefinedValues ); @@ -479,8 +481,8 @@ export function toConstant(schema) { } } -export function isSelect(_schema, definitions = {}) { - const schema = retrieveSchema(_schema, definitions); +export function isSelect(_schema, rootSchema = {}) { + const schema = retrieveSchema(_schema, rootSchema); const altSchemas = schema.oneOf || schema.anyOf; if (Array.isArray(schema.enum)) { return true; @@ -490,18 +492,18 @@ export function isSelect(_schema, definitions = {}) { return false; } -export function isMultiSelect(schema, definitions = {}) { +export function isMultiSelect(schema, rootSchema = {}) { if (!schema.uniqueItems || !schema.items) { return false; } - return isSelect(schema.items, definitions); + return isSelect(schema.items, rootSchema); } -export function isFilesArray(schema, uiSchema, definitions = {}) { +export function isFilesArray(schema, uiSchema, rootSchema = {}) { if (uiSchema["ui:widget"] === "files") { return true; } else if (schema.items) { - const itemsSchema = retrieveSchema(schema.items, definitions); + const itemsSchema = retrieveSchema(schema.items, rootSchema); return itemsSchema.type === "string" && itemsSchema.format === "data-url"; } return false; @@ -538,29 +540,22 @@ export function optionsList(schema) { } } -function findSchemaDefinition($ref, definitions = {}) { - // Extract and use the referenced definition if we have it. - const match = /^#\/definitions\/(.*)$/.exec($ref); - if (match && match[1]) { - const parts = match[1].split("/"); - let current = definitions; - for (let part of parts) { - part = part.replace(/~1/g, "/").replace(/~0/g, "~"); - while (current.hasOwnProperty("$ref")) { - current = findSchemaDefinition(current.$ref, definitions); - } - if (current.hasOwnProperty(part)) { - current = current[part]; - } else { - // No matching definition found, that's an error (bogus schema?) - throw new Error(`Could not find a definition for ${$ref}.`); - } - } - return current; +function findSchemaDefinition($ref, rootSchema = {}) { + const origRef = $ref; + if ($ref.startsWith("#")) { + // Decode URI fragment representation. + $ref = decodeURIComponent($ref.substring(1)); + } else { + throw new Error(`Could not find a definition for ${origRef}.`); } - - // No matching definition found, that's an error (bogus schema?) - throw new Error(`Could not find a definition for ${$ref}.`); + const current = jsonpointer.get(rootSchema, $ref); + if (current === undefined) { + throw new Error(`Could not find a definition for ${origRef}.`); + } + if (current.hasOwnProperty("$ref")) { + return findSchemaDefinition(current.$ref, rootSchema); + } + return current; } // In the case where we have to implicitly create a schema, it is useful to know what type to use @@ -586,7 +581,7 @@ export const guessType = function guessType(value) { // This function will create new "properties" items for each key in our formData export function stubExistingAdditionalProperties( schema, - definitions = {}, + rootSchema = {}, formData = {} ) { // Clone the schema so we don't ruin the consumer's original @@ -605,7 +600,7 @@ export function stubExistingAdditionalProperties( if (schema.additionalProperties.hasOwnProperty("$ref")) { additionalProperties = retrieveSchema( { $ref: schema.additionalProperties["$ref"] }, - definitions, + rootSchema, formData ); } else if (schema.additionalProperties.hasOwnProperty("type")) { @@ -623,17 +618,17 @@ export function stubExistingAdditionalProperties( return schema; } -export function resolveSchema(schema, definitions = {}, formData = {}) { +export function resolveSchema(schema, rootSchema = {}, formData = {}) { if (schema.hasOwnProperty("$ref")) { - return resolveReference(schema, definitions, formData); + return resolveReference(schema, rootSchema, formData); } else if (schema.hasOwnProperty("dependencies")) { - const resolvedSchema = resolveDependencies(schema, definitions, formData); - return retrieveSchema(resolvedSchema, definitions, formData); + const resolvedSchema = resolveDependencies(schema, rootSchema, formData); + return retrieveSchema(resolvedSchema, rootSchema, formData); } else if (schema.hasOwnProperty("allOf")) { return { ...schema, allOf: schema.allOf.map(allOfSubschema => - retrieveSchema(allOfSubschema, definitions, formData) + retrieveSchema(allOfSubschema, rootSchema, formData) ), }; } else { @@ -642,24 +637,24 @@ export function resolveSchema(schema, definitions = {}, formData = {}) { } } -function resolveReference(schema, definitions, formData) { +function resolveReference(schema, rootSchema, formData) { // Retrieve the referenced schema definition. - const $refSchema = findSchemaDefinition(schema.$ref, definitions); + const $refSchema = findSchemaDefinition(schema.$ref, rootSchema); // Drop the $ref property of the source schema. const { $ref, ...localSchema } = schema; // Update referenced schema definition with local schema properties. return retrieveSchema( { ...$refSchema, ...localSchema }, - definitions, + rootSchema, formData ); } -export function retrieveSchema(schema, definitions = {}, formData = {}) { +export function retrieveSchema(schema, rootSchema = {}, formData = {}) { if (!isObject(schema)) { return {}; } - let resolvedSchema = resolveSchema(schema, definitions, formData); + let resolvedSchema = resolveSchema(schema, rootSchema, formData); if ("allOf" in schema) { try { resolvedSchema = mergeAllOf({ @@ -678,38 +673,38 @@ export function retrieveSchema(schema, definitions = {}, formData = {}) { if (hasAdditionalProperties) { return stubExistingAdditionalProperties( resolvedSchema, - definitions, + rootSchema, formData ); } return resolvedSchema; } -function resolveDependencies(schema, definitions, formData) { +function resolveDependencies(schema, rootSchema, formData) { // Drop the dependencies from the source schema. let { dependencies = {}, ...resolvedSchema } = schema; if ("oneOf" in resolvedSchema) { resolvedSchema = resolvedSchema.oneOf[ - getMatchingOption(formData, resolvedSchema.oneOf, definitions) + getMatchingOption(formData, resolvedSchema.oneOf, rootSchema) ]; } else if ("anyOf" in resolvedSchema) { resolvedSchema = resolvedSchema.anyOf[ - getMatchingOption(formData, resolvedSchema.anyOf, definitions) + getMatchingOption(formData, resolvedSchema.anyOf, rootSchema) ]; } return processDependencies( dependencies, resolvedSchema, - definitions, + rootSchema, formData ); } function processDependencies( dependencies, resolvedSchema, - definitions, + rootSchema, formData ) { // Process dependencies updating the local schema properties as appropriate. @@ -734,7 +729,7 @@ function processDependencies( } else if (isObject(dependencyValue)) { resolvedSchema = withDependentSchema( resolvedSchema, - definitions, + rootSchema, formData, dependencyKey, dependencyValue @@ -743,7 +738,7 @@ function processDependencies( return processDependencies( remainingDependencies, resolvedSchema, - definitions, + rootSchema, formData ); } @@ -762,14 +757,14 @@ function withDependentProperties(schema, additionallyRequired) { function withDependentSchema( schema, - definitions, + rootSchema, formData, dependencyKey, dependencyValue ) { let { oneOf, ...dependentSchema } = retrieveSchema( dependencyValue, - definitions, + rootSchema, formData ); schema = mergeSchemas(schema, dependentSchema); @@ -782,12 +777,12 @@ function withDependentSchema( // Resolve $refs inside oneOf. const resolvedOneOf = oneOf.map(subschema => subschema.hasOwnProperty("$ref") - ? resolveReference(subschema, definitions, formData) + ? resolveReference(subschema, rootSchema, formData) : subschema ); return withExactlyOneSubschema( schema, - definitions, + rootSchema, formData, dependencyKey, resolvedOneOf @@ -796,7 +791,7 @@ function withDependentSchema( function withExactlyOneSubschema( schema, - definitions, + rootSchema, formData, dependencyKey, oneOf @@ -831,7 +826,7 @@ function withExactlyOneSubschema( const dependentSchema = { ...subschema, properties: dependentSubschema }; return mergeSchemas( schema, - retrieveSchema(dependentSchema, definitions, formData) + retrieveSchema(dependentSchema, rootSchema, formData) ); } @@ -954,7 +949,7 @@ export function shouldRender(comp, nextProps, nextState) { export function toIdSchema( schema, id, - definitions, + rootSchema, formData = {}, idPrefix = "root" ) { @@ -962,11 +957,11 @@ export function toIdSchema( $id: id || idPrefix, }; if ("$ref" in schema || "dependencies" in schema || "allOf" in schema) { - const _schema = retrieveSchema(schema, definitions, formData); - return toIdSchema(_schema, id, definitions, formData, idPrefix); + const _schema = retrieveSchema(schema, rootSchema, formData); + return toIdSchema(_schema, id, rootSchema, formData, idPrefix); } if ("items" in schema && !schema.items.$ref) { - return toIdSchema(schema.items, id, definitions, formData, idPrefix); + return toIdSchema(schema.items, id, rootSchema, formData, idPrefix); } if (schema.type !== "object") { return idSchema; @@ -977,7 +972,7 @@ export function toIdSchema( idSchema[name] = toIdSchema( isObject(field) ? field : {}, fieldId, - definitions, + rootSchema, // It's possible that formData is not an object -- this can happen if an // array item has just been added, but not populated with data yet (formData || {})[name], @@ -987,20 +982,20 @@ export function toIdSchema( return idSchema; } -export function toPathSchema(schema, name = "", definitions, formData = {}) { +export function toPathSchema(schema, name = "", rootSchema, formData = {}) { const pathSchema = { $name: name.replace(/^\./, ""), }; if ("$ref" in schema || "dependencies" in schema || "allOf" in schema) { - const _schema = retrieveSchema(schema, definitions, formData); - return toPathSchema(_schema, name, definitions, formData); + const _schema = retrieveSchema(schema, rootSchema, formData); + return toPathSchema(_schema, name, rootSchema, formData); } if (schema.hasOwnProperty("items") && Array.isArray(formData)) { formData.forEach((element, i) => { pathSchema[i] = toPathSchema( schema.items, `${name}.${i}`, - definitions, + rootSchema, element ); }); @@ -1009,7 +1004,7 @@ export function toPathSchema(schema, name = "", definitions, formData = {}) { pathSchema[property] = toPathSchema( schema.properties[property], `${name}.${property}`, - definitions, + rootSchema, // It's possible that formData is not an object -- this can happen if an // array item has just been added, but not populated with data yet (formData || {})[property] @@ -1108,16 +1103,9 @@ export function rangeSpec(schema) { return spec; } -export function getMatchingOption(formData, options, definitions) { +export function getMatchingOption(formData, options, rootSchema) { for (let i = 0; i < options.length; i++) { - // Assign the definitions to the option, otherwise the match can fail if - // the new option uses a $ref - const option = Object.assign( - { - definitions, - }, - options[i] - ); + const option = options[i]; // If the schema describes an object then we need to add slightly more // strict matching to the schema, because unless the schema uses the diff --git a/packages/core/src/validate.js b/packages/core/src/validate.js index e70aebfee1..73046208df 100644 --- a/packages/core/src/validate.js +++ b/packages/core/src/validate.js @@ -173,8 +173,8 @@ export default function validateFormData( customFormats = {} ) { // Include form data with undefined values, which is required for validation. - const { definitions } = schema; - formData = getDefaultFormState(schema, formData, definitions, true); + const rootSchema = schema; + formData = getDefaultFormState(schema, formData, rootSchema, true); const newMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas); const newFormats = !deepEquals(formerCustomFormats, customFormats); diff --git a/packages/core/test/Form_test.js b/packages/core/test/Form_test.js index ebbd01efff..c8d76de446 100644 --- a/packages/core/test/Form_test.js +++ b/packages/core/test/Form_test.js @@ -657,57 +657,6 @@ describeRepeated("Form common", createFormComponent => { expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); }); - it("should handle recursive references to deep schema definitions", () => { - const schema = { - definitions: { - testdef: { - $ref: "#/definitions/testdefref", - }, - testdefref: { - type: "object", - properties: { - bar: { type: "string" }, - }, - }, - }, - type: "object", - properties: { - foo: { $ref: "#/definitions/testdef/properties/bar" }, - }, - }; - - const { node } = createFormComponent({ schema }); - - expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); - }); - - it("should handle multiple recursive references to deep schema definitions", () => { - const schema = { - definitions: { - testdef: { - $ref: "#/definitions/testdefref1", - }, - testdefref1: { - $ref: "#/definitions/testdefref2", - }, - testdefref2: { - type: "object", - properties: { - bar: { type: "string" }, - }, - }, - }, - type: "object", - properties: { - foo: { $ref: "#/definitions/testdef/properties/bar" }, - }, - }; - - const { node } = createFormComponent({ schema }); - - expect(node.querySelectorAll("input[type=text]")).to.have.length.of(1); - }); - it("should priorize definition over schema type property", () => { // Refs bug #140 const schema = { diff --git a/packages/core/test/SchemaField_test.js b/packages/core/test/SchemaField_test.js index e94adea6df..c685993f6b 100644 --- a/packages/core/test/SchemaField_test.js +++ b/packages/core/test/SchemaField_test.js @@ -20,6 +20,66 @@ describe("SchemaField", () => { sandbox.restore(); }); + describe("registry", () => { + it("should provide expected registry as prop", () => { + let receivedProps; + const schema = { + type: "object", + definitions: { + a: { type: "string" }, + }, + }; + createFormComponent({ + schema, + uiSchema: { + "ui:field": props => { + receivedProps = props; + return null; + }, + }, + }); + + const { registry } = receivedProps; + expect(registry).eql({ + widgets: getDefaultRegistry().widgets, + fields: getDefaultRegistry().fields, + definitions: schema.definitions, + rootSchema: schema, + ArrayFieldTemplate: undefined, + FieldTemplate: undefined, + ObjectFieldTemplate: undefined, + formContext: {}, + }); + }); + it("should set definitions to empty object if it is undefined", () => { + let receivedProps; + const schema = { + type: "object", + }; + createFormComponent({ + schema, + uiSchema: { + "ui:field": props => { + receivedProps = props; + return null; + }, + }, + }); + + const { registry } = receivedProps; + expect(registry).eql({ + widgets: getDefaultRegistry().widgets, + fields: getDefaultRegistry().fields, + definitions: {}, + rootSchema: schema, + ArrayFieldTemplate: undefined, + FieldTemplate: undefined, + ObjectFieldTemplate: undefined, + formContext: {}, + }); + }); + }); + describe("Unsupported field", () => { it("should warn on invalid field type", () => { const { node } = createFormComponent({ @@ -102,21 +162,16 @@ describe("SchemaField", () => { createFormComponent({ schema, uiSchema: { - "ui:field": class extends React.Component { - constructor(props) { - super(props); - receivedProps = props; - } - render() { - return null; - } + "ui:field": props => { + receivedProps = props; + return null; }, }, }); const { registry } = receivedProps; expect(registry.widgets).eql(getDefaultRegistry().widgets); - expect(registry.definitions).eql({}); + expect(registry.rootSchema).eql(schema); expect(registry.fields).to.be.an("object"); expect(registry.fields.SchemaField).eql(SchemaField); expect(registry.fields.TitleField).eql(TitleField); diff --git a/packages/core/test/utils_test.js b/packages/core/test/utils_test.js index e35af7cfbb..d6a629e3b6 100644 --- a/packages/core/test/utils_test.js +++ b/packages/core/test/utils_test.js @@ -549,7 +549,7 @@ describe("utils", () => { default: { foo: 42 }, }; - expect(getDefaultFormState(schema, undefined, schema.definitions)).eql({ + expect(getDefaultFormState(schema, undefined, schema)).eql({ foo: 42, }); }); @@ -1309,7 +1309,7 @@ describe("utils", () => { const definitions = { FooItem: { type: "string", enum: ["foo"] }, }; - expect(isMultiSelect(schema, definitions)).to.be.true; + expect(isMultiSelect(schema, { definitions })).to.be.true; }); }); @@ -1644,7 +1644,59 @@ describe("utils", () => { }; const definitions = { address }; - expect(retrieveSchema(schema, definitions)).eql(address); + expect(retrieveSchema(schema, { definitions })).eql(address); + }); + + it("should 'resolve' a schema which contains definitions not in `#/definitions`", () => { + const address = { + type: "object", + properties: { + street_address: { type: "string" }, + city: { type: "string" }, + state: { type: "string" }, + }, + required: ["street_address", "city", "state"], + }; + const schema = { + $ref: "#/components/schemas/address", + components: { schemas: { address } }, + }; + + expect(retrieveSchema(schema, schema)).eql({ + components: { schemas: { address } }, + ...address, + }); + }); + + it("should give an error when JSON pointer is not in a URI encoded format", () => { + const address = { + type: "object", + properties: { + street_address: { type: "string" }, + city: { type: "string" }, + state: { type: "string" }, + }, + required: ["street_address", "city", "state"], + }; + const schema = { + $ref: "/components/schemas/address", + components: { schemas: { address } }, + }; + + expect(() => retrieveSchema(schema, schema)).to.throw( + "Could not find a definition" + ); + }); + + it("should give an error when JSON pointer does not point to anything", () => { + const schema = { + $ref: "#/components/schemas/address", + components: { schemas: {} }, + }; + + expect(() => retrieveSchema(schema, schema)).to.throw( + "Could not find a definition" + ); }); it("should 'resolve' escaped JSON Pointers", () => { @@ -1652,7 +1704,7 @@ describe("utils", () => { const address = { type: "string" }; const definitions = { "a~complex/name": address }; - expect(retrieveSchema(schema, definitions)).eql(address); + expect(retrieveSchema(schema, { definitions })).eql(address); }); it("should 'resolve' and stub out a schema which contains an `additionalProperties` with a definition", () => { @@ -1676,7 +1728,7 @@ describe("utils", () => { const definitions = { components: { schemas: { address } } }; const formData = { newKey: {} }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ ...schema, properties: { newKey: { @@ -1702,7 +1754,7 @@ describe("utils", () => { const definitions = { number }; const formData = { newKey: {} }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ ...schema, properties: { newKey: { @@ -1724,7 +1776,7 @@ describe("utils", () => { }; const definitions = { address }; - expect(retrieveSchema(schema, definitions)).eql({ + expect(retrieveSchema(schema, { definitions })).eql({ ...address, title: "foo", }); @@ -1746,7 +1798,7 @@ describe("utils", () => { }; const definitions = {}; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1772,7 +1824,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "1" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1798,7 +1850,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "1" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1830,7 +1882,7 @@ describe("utils", () => { }; const definitions = {}; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1856,7 +1908,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "1" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1883,7 +1935,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "1" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1912,7 +1964,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "FOO" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string", enum: ["FOO"] }, @@ -1944,7 +1996,7 @@ describe("utils", () => { }, }; const formData = { a: "1" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -1985,7 +2037,7 @@ describe("utils", () => { }, }; const formData = { a: "typeB" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { enum: ["typeA", "typeB"] }, @@ -2025,7 +2077,7 @@ describe("utils", () => { }; const definitions = {}; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string" }, @@ -2062,7 +2114,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "int" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string", enum: ["int", "bool"] }, @@ -2098,7 +2150,7 @@ describe("utils", () => { }; const definitions = {}; const formData = { a: "bool" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string", enum: ["int", "bool"] }, @@ -2184,7 +2236,7 @@ describe("utils", () => { employee_accounts: false, update_absences: "BOTH", }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { employee_accounts: { @@ -2200,7 +2252,7 @@ describe("utils", () => { employee_accounts: true, update_absences: "BOTH", }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { employee_accounts: { @@ -2259,7 +2311,7 @@ describe("utils", () => { }, }; const formData = { a: "bool" }; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "object", properties: { a: { type: "string", enum: ["int", "bool"] }, @@ -2278,7 +2330,7 @@ describe("utils", () => { }; const definitions = {}; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "string", }); }); @@ -2289,7 +2341,7 @@ describe("utils", () => { }; const definitions = {}; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({}); + expect(retrieveSchema(schema, { definitions }, formData)).eql({}); expect( console.warn.calledWithMatch(/could not merge subschemas in allOf/) ).to.be.true; @@ -2303,7 +2355,7 @@ describe("utils", () => { "2": { minLength: 5 }, }; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "string", minLength: 5, }); @@ -2323,7 +2375,7 @@ describe("utils", () => { }; const definitions = {}; const formData = {}; - expect(retrieveSchema(schema, definitions, formData)).eql({ + expect(retrieveSchema(schema, { definitions }, formData)).eql({ type: "string", minLength: 4, maxLength: 5, @@ -2533,7 +2585,7 @@ describe("utils", () => { $ref: "#/definitions/testdef", }; - expect(toIdSchema(schema, undefined, schema.definitions)).eql({ + expect(toIdSchema(schema, undefined, schema)).eql({ $id: "root", foo: { $id: "root_foo" }, bar: { $id: "root_bar" }, @@ -2558,7 +2610,7 @@ describe("utils", () => { foo: "test", }; - expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + expect(toIdSchema(schema, undefined, schema, formData)).eql({ $id: "root", foo: { $id: "root_foo" }, bar: { $id: "root_bar" }, @@ -2590,7 +2642,7 @@ describe("utils", () => { }, }; - expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + expect(toIdSchema(schema, undefined, schema, formData)).eql({ $id: "root", obj: { $id: "root_obj", @@ -2617,7 +2669,7 @@ describe("utils", () => { const formData = {}; - expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + expect(toIdSchema(schema, undefined, schema, formData)).eql({ $id: "root", foo: { $id: "root_foo" }, }); @@ -2637,13 +2689,11 @@ describe("utils", () => { $ref: "#/definitions/testdef", }; - expect(toIdSchema(schema, undefined, schema.definitions, {}, "rjsf")).eql( - { - $id: "rjsf", - foo: { $id: "rjsf_foo" }, - bar: { $id: "rjsf_bar" }, - } - ); + expect(toIdSchema(schema, undefined, schema, {}, "rjsf")).eql({ + $id: "rjsf", + foo: { $id: "rjsf_foo" }, + bar: { $id: "rjsf_bar" }, + }); }); it("should handle null form data for object schemas", () => { @@ -2738,7 +2788,7 @@ describe("utils", () => { ], }; - expect(toPathSchema(schema, "", schema.definitions, formData)).eql({ + expect(toPathSchema(schema, "", schema, formData)).eql({ $name: "", list: { $name: "list", @@ -2812,7 +2862,7 @@ describe("utils", () => { }, }; - expect(toPathSchema(schema, "", schema.definitions, formData)).eql({ + expect(toPathSchema(schema, "", schema, formData)).eql({ $name: "", billing_address: { $name: "billing_address", @@ -2875,7 +2925,7 @@ describe("utils", () => { ], }; - expect(toPathSchema(schema, "", schema.definitions, formData)).eql({ + expect(toPathSchema(schema, "", schema, formData)).eql({ $name: "", address_list: { $name: "address_list", @@ -3093,7 +3143,7 @@ describe("utils", () => { ], }; - expect(toPathSchema(schema, "", schema.definitions, formData)).eql({ + expect(toPathSchema(schema, "", schema, formData)).eql({ $name: "", defaultsAndMinItems: { $name: "defaultsAndMinItems", diff --git a/packages/material-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx b/packages/material-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx index 86dc772ed2..da721de12c 100644 --- a/packages/material-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx +++ b/packages/material-ui/src/ArrayFieldTemplate/ArrayFieldTemplate.tsx @@ -17,7 +17,8 @@ import IconButton from '../IconButton/IconButton'; const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => { const { schema, registry = getDefaultRegistry() } = props; - if (isMultiSelect(schema, registry.definitions)) { + // TODO: update types so we don't have to cast registry as any + if (isMultiSelect(schema, (registry as any).rootSchema)) { return ; } else { return ;