json-schema-describes-subset
Version:
Tools for static JSON schema analysis, including functions to determine if one schema describes a subset of another or if a schema describes the empty set or to convert a schema to its disjunctive normal form (DNF).
137 lines • 4.58 kB
JavaScript
import isString from 'lodash/isString.js';
import {} from '../json-schema/index.js';
import { mapTuple } from '../utils/type-helpers/index.js';
import { cloneData } from '../utils/clone-data/index.js';
import { traverseJSONSchema } from '../utils/traverse-json-schema/index.js';
export function normalizeURI(uri) {
if (uri === undefined) {
return undefined;
}
return uri.replace(/#\/?$/, '');
}
// export function isNonRelativeURI(uri: string): boolean {
// try {
// new URL(uri)
// return true
// } catch {
// return false
// }
// }
/**
* Returns a non relative uri or `undefined`
*
*/
export function resolveSchemaIdValue(baseURI, $id) {
if ($id === undefined) {
if (baseURI === undefined) {
return undefined;
}
try {
return normalizeURI(new URL(baseURI).href);
}
catch {
return undefined;
}
}
if ($id.startsWith('urn:') || $id.startsWith('tag:')) {
// `$id` already is non relative
return normalizeURI($id);
}
try {
return normalizeURI(new URL($id, baseURI).href);
}
catch {
/* The uri being invalid is only relevant upon encountering a relative
* `$ref` or relative `$dynamicRef` => don't throw */
return undefined;
}
}
export function createSchemaCloneWithNonRelativeIdsAndRefs(schema, baseURI, idDenyList) {
const clonedSchema = cloneData(schema);
const ids = new Set();
traverseJSONSchema(clonedSchema, (schema, baseURI, { parent }) => {
const { $id } = schema;
let newId;
if (isString($id) || (parent === null && baseURI !== undefined)) {
newId = resolveSchemaIdValue(baseURI, isString($id) ? $id : undefined);
if (newId !== undefined && !idDenyList.has(newId)) {
schema.$id = newId;
ids.add(newId);
}
else {
// remove relative `$id`s that cannot be resolved
delete schema.$id;
}
}
for (const refKeyword of ['$ref', '$dynamicRef']) {
const refValue = schema[refKeyword];
if (!isString(refValue)) {
continue;
}
const newRefValue = resolveSchemaIdValue(newId ?? baseURI, refValue);
if (newRefValue !== undefined) {
schema[refKeyword] = newRefValue;
}
}
return newId ?? baseURI;
}, baseURI);
return {
schema: clonedSchema,
ids,
};
}
export function getBaseURIsFromOptions(options) {
const baseURI = options?.baseURI;
if (Array.isArray(baseURI)) {
return baseURI.map((baseURI) => baseURI ?? undefined);
}
return [baseURI];
}
/**
* Takes `options`' `baseURI` property and returns the given `schemas` with a
* `$id` that has been resolved accordingly. The returned `options`' `baseURI`
* is set to `undefined` since this is not needed any more.
*
*/
export function resolveSchemaArgumentsIds(schemas, options) {
const baseURIs = getBaseURIsFromOptions(options);
const includedIds = new Set();
const resolvedSchemas = mapTuple(schemas, (schema, index) => {
const baseURI = baseURIs[index];
if (typeof schema === 'boolean') {
try {
/* if possible add baseURI as $id */
return {
$id: normalizeURI(new URL(baseURI ?? '').href),
allOf: [schema],
};
}
catch {
return schema;
}
}
const { schema: clonedSchema, ids } = createSchemaCloneWithNonRelativeIdsAndRefs(schema, baseURI,
/* don't include $ids that were already included in other schemas,
* this might affect JSON pointers that 'move up' and out of resource
* scope, which shouldn't be possible in draft 2020-12 anyways */
includedIds);
ids.forEach((id) => includedIds.add(id));
return clonedSchema;
});
const schemasWithoutIds = mapTuple(resolvedSchemas, (schema) => {
const clonedSchema = cloneData(schema);
traverseJSONSchema(clonedSchema, (schema) => {
delete schema.$id;
}, undefined);
return clonedSchema;
});
return {
schemas: schemasWithoutIds,
options: {
...options,
baseURI: undefined,
definitions: [...resolvedSchemas, ...(options?.definitions ?? [])],
},
};
}
//# sourceMappingURL=id.js.map