@rjsf/utils
Version:
Utility functions for @rjsf/core
1,305 lines (1,085 loc) • 82.2 kB
JavaScript
import isEqualWith from 'lodash-es/isEqualWith';
import get from 'lodash-es/get';
import isEmpty from 'lodash-es/isEmpty';
import jsonpointer from 'jsonpointer';
import omit from 'lodash-es/omit';
import set from 'lodash-es/set';
import mergeAllOf from 'json-schema-merge-allof';
import union from 'lodash-es/union';
import React from 'react';
import ReactIs from 'react-is';
/** 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(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(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.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(defaults, key) : {}, get(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(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(schema.additionalProperties, [REF_KEY])
}, rootSchema, formData);
} else if ("type" in schema.additionalProperties) {
additionalProperties = { ...schema.additionalProperties
};
} else {
additionalProperties = {
type: guessType(get(formData, [key]))
};
}
} else {
additionalProperties = {
type: guessType(get(formData, [key]))
};
} // The type of our new key should match the additionalProperties value;
schema.properties[key] = additionalProperties; // Set our additional property flag so we know it was dynamically added
set(schema.properties, [key, ADDITIONAL_PROPERTY_FLAG], true);
});
return schema;
}
/** Retrieves an expanded schema that has had all of its conditions, additional properties, references and dependencies
* resolved and merged into the `schema` given a `validator`, `rootSchema` and `rawFormData` that is used to do the
* potentially recursive resolution.
*
* @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({ ...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(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(formData) ? undefined : formData, schema.oneOf, rootSchema)];
} else if (ANY_OF_KEY in schema) {
schema = schema.anyOf[getMatchingOption(validator, isEmpty(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(schema, [PROPERTIES_KEY, key]), get(defaults, [key]), rootSchema, get(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(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(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(schema, [ITEMS_KEY, REF_KEY])) {
return toIdSchema(validator, get(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 in schema.properties) {
const field = get(schema, [PROPERTIES_KEY, name]);
const fieldId = idSchema[ID_KEY] + idSeparator + name;
idSchema[name] = toIdSchema(validator, isObject(field) ? field : {}, fieldId, 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
get(formData, [name]), idPrefix, idSeparator);
}
}
return idSchema;
}
/** Generates an `PathSchema` 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 `PathSchema` is desired
* @param [name=''] - The base name 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
* @returns - The `PathSchema` object for the `schema`
*/
function toPathSchema(validator, schema, name, rootSchema, formData) {
if (name === void 0) {
name = "";
}
if (REF_KEY in schema || DEPENDENCIES_KEY in schema || ALL_OF_KEY in schema) {
const _schema = retrieveSchema(validator, schema, rootSchema, formData);
return toPathSchema(valid