UNPKG

@rjsf/utils

Version:
1,292 lines (1,073 loc) 85.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var isEqualWith = require('lodash/isEqualWith'); var get = require('lodash/get'); var isEmpty = require('lodash/isEmpty'); var jsonpointer = require('jsonpointer'); var omit = require('lodash/omit'); var set = require('lodash/set'); var mergeAllOf = require('json-schema-merge-allof'); var union = require('lodash/union'); var React = require('react'); var ReactIs = require('react-is'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var isEqualWith__default = /*#__PURE__*/_interopDefaultLegacy(isEqualWith); var get__default = /*#__PURE__*/_interopDefaultLegacy(get); var isEmpty__default = /*#__PURE__*/_interopDefaultLegacy(isEmpty); var jsonpointer__default = /*#__PURE__*/_interopDefaultLegacy(jsonpointer); var omit__default = /*#__PURE__*/_interopDefaultLegacy(omit); var set__default = /*#__PURE__*/_interopDefaultLegacy(set); var mergeAllOf__default = /*#__PURE__*/_interopDefaultLegacy(mergeAllOf); var union__default = /*#__PURE__*/_interopDefaultLegacy(union); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var ReactIs__default = /*#__PURE__*/_interopDefaultLegacy(ReactIs); /** Determines whether a `thing` is an object for the purposes of RSJF. In this case, `thing` is an object if it has * the type `object` but is NOT null, an array or a File. * * @param thing - The thing to check to see whether it is an object * @returns - True if it is a non-null, non-array, non-File object */ function isObject(thing) { if (typeof File !== "undefined" && thing instanceof File) { return false; } return typeof thing === "object" && thing !== null && !Array.isArray(thing); } /** Checks the schema to see if it is allowing additional items, by verifying that `schema.additionalItems` is an * object. The user is warned in the console if `schema.additionalItems` has the value `true`. * * @param schema - The schema object to check * @returns - True if additional items is allowed, otherwise false */ function allowAdditionalItems(schema) { if (schema.additionalItems === true) { console.warn("additionalItems=true is currently not supported"); } return isObject(schema.additionalItems); } /** Attempts to convert the string into a number. If an empty string is provided, then `undefined` is returned. If a * `null` is provided, it is returned. If the string ends in a `.` then the string is returned because the user may be * in the middle of typing a float number. If a number ends in a pattern like `.0`, `.20`, `.030`, string is returned * because the user may be typing number that will end in a non-zero digit. Otherwise, the string is wrapped by * `Number()` and if that result is not `NaN`, that number will be returned, otherwise the string `value` will be. * * @param value - The string or null value to convert to a number * @returns - The `value` converted to a number when appropriate, otherwise the `value` */ function asNumber(value) { if (value === "") { return undefined; } if (value === null) { return null; } if (/\.$/.test(value)) { // '3.' can't really be considered a number even if it parses in js. The // user is most likely entering a float. return value; } if (/\.0$/.test(value)) { // we need to return this as a string here, to allow for input like 3.07 return value; } if (/\.\d*0$/.test(value)) { // It's a number, that's cool - but we need it as a string so it doesn't screw // with the user when entering dollar amounts or other values (such as those with // specific precision or number of significant digits) return value; } const n = Number(value); const valid = typeof n === "number" && !Number.isNaN(n); return valid ? n : value; } /** Below are the list of all the keys into various elements of a RJSFSchema or UiSchema that are used by the various * utility functions. In addition to those keys, there are the special `ADDITIONAL_PROPERTY_FLAG` and * `RJSF_ADDITONAL_PROPERTIES_FLAG` flags that is added to a schema under certain conditions by the `retrieveSchema()` * utility. */ const ADDITIONAL_PROPERTY_FLAG = "__additional_property"; const ADDITIONAL_PROPERTIES_KEY = "additionalProperties"; const ALL_OF_KEY = "allOf"; const ANY_OF_KEY = "anyOf"; const CONST_KEY = "const"; const DEFAULT_KEY = "default"; const DEFINITIONS_KEY = "definitions"; const DEPENDENCIES_KEY = "dependencies"; const ENUM_KEY = "enum"; const ERRORS_KEY = "__errors"; const ID_KEY = "$id"; const ITEMS_KEY = "items"; const NAME_KEY = "$name"; const ONE_OF_KEY = "oneOf"; const PROPERTIES_KEY = "properties"; const REQUIRED_KEY = "required"; const SUBMIT_BTN_OPTIONS_KEY = "submitButtonOptions"; const REF_KEY = "$ref"; const RJSF_ADDITONAL_PROPERTIES_FLAG = "__rjsf_additionalProperties"; const UI_FIELD_KEY = "ui:field"; const UI_WIDGET_KEY = "ui:widget"; const UI_OPTIONS_KEY = "ui:options"; /** Get all passed options from ui:options, and ui:<optionName>, returning them in an object with the `ui:` * stripped off. * * @param [uiSchema={}] - The UI Schema from which to get any `ui:xxx` options * @returns - An object containing all of the `ui:xxx` options with the stripped off */ function getUiOptions(uiSchema) { if (uiSchema === void 0) { uiSchema = {}; } return Object.keys(uiSchema).filter(key => key.indexOf("ui:") === 0).reduce((options, key) => { const value = uiSchema[key]; 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_KEY && isObject(value)) { return { ...options, ...value }; } return { ...options, [key.substring(3)]: value }; }, {}); } /** Checks whether the field described by `schema`, having the `uiSchema` and `formData` supports expanding. The UI for * the field can expand if it has additional properties, is not forced as non-expandable by the `uiSchema` and the * `formData` object doesn't already have `schema.maxProperties` elements. * * @param schema - The schema for the field that is being checked * @param [uiSchema={}] - The uiSchema for the field * @param [formData] - The formData for the field * @returns - True if the schema element has additionalProperties, is expandable, and not at the maxProperties limit */ function canExpand(schema, uiSchema, formData) { if (uiSchema === void 0) { uiSchema = {}; } if (!schema.additionalProperties) { return false; } const { expandable = true } = getUiOptions(uiSchema); if (expandable === false) { return expandable; } // if ui:options.expandable was not explicitly set to false, we can add // another property if we have not exceeded maxProperties yet if (schema.maxProperties !== undefined && formData) { return Object.keys(formData).length < schema.maxProperties; } return true; } /** Implements a deep equals using the `lodash.isEqualWith` function, that provides a customized comparator that * assumes all functions are equivalent. * * @param a - The first element to compare * @param b - The second element to compare * @returns - True if the `a` and `b` are deeply equal, false otherwise */ function deepEquals(a, b) { return isEqualWith__default["default"](a, b, (obj, other) => { if (typeof obj === "function" && typeof other === "function") { // Assume all functions are equivalent // see https://github.com/rjsf-team/react-jsonschema-form/issues/255 return true; } return undefined; // fallback to default isEquals behavior }); } /** 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]` */ function splitKeyElementFromObject(key, object) { const value = object[key]; const remaining = omit__default["default"](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 * the schema, then throw an Error. Otherwise return the sub-schema. Also deals with nested `$ref`s in the sub-schema. * * @param $ref - The ref string for which the schema definition is desired * @param [rootSchema={}] - The root schema in which to search for the definition * @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists * @throws - Error indicating that no schema for that reference exists */ function findSchemaDefinition($ref, rootSchema) { if (rootSchema === void 0) { rootSchema = {}; } let ref = $ref || ""; if (ref.startsWith("#")) { // Decode URI fragment representation. ref = decodeURIComponent(ref.substring(1)); } else { throw new Error("Could not find a definition for " + $ref + "."); } const current = jsonpointer__default["default"].get(rootSchema, ref); if (current === undefined) { throw new Error("Could not find a definition for " + $ref + "."); } 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; } return current; } /** Given the `formData` and list of `options`, attempts to find the index of the option that best matches the data. * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param formData - The current formData, if any, used to figure out a match * @param options - The list of options to find a matching options from * @param rootSchema - The root schema, used to primarily to look up `$ref`s * @returns - The index of the matched option or 0 if none is available */ function getMatchingOption(validator, formData, options, rootSchema) { // For performance, skip validating subschemas if formData is undefined. We just // want to get the first option in that case. if (formData === undefined) { return 0; } for (let i = 0; i < options.length; 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 // "requires" keyword, an object will match the schema as long as it // doesn't have matching keys with a conflicting type. To do this we use an // "anyOf" with an array of requires. This augmentation expresses that the // schema should match if any of the keys in the schema are present on the // object and pass validation. if (option.properties) { // Create an "anyOf" schema that requires at least one of the keys in the // "properties" object const requiresAnyOf = { anyOf: Object.keys(option.properties).map(key => ({ required: [key] })) }; let augmentedSchema; // If the "anyOf" keyword already exists, wrap the augmentation in an "allOf" if (option.anyOf) { // Create a shallow clone of the option const { ...shallowClone } = option; if (!shallowClone.allOf) { shallowClone.allOf = []; } else { // If "allOf" already exists, shallow clone the array shallowClone.allOf = shallowClone.allOf.slice(); } shallowClone.allOf.push(requiresAnyOf); augmentedSchema = shallowClone; } else { augmentedSchema = Object.assign({}, option, requiresAnyOf); } // Remove the "required" field as it's likely that not all fields have // been filled in yet, which will mean that the schema is not valid delete augmentedSchema.required; if (validator.isValid(augmentedSchema, formData, rootSchema)) { return i; } } else if (validator.isValid(option, formData, rootSchema)) { return i; } } return 0; } /** Given a specific `value` attempts to guess the type of a schema element. In the case where we have to implicitly * create a schema, it is useful to know what type to use based on the data we are defining. * * @param value - The value from which to guess the type * @returns - The best guess for the object type */ function guessType(value) { if (Array.isArray(value)) { return "array"; } if (typeof value === "string") { return "string"; } if (value == null) { return "null"; } if (typeof value === "boolean") { return "boolean"; } if (!isNaN(value)) { return "number"; } if (typeof value === "object") { return "object"; } // Default to string if we can't figure it out return "string"; } /** Gets the type of a given `schema`. If the type is not explicitly defined, then an attempt is made to infer it from * other elements of the schema as follows: * - schema.const: Returns the `guessType()` of that value * - schema.enum: Returns `string` * - schema.properties: Returns `object` * - schema.additionalProperties: 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 * @returns - The type of the schema */ function getSchemaType(schema) { let { type } = schema; if (!type && schema.const) { return guessType(schema.const); } if (!type && schema.enum) { return "string"; } if (!type && (schema.properties || schema.additionalProperties)) { return "object"; } if (Array.isArray(type) && type.length === 2 && type.includes("null")) { type = type.find(type => type !== "null"); } return type; } /** Detects whether the given `schema` contains fixed items. This is the case when `schema.items` is a non-empty array * that only contains objects. * * @param schema - The schema in which to check for fixed items * @returns - True if there are fixed items in the schema, false otherwise */ function isFixedItems(schema) { return Array.isArray(schema.items) && schema.items.length > 0 && schema.items.every(item => isObject(item)); } /** Merges the `defaults` object of type `T` into the `formData` of type `T` * * When merging defaults and form data, we want to merge in this specific way: * - objects are deeply merged * - arrays are merged in such a way that: * - when the array is set in form data, only array entries set in form data * are deeply merged; additional entries from the defaults are ignored * - when the array is not set in form data, the default is copied over * - scalars are overwritten/set by form data * * @param defaults - The defaults to merge * @param formData - The form data into which the defaults will be merged * @returns - The resulting merged form data with defaults */ function mergeDefaultsWithFormData(defaults, formData) { if (Array.isArray(formData)) { const defaultsArray = Array.isArray(defaults) ? defaults : []; const mapped = formData.map((value, idx) => { if (defaultsArray[idx]) { return mergeDefaultsWithFormData(defaultsArray[idx], value); } return value; }); return mapped; } if (isObject(formData)) { // eslint-disable-next-line no-unused-vars const acc = Object.assign({}, defaults); // Prevent mutation of source object. return Object.keys(formData).reduce((acc, key) => { acc[key] = mergeDefaultsWithFormData(defaults ? get__default["default"](defaults, key) : {}, get__default["default"](formData, key)); return acc; }, acc); } return formData; } /** Recursively merge deeply nested objects. * * @param obj1 - The first object to merge * @param obj2 - The second object to merge * @param [concatArrays=false] - Optional flag that, when true, will cause arrays to be concatenated * @returns - A new object that is the merge of the two given objects */ function mergeObjects(obj1, obj2, concatArrays) { if (concatArrays === void 0) { concatArrays = false; } return Object.keys(obj2).reduce((acc, key) => { const left = obj1 ? obj1[key] : {}, right = obj2[key]; 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); } else { acc[key] = right; } return acc; }, Object.assign({}, obj1)); // Prevent mutation of source object. } /** This function checks if the given `schema` matches a single constant value. This happens when either the schema has * an `enum` array with a single value or there is a `const` defined. * * @param schema - The schema for a field * @returns - True if the `schema` has a single constant value, false otherwise */ function isConstant(schema) { return Array.isArray(schema.enum) && schema.enum.length === 1 || CONST_KEY in schema; } /** 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. * * @param obj1 - The first schema object to merge * @param obj2 - The second schema object to merge * @returns - The merged schema object */ function mergeSchemas(obj1, obj2) { const acc = Object.assign({}, obj1); // Prevent mutation of source object. return Object.keys(obj2).reduce((acc, key) => { const left = obj1 ? obj1[key] : {}, right = obj2[key]; if (obj1 && key in obj1 && isObject(right)) { acc[key] = mergeSchemas(left, right); } else if (obj1 && obj2 && (getSchemaType(obj1) === "object" || getSchemaType(obj2) === "object") && key === REQUIRED_KEY && Array.isArray(left) && Array.isArray(right)) { // Don't include duplicate values when merging 'required' fields. acc[key] = union__default["default"](left, right); } else { acc[key] = right; } return acc; }, acc); } /** Resolves a conditional block (if/else/then) by removing the condition and merging the appropriate conditional branch * with the rest of the schema * * @param validator - An implementation of the `ValidatorType` interface that is used to detect valid schema conditions * @param schema - The schema for which resolving a condition is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param formData - The current formData to assist retrieving a schema * @returns - A schema with the appropriate condition resolved */ function resolveCondition(validator, schema, rootSchema, formData) { const { if: expression, then, else: otherwise, ...resolvedSchemaLessConditional } = schema; const conditionalSchema = validator.isValid(expression, formData, rootSchema) ? then : otherwise; if (conditionalSchema && typeof conditionalSchema !== "boolean") { return retrieveSchema(validator, mergeSchemas(resolvedSchemaLessConditional, retrieveSchema(validator, conditionalSchema, rootSchema, formData)), rootSchema, formData); } return retrieveSchema(validator, resolvedSchemaLessConditional, rootSchema, formData); } /** Resolves references and dependencies within a schema and its 'allOf' children. * Called internally by retrieveSchema. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which resolving a schema is desired * @param [rootSchema={}] - The root schema that will be forwarded to all the APIs * @param [formData] - The current formData, if any, to assist retrieving a schema * @returns - The schema having its references and dependencies resolved */ function resolveSchema(validator, schema, rootSchema, formData) { if (rootSchema === void 0) { rootSchema = {}; } if (REF_KEY in schema) { return resolveReference(validator, schema, rootSchema, formData); } if (DEPENDENCIES_KEY in schema) { const resolvedSchema = resolveDependencies(validator, schema, rootSchema, formData); return retrieveSchema(validator, resolvedSchema, rootSchema, formData); } if (ALL_OF_KEY in schema) { return { ...schema, allOf: schema.allOf.map(allOfSubschema => retrieveSchema(validator, allOfSubschema, rootSchema, formData)) }; } // No $ref or dependencies attribute found, returning the original schema. return schema; } /** Resolves references within a schema and its 'allOf' children. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which resolving a reference is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param [formData] - The current formData, if any, to assist retrieving a schema * @returns - The schema having its references resolved */ function resolveReference(validator, schema, rootSchema, formData) { // Retrieve the referenced schema definition. 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(validator, { ...$refSchema, ...localSchema }, rootSchema, formData); } /** 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 */ function stubExistingAdditionalProperties(validator, theSchema, rootSchema, aFormData) { // 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 = 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 = {}; if (typeof schema.additionalProperties !== "boolean") { if (REF_KEY in schema.additionalProperties) { additionalProperties = retrieveSchema(validator, { $ref: get__default["default"](schema.additionalProperties, [REF_KEY]) }, rootSchema, formData); } else if ("type" in schema.additionalProperties) { additionalProperties = { ...schema.additionalProperties }; } else { additionalProperties = { type: guessType(get__default["default"](formData, [key])) }; } } else { additionalProperties = { type: guessType(get__default["default"](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__default["default"](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. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which retrieving a schema is desired * @param [rootSchema={}] - The root schema that will be forwarded to all the APIs * @param [rawFormData] - The current formData, if any, to assist retrieving a schema * @returns - The schema having its conditions, additional properties, references and dependencies resolved */ function retrieveSchema(validator, schema, rootSchema, rawFormData) { if (rootSchema === void 0) { rootSchema = {}; } if (!isObject(schema)) { return {}; } let resolvedSchema = resolveSchema(validator, schema, rootSchema, rawFormData); if ("if" in schema) { return resolveCondition(validator, schema, rootSchema, rawFormData); } const formData = rawFormData || {}; // For each level of the dependency, we need to recursively determine the appropriate resolved schema given the current state of formData. // Otherwise, nested allOf subschemas will not be correctly displayed. if (resolvedSchema.properties) { const properties = {}; Object.entries(resolvedSchema.properties).forEach(entries => { const propName = entries[0]; const propSchema = entries[1]; const rawPropData = formData[propName]; const propData = isObject(rawPropData) ? rawPropData : {}; const resolvedPropSchema = retrieveSchema(validator, propSchema, rootSchema, propData); properties[propName] = resolvedPropSchema; if (propSchema !== resolvedPropSchema && resolvedSchema.properties !== properties) { resolvedSchema = { ...resolvedSchema, properties }; } }); } if (ALL_OF_KEY in schema) { try { resolvedSchema = mergeAllOf__default["default"]({ ...resolvedSchema, allOf: resolvedSchema.allOf }); } catch (e) { console.warn("could not merge subschemas in allOf:\n" + e); const { allOf, ...resolvedSchemaWithoutAllOf } = resolvedSchema; return resolvedSchemaWithoutAllOf; } } const hasAdditionalProperties = ADDITIONAL_PROPERTIES_KEY in resolvedSchema && resolvedSchema.additionalProperties !== false; if (hasAdditionalProperties) { return stubExistingAdditionalProperties(validator, resolvedSchema, rootSchema, formData); } return resolvedSchema; } /** Resolves dependencies within a schema and its 'allOf' children. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which resolving a dependency is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param [formData] - The current formData, if any, to assist retrieving a schema * @returns - The schema with its dependencies resolved */ function resolveDependencies(validator, schema, rootSchema, formData) { // Drop the dependencies from the source schema. const { dependencies, ...remainingSchema } = schema; let resolvedSchema = remainingSchema; if (Array.isArray(resolvedSchema.oneOf)) { resolvedSchema = resolvedSchema.oneOf[getMatchingOption(validator, formData, resolvedSchema.oneOf, rootSchema)]; } else if (Array.isArray(resolvedSchema.anyOf)) { resolvedSchema = resolvedSchema.anyOf[getMatchingOption(validator, formData, resolvedSchema.anyOf, rootSchema)]; } return processDependencies(validator, dependencies, resolvedSchema, rootSchema, formData); } /** Processes all the `dependencies` recursively into the `resolvedSchema` as needed * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param dependencies - The set of dependencies that needs to be processed * @param resolvedSchema - The schema for which processing dependencies is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param [formData] - The current formData, if any, to assist retrieving a schema * @returns - The schema with the `dependencies` resolved into it */ function processDependencies(validator, dependencies, resolvedSchema, rootSchema, formData) { let schema = resolvedSchema; // Process dependencies updating the local schema properties as appropriate. for (const dependencyKey in dependencies) { // Skip this dependency if its trigger property is not present. if (get__default["default"](formData, [dependencyKey]) === undefined) { continue; } // Skip this dependency if it is not included in the schema (such as when dependencyKey is itself a hidden dependency.) if (schema.properties && !(dependencyKey in schema.properties)) { continue; } const [remainingDependencies, dependencyValue] = splitKeyElementFromObject(dependencyKey, dependencies); if (Array.isArray(dependencyValue)) { schema = withDependentProperties(schema, dependencyValue); } else if (isObject(dependencyValue)) { schema = withDependentSchema(validator, schema, rootSchema, dependencyKey, dependencyValue, formData); } return processDependencies(validator, remainingDependencies, schema, rootSchema, formData); } return schema; } /** Updates a schema with additionally required properties added * * @param schema - The schema for which resolving a dependent properties is desired * @param [additionallyRequired] - An optional array of additionally required names * @returns - The schema with the additional required values merged in */ function withDependentProperties(schema, additionallyRequired) { if (!additionallyRequired) { return schema; } const required = Array.isArray(schema.required) ? Array.from(new Set([...schema.required, ...additionallyRequired])) : additionallyRequired; return { ...schema, required: required }; } /** Merges a dependent schema into the `schema` dealing with oneOfs and references * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which resolving a dependent schema is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param dependencyKey - The key name of the dependency * @param dependencyValue - The potentially dependent schema * @param formData- The current formData to assist retrieving a schema * @returns - The schema with the dependent schema resolved into it */ function withDependentSchema(validator, schema, rootSchema, dependencyKey, dependencyValue, formData) { const { oneOf, ...dependentSchema } = retrieveSchema(validator, dependencyValue, rootSchema, formData); schema = mergeSchemas(schema, dependentSchema); // Since it does not contain oneOf, we return the original schema. if (oneOf === undefined) { return schema; } // Resolve $refs inside oneOf. const resolvedOneOf = oneOf.map(subschema => { if (typeof subschema === "boolean" || !(REF_KEY in subschema)) { return subschema; } return resolveReference(validator, subschema, rootSchema, formData); }); return withExactlyOneSubschema(validator, schema, rootSchema, dependencyKey, resolvedOneOf, formData); } /** Returns a `schema` with the best choice from the `oneOf` options merged into it * * @param validator - An implementation of the `ValidatorType` interface that will be used to validate oneOf options * @param schema - The schema for which resolving a oneOf subschema is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param dependencyKey - The key name of the oneOf dependency * @param oneOf - The list of schemas representing the oneOf options * @param [formData] - The current formData to assist retrieving a schema * @returns The schema with best choice of oneOf schemas merged into */ function withExactlyOneSubschema(validator, schema, rootSchema, dependencyKey, oneOf, formData) { const validSubschemas = oneOf.filter(subschema => { if (typeof subschema === "boolean" || !subschema.properties) { return false; } const { [dependencyKey]: conditionPropertySchema } = subschema.properties; if (conditionPropertySchema) { const conditionSchema = { type: "object", properties: { [dependencyKey]: conditionPropertySchema } }; const { errors } = validator.validateFormData(formData, conditionSchema); return errors.length === 0; } return false; }); if (validSubschemas.length !== 1) { console.warn("ignoring oneOf in dependencies because there isn't exactly one subschema that is valid"); return schema; } const subschema = validSubschemas[0]; const [dependentSubschema] = splitKeyElementFromObject(dependencyKey, subschema.properties); const dependentSchema = { ...subschema, properties: dependentSubschema }; return mergeSchemas(schema, retrieveSchema(validator, dependentSchema, rootSchema, formData)); } /** Checks to see if the `schema` combination represents a select * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param theSchema - The schema for which check for a select flag is desired * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @returns - True if schema contains a select, otherwise false */ function isSelect(validator, theSchema, rootSchema) { if (rootSchema === void 0) { rootSchema = {}; } const schema = retrieveSchema(validator, theSchema, rootSchema, undefined); const altSchemas = schema.oneOf || schema.anyOf; if (Array.isArray(schema.enum)) { return true; } if (Array.isArray(altSchemas)) { return altSchemas.every(altSchemas => typeof altSchemas !== "boolean" && isConstant(altSchemas)); } return false; } /** Checks to see if the `schema` combination represents a multi-select * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param schema - The schema for which check for a multi-select flag is desired * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @returns - True if schema contains a multi-select, otherwise false */ function isMultiSelect(validator, schema, rootSchema) { if (!schema.uniqueItems || !schema.items || typeof schema.items === "boolean") { return false; } return isSelect(validator, schema.items, rootSchema); } /** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function. */ var AdditionalItemsHandling; (function (AdditionalItemsHandling) { AdditionalItemsHandling[AdditionalItemsHandling["Ignore"] = 0] = "Ignore"; AdditionalItemsHandling[AdditionalItemsHandling["Invert"] = 1] = "Invert"; AdditionalItemsHandling[AdditionalItemsHandling["Fallback"] = 2] = "Fallback"; })(AdditionalItemsHandling || (AdditionalItemsHandling = {})); /** Given a `schema` will return an inner schema that for an array item. This is computed differently based on the * `additionalItems` enum and the value of `idx`. There are four possible returns: * 1. If `idx` is >= 0, then if `schema.items` is an array the `idx`th element of the array is returned if it is a valid * index and not a boolean, otherwise it falls through to 3. * 2. If `schema.items` is not an array AND truthy and not a boolean, then `schema.items` is returned since it actually * is a schema, otherwise it falls through to 3. * 3. If `additionalItems` is not `AdditionalItemsHandling.Ignore` and `schema.additionalItems` is an object, then * `schema.additionalItems` is returned since it actually is a schema, otherwise it falls through to 4. * 4. {} is returned representing an empty schema * * @param schema - The schema from which to get the particular item * @param [additionalItems=AdditionalItemsHandling.Ignore] - How do we want to handle additional items? * @param [idx=-1] - Index, if non-negative, will be used to return the idx-th element in a `schema.items` array * @returns - The best fit schema object from the `schema` given the `additionalItems` and `idx` modifiers */ function getInnerSchemaForArrayItem(schema, additionalItems, idx) { if (additionalItems === void 0) { additionalItems = AdditionalItemsHandling.Ignore; } if (idx === void 0) { idx = -1; } if (idx >= 0) { if (Array.isArray(schema.items) && idx < schema.items.length) { const item = schema.items[idx]; if (typeof item !== "boolean") { return item; } } } else if (schema.items && !Array.isArray(schema.items) && typeof schema.items !== "boolean") { return schema.items; } if (additionalItems !== AdditionalItemsHandling.Ignore && isObject(schema.additionalItems)) { return schema.additionalItems; } return {}; } /** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into * each level of the schema, recursively, to fill out every level of defaults provided by the schema. * * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary * @param schema - The schema for which the default state is desired * @param [parentDefaults] - Any defaults provided by the parent field in the schema * @param [rootSchema] - The options root schema, used to primarily to look up `$ref`s * @param [rawFormData] - 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 */ function computeDefaults(validator, schema, parentDefaults, rootSchema, rawFormData, includeUndefinedValues) { if (rootSchema === void 0) { rootSchema = {}; } if (includeUndefinedValues === void 0) { includeUndefinedValues = false; } const formData = isObject(rawFormData) ? rawFormData : {}; // Compute the defaults recursively: give highest priority to deepest nodes. let defaults = parentDefaults; if (isObject(defaults) && isObject(schema.default)) { // For object defaults, only override parent defaults that are defined in // schema.default. defaults = mergeObjects(defaults, schema.default); } else if (DEFAULT_KEY in schema) { defaults = schema.default; } else if (REF_KEY in schema) { // Use referenced schema defaults for this node. const refSchema = findSchemaDefinition(schema[REF_KEY], rootSchema); return computeDefaults(validator, refSchema, defaults, rootSchema, formData, includeUndefinedValues); } else if (DEPENDENCIES_KEY in schema) { const resolvedSchema = resolveDependencies(validator, schema, rootSchema, formData); return computeDefaults(validator, resolvedSchema, defaults, rootSchema, formData, includeUndefinedValues); } else if (isFixedItems(schema)) { defaults = schema.items.map((itemSchema, idx) => computeDefaults(validator, itemSchema, Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined, rootSchema, formData, includeUndefinedValues)); } else if (ONE_OF_KEY in schema) { schema = schema.oneOf[getMatchingOption(validator, isEmpty__default["default"](formData) ? undefined : formData, schema.oneOf, rootSchema)]; } else if (ANY_OF_KEY in schema) { schema = schema.anyOf[getMatchingOption(validator, isEmpty__default["default"](formData) ? undefined : formData, schema.anyOf, rootSchema)]; } // Not defaults defined for this node, fallback to generic typed ones. if (typeof defaults === "undefined") { defaults = schema.default; } switch (getSchemaType(schema)) { // We need to recur for object schema inner default values. case "object": return Object.keys(schema.properties || {}).reduce((acc, key) => { // Compute the defaults for this node, with the parent defaults we might // have from a previous run: defaults[key]. const computedDefault = computeDefaults(validator, get__default["default"](schema, [PROPERTIES_KEY, key]), get__default["default"](defaults, [key]), rootSchema, get__default["default"](formData, [key]), includeUndefinedValues); if (includeUndefinedValues || computedDefault !== undefined) { acc[key] = computedDefault; } return acc; }, {}); case "array": // Inject defaults into existing array defaults if (Array.isArray(defaults)) { defaults = defaults.map((item, idx) => { const schemaItem = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Fallback, idx); return computeDefaults(validator, schemaItem, item, rootSchema); }); } // Deeply inject defaults into already existing form data if (Array.isArray(rawFormData)) { const schemaItem = getInnerSchemaForArrayItem(schema); defaults = rawFormData.map((item, idx) => { return computeDefaults(validator, schemaItem, get__default["default"](defaults, [idx]), rootSchema, item); }); } if (schema.minItems) { if (!isMultiSelect(validator, schema, rootSchema)) { const defaultsLength = Array.isArray(defaults) ? defaults.length : 0; if (schema.minItems > defaultsLength) { const defaultEntries = defaults || []; // populate the array with the defaults const fillerSchema = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert); const fillerDefault = fillerSchema.default; const fillerEntries = new Array(schema.minItems - defaultsLength).fill(computeDefaults(validator, fillerSchema, fillerDefault, rootSchema)); // then fill up the rest with either the item default or empty, up to minItems return defaultEntries.concat(fillerEntries); } } return defaults ? defaults : []; } } return defaults; } /** 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 validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param theSchema - The schema for which the default state is desired * @param [formData] - The current formData, if any, onto which to provide any missing defaults * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults * @returns - The resulting `formData` with all the defaults provided */ function getDefaultFormState(validator, theSchema, formData, rootSchema, includeUndefinedValues) { if (includeUndefinedValues === void 0) { includeUndefinedValues = false; } if (!isObject(theSchema)) { throw new Error("Invalid schema: " + theSchema); } const schema = retrieveSchema(validator, theSchema, rootSchema, formData); const defaults = computeDefaults(validator, schema, undefined, rootSchema, formData, includeUndefinedValues); if (typeof formData === "undefined" || formData === null || typeof formData === "number" && isNaN(formData)) { // No form data? Use schema defaults. return defaults; } if (isObject(formData)) { return mergeDefaultsWithFormData(defaults, formData); } if (Array.isArray(formData)) { return mergeDefaultsWithFormData(defaults, formData); } return formData; } /** Checks to see if the `uiSchema` contains the `widget` field and that the widget is not `hidden` * * @param uiSchema - The UI Schema from which to detect if it is customized * @returns - True if the `uiSchema` describes a custom widget, false otherwise */ function isCustomWidget(uiSchema) { if (uiSchema === void 0) { uiSchema = {}; } return (// TODO: Remove the `&& uiSchema['ui:widget'] !== 'hidden'` once we support hidden widgets for arrays. // https://react-jsonschema-form.readthedocs.io/en/latest/usage/widgets/#hidden-widgets "widget" in getUiOptions(uiSchema) && getUiOptions(uiSchema)["widget"] !== "hidden" ); } /** Checks to see if the `schema` and `uiSchema` combination represents an array of files * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @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 * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @returns - True if schema/uiSchema contains an array of files, otherwise false */ function isFilesArray(validator, schema, uiSchema, rootSchema) { if (uiSchema === void 0) { uiSchema = {}; } if (uiSchema[UI_WIDGET_KEY] === "files") { return true; } if (schema.items) { const itemsSchema = retrieveSchema(validator, schema.items, rootSchema); return itemsSchema.type === "string" && itemsSchema.format === "data-url"; } return false; } /** Determines whether the combination of `schema` and `uiSchema` properties indicates that the label for the `schema` * should be displayed in a UI. * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param schema - The schema for which the display label flag is desired * @param [uiSchema={}] - The UI schema from which to derive potentially displayable information * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @returns - True if the label should be displayed or false if it should not */ function getDisplayLabel(validator, schema, uiSchema, rootSchema) { if (uiSchema === void 0) { uiSchema = {}; } const uiOptions = getUiOptions(uiSchema); const { label = true } = uiOptions; let displayLabel = !!label; const schemaType = getSchemaType(schema); if (schemaType === "array") { displayLabel = isMultiSelect(validator, schema, rootSchema) || isFilesArray(validator, schema, uiSchema, rootSchema) || isCustomWidget(uiSchema); } if (schemaType === "object") { displayLabel = false; } if (schemaType === "boolean" && !uiSchema[UI_WIDGET_KEY]) { displayLabel = false; } if (uiSchema[UI_FIELD_KEY]) { displayLabel = false; } return displayLabel; } /** Merges the errors in `additionalErrorSchema` into the existing `validationData` by combining the hierarchies in the * two `ErrorSchema`s and then appending the error list from the `additionalErrorSchema` obtained by calling * `validator.toErrorList()` onto the `errors` in the `validationData`. If no `additionalErrorSchema` is passed, then * `validationData` is returned. * * @param validator - The validator used to convert an ErrorSchema to a list of errors * @param validationData - The current `ValidationData` into which to merge the additional errors * @param [additionalErrorSchema] - The additional set of errors in an `ErrorSchema` * @returns - The `validationData` with the additional errors from `additionalErrorSchema` merged into it, if provided. */ function mergeValidationData(validator, validationData, additionalErrorSchema) { if (!additionalErrorSchema) { return validationData; } const { errors: oldErrors, errorSchema: oldErrorSchema } = validationData; let errors = validator.toErrorList(additionalErrorSchema); let errorSchema = additionalErrorSchema; if (!isEmpty__default["default"](oldErrorSchema)) { errorSchema = mergeObjects(oldErrorSchema, additionalErrorSchema, true); errors = [...oldErrors].concat(errors); } return { errorSchema, errors }; } /** Generates an `IdSchema` object for the `schema`, recursively * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param schema - The schema for which the `IdSchema` is desired * @param [id] - The base id for the schema * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s * @param [formData] - The current formData, if any, to assist retrieving a schema * @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` */ function toIdSchema(validator, schema, id, rootSchema, formData, idPrefix, idSeparator) { if (idPrefix === void 0) { idPrefix = "root"; } if (idSeparator === void 0) { idSeparator = "_"; } if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) { const _schema = retrieveSchema(validator, schema, rootSchema, formData); return toIdSchema(validator, _schema, id, rootSchema, formData, idPrefix, idSeparator); } if (ITEMS_KEY in schema && !get__default["default"](schema, [ITEMS_KEY, REF_KEY])) { return toIdSchema(validator, get__default["default"](schema, ITEMS_KEY), id, rootSchema, formData, idPrefix, idSeparator); } const $id = id || idPrefix; const idSchema = { $id }; if (schema.type === "object" && PROPERTIES_KEY in schema) { for (const name