botbook/node_modules/json-schema-library/lib/getTemplate.ts

480 lines
16 KiB
TypeScript
Raw Normal View History

2024-09-04 13:13:15 -03:00
/* eslint quote-props: 0, max-statements-per-line: ["error", { "max": 2 }] */
import { resolveOneOfFuzzy } from "./features/oneOf";
import getTypeOf from "./getTypeOf";
import merge from "./utils/merge";
import copy from "./utils/copy";
import settings from "./config/settings";
import { JsonSchema, JsonPointer, isJsonError } from "./types";
import { Draft } from "./draft";
import { isEmpty } from "./utils/isEmpty";
import { resolveIfSchema } from "./features/if";
import { mergeAllOfSchema, resolveSchema } from "./features/allOf";
import { resolveDependencies } from "./features/dependencies";
import { mergeSchema } from "./mergeSchema";
export type TemplateOptions = {
/** Add all properties (required and optional) to the generated data */
addOptionalProps?: boolean;
/** Remove data that does not match input schema. Defaults to false */
removeInvalidData?: boolean;
/** Set to false to take default values as they are and not extend them.
* Defaults to true.
* This allows to control template data e.g. enforcing arrays to be empty,
* regardless of minItems settings.
*/
extendDefaults?: boolean;
};
const defaultOptions: TemplateOptions = settings.templateDefaultOptions;
let cache: Record<string, JsonSchema>;
function shouldResolveRef(schema: JsonSchema, pointer: JsonPointer) {
const { $ref } = schema;
if ($ref == null) {
return true;
}
const value = cache[pointer] == null || cache[pointer][$ref] == null ? 0 : cache[pointer][$ref];
return value < settings.GET_TEMPLATE_RECURSION_LIMIT;
}
function resolveRef(draft: Draft, schema: JsonSchema, pointer: JsonPointer) {
const { $ref } = schema;
if ($ref == null) {
return schema;
}
// @todo pointer + ref is redundant?
cache[pointer] = cache[pointer] || {};
cache[pointer][$ref] = cache[pointer][$ref] || 0;
cache[pointer][$ref] += 1;
return draft.resolveRef(schema);
}
function convertValue(type: string, value: any) {
if (type === "string") {
return JSON.stringify(value);
} else if (typeof value !== "string") {
return null;
}
try {
value = JSON.parse(value);
if (typeof value === type) {
return value;
}
} catch (e) {} // eslint-disable-line no-empty
return null;
}
/**
* Resolves $ref, allOf and anyOf schema-options, returning a combined json-schema.
* Also returns a pointer-property on schema, that must be used as current pointer.
*
* @param draft
* @param schema
* @param data
* @param pointer
* @return resolved json-schema or input-schema
*/
function createTemplateSchema(
draft: Draft,
schema: JsonSchema,
data: unknown,
pointer: JsonPointer,
opts: TemplateOptions
): JsonSchema | false {
// invalid schema
if (getTypeOf(schema) !== "object") {
return Object.assign({ pointer }, schema);
}
// return if reached recursion limit
if (shouldResolveRef(schema, pointer) === false && data == null) {
return false;
}
// resolve $ref and copy schema
let templateSchema = copy(resolveRef(draft, schema, pointer));
// @feature anyOf
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
// test if we may resolve
if (shouldResolveRef(schema.anyOf[0], `${pointer}/anyOf/0`)) {
const resolvedAnyOf = resolveRef(draft, schema.anyOf[0], `${pointer}/anyOf/0`);
templateSchema = merge(templateSchema, resolvedAnyOf);
// add pointer return-value, if any
templateSchema.pointer = schema.anyOf[0].$ref || templateSchema.pointer;
}
delete templateSchema.anyOf;
}
// @feature allOf
if (Array.isArray(schema.allOf)) {
const mayResolve = schema.allOf
.map((allOf, index) => shouldResolveRef(allOf, `${pointer}/allOf/${index}`))
.reduceRight((next, before) => next && before, true);
if (mayResolve) {
// before merging all-of, we need to resolve all if-then-else statesments
// we need to udpate data on the way to trigger if-then-else schemas sequentially.
// Note that this will make if-then-else order-dependent
const allOf = [];
let extendedData = copy(data);
for (let i = 0; i < schema.allOf.length; i += 1) {
allOf.push(resolveSchema(draft, schema.allOf[i], extendedData));
extendedData = getTemplate(draft, extendedData, { type: schema.type, ...allOf[i] }, `${pointer}/allOf/${i}`, opts);
}
const resolvedSchema = mergeAllOfSchema(draft, { allOf });
if (resolvedSchema) {
templateSchema = mergeSchema(templateSchema, resolvedSchema);
}
}
}
templateSchema.pointer = templateSchema.pointer || schema.$ref || pointer;
return templateSchema;
}
const isJsonSchema = (template: unknown): template is JsonSchema =>
template && typeof template === "object";
/**
* Create data object matching the given schema
*
* @param draft - json schema draft
* @param [data] - optional template data
* @param [schema] - json schema, defaults to rootSchema
* @return created template data
*/
function getTemplate(
draft: Draft,
data?: unknown,
_schema?: JsonSchema,
pointer?: JsonPointer,
opts?: TemplateOptions
) {
if (_schema == null) {
throw new Error(`getTemplate: missing schema for data: ${JSON.stringify(data)}`);
}
if (pointer == null) {
throw new Error("Missing pointer");
}
// resolve $ref references, allOf and first anyOf definitions
let schema = createTemplateSchema(draft, _schema, data, pointer, opts);
if (!isJsonSchema(schema)) {
return undefined;
}
pointer = schema.pointer;
if (schema?.const) {
return schema.const;
}
// @feature oneOf
if (Array.isArray(schema.oneOf)) {
if (isEmpty(data)) {
const type =
schema.oneOf[0].type ||
schema.type ||
(schema.const && typeof schema.const) ||
getTypeOf(data);
schema = { ...schema.oneOf[0], type };
} else {
// find correct schema for data
const resolvedSchema = resolveOneOfFuzzy(draft, data, schema);
if (isJsonError(resolvedSchema)) {
if (data != null && opts.removeInvalidData !== true) {
return data;
}
// override
schema = schema.oneOf[0];
data = undefined;
} else {
resolvedSchema.type = resolvedSchema.type ?? schema.type;
schema = resolvedSchema;
}
}
}
// @todo Array.isArray(schema.type)
// -> hasDefault? return
// if not -> pick first types
if (!isJsonSchema(schema) || schema.type == null) {
return undefined;
}
// @attention - very special case to support file instances
if (data instanceof File) {
return data;
}
const type = Array.isArray(schema.type)
? selectType(schema.type, data, schema.default)
: schema.type;
// reset invalid type
const javascriptTypeOfData = getTypeOf(data);
if (
data != null &&
javascriptTypeOfData !== type &&
!(javascriptTypeOfData === "number" && type === "integer")
) {
data = convertValue(type, data);
}
if (TYPE[type] == null) {
// in case we could not resolve the type
// (schema-type could not be resolved and returned an error)
if (opts.removeInvalidData) {
return undefined;
}
return data;
}
const templateData = TYPE[type](draft, schema, data, pointer, opts);
return templateData;
}
function selectType(types: string[], data: unknown, defaultValue: unknown) {
if (data == undefined) {
if (defaultValue != null) {
const defaultType = getTypeOf(defaultValue);
if (types.includes(defaultType)) {
return defaultType;
}
}
return types[0];
}
const dataType = getTypeOf(data);
if (types.includes(dataType)) {
return dataType;
}
return types[0];
}
const TYPE: Record<
string,
(
draft: Draft,
schema: JsonSchema,
data: unknown,
pointer: JsonPointer,
opts: TemplateOptions
) => unknown
> = {
null: (draft, schema, data) => getDefault(schema, data, null),
string: (draft, schema, data) => getDefault(schema, data, ""),
number: (draft, schema, data) => getDefault(schema, data, 0),
integer: (draft, schema, data) => getDefault(schema, data, 0),
boolean: (draft, schema, data) => getDefault(schema, data, false),
object: (
draft,
schema,
data: Record<string, unknown> | undefined,
pointer: JsonPointer,
opts: TemplateOptions
) => {
const template = schema.default === undefined ? {} : schema.default;
const d: Record<string, unknown> = {}; // do not assign data here, to keep ordering from json-schema
const required = (opts.extendDefaults === false && schema.default !== undefined) ? [] : (schema.required ?? []);
if (schema.properties) {
Object.keys(schema.properties).forEach((key) => {
const value = data == null || data[key] == null ? template[key] : data[key];
const isRequired = required.includes(key);
// Omit adding a property if it is not required or optional props should be added
if (value != null || isRequired || opts.addOptionalProps) {
d[key] = getTemplate(
draft,
value,
schema.properties[key],
`${pointer}/properties/${key}`,
opts
);
}
});
}
// @feature dependencies
// has to be done after resolving properties so dependency may trigger
let dependenciesSchema = resolveDependencies(draft, schema, d);
if (dependenciesSchema) {
dependenciesSchema = mergeSchema(schema, dependenciesSchema);
delete dependenciesSchema.dependencies;
const dependencyData = getTemplate(
draft,
data,
dependenciesSchema,
`${pointer}/dependencies`,
opts
);
Object.assign(d, dependencyData);
}
if (data) {
if (
opts.removeInvalidData === true &&
(schema.additionalProperties === false ||
getTypeOf(schema.additionalProperties) === "object")
) {
if (getTypeOf(schema.additionalProperties) === "object") {
Object.keys(data).forEach((key) => {
if (d[key] == null) {
// merge valid missing data (additionals) to resulting object
if (draft.isValid(data[key], schema.additionalProperties)) {
d[key] = data[key];
}
}
});
}
} else {
// merge any missing data (additionals) to resulting object
Object.keys(data).forEach((key) => d[key] == null && (d[key] = data[key]));
}
}
// @feature if-then-else
const ifSchema = resolveIfSchema(draft, schema, d);
if (ifSchema) {
const additionalData = getTemplate(
draft,
d,
{ type: "object", ...ifSchema },
pointer,
opts
);
Object.assign(d, additionalData);
}
// returns object, which is ordered by json-schema
return d;
},
// build array type of items, ignores additionalItems
array: (
draft: Draft,
schema: JsonSchema,
data: unknown[],
pointer: JsonPointer,
opts: TemplateOptions
) => {
if (schema.items == null) {
return data || []; // items are undefined
}
const template = schema.default === undefined ? [] : schema.default;
const d: unknown[] = data || template;
const minItems = (opts.extendDefaults === false && schema.default !== undefined) ? 0 : (schema.minItems || 0);
// build defined set of items
if (Array.isArray(schema.items)) {
for (let i = 0, l = Math.max(minItems ?? 0, schema.items?.length ?? 0); i < l; i += 1) {
d[i] = getTemplate(
draft,
d[i] == null ? template[i] : d[i],
schema.items[i],
`${pointer}/items/${i}`,
opts
);
}
return d;
}
// abort if the schema is invalid
if (getTypeOf(schema.items) !== "object") {
return d;
}
// resolve allOf and first anyOf definition
const templateSchema = createTemplateSchema(draft, schema.items, data, pointer, opts);
if (templateSchema === false) {
return d;
}
pointer = templateSchema.pointer || pointer;
// build data for first oneOf-schema
if (templateSchema.oneOf && d.length === 0) {
const oneOfSchema = templateSchema.oneOf[0];
for (let i = 0; i < minItems; i += 1) {
d[i] = getTemplate(
draft,
d[i] == null ? template[i] : d[i],
oneOfSchema,
`${pointer}/oneOf/0`,
opts
);
}
return d;
}
// complete data selecting correct oneOf-schema
if (templateSchema.oneOf && d.length > 0) {
const itemCount = Math.max(minItems, d.length);
for (let i = 0; i < itemCount; i += 1) {
let value = d[i] == null ? template[i] : d[i];
let one = resolveOneOfFuzzy(draft, value, templateSchema);
if (one == null || isJsonError(one)) {
// schema could not be resolved or data is invalid
if (value != null && opts.removeInvalidData !== true) {
// keep invalid value
d[i] = value;
} else {
// replace invalid value
value = undefined;
one = templateSchema.oneOf[0];
d[i] = getTemplate(draft, value, one, `${pointer}/oneOf/${i}`, opts);
}
} else {
// schema is valid
d[i] = getTemplate(draft, value, one, `${pointer}/oneOf/${i}`, opts);
}
}
return d;
}
// build data from items-definition
if (templateSchema.type) {
for (let i = 0, l = Math.max(minItems, d.length); i < l; i += 1) {
d[i] = getTemplate(
draft,
d[i] == null ? template[i] : d[i],
templateSchema,
`${pointer}/items`,
opts
);
}
return d;
}
return d;
}
};
function getDefault(schema: JsonSchema, templateValue: any, initValue: any) {
if (templateValue != null) {
return templateValue;
} else if (schema.const) {
return schema.const;
} else if (schema.default === undefined && Array.isArray(schema.enum)) {
return schema.enum[0];
} else if (schema.default === undefined) {
return initValue;
}
return schema.default;
}
export default (
draft: Draft,
data?: any,
schema: JsonSchema = draft.rootSchema,
opts?: TemplateOptions
) => {
cache = {};
if (opts) {
return getTemplate(draft, data, schema, "#", { ...defaultOptions, ...opts });
}
return getTemplate(draft, data, schema, "#", defaultOptions);
};