@hyperjump/json-schema
Version:
A JSON Schema validator with support for custom keywords, vocabularies, and dialects
272 lines (222 loc) • 9.27 kB
JavaScript
import curry from "just-curry-it";
import { v4 as uuid } from "uuid";
import * as Browser from "@hyperjump/browser";
import * as JsonPointer from "@hyperjump/json-pointer";
import { asyncCollectSet, asyncFilter, asyncFlatten, asyncMap, pipe } from "@hyperjump/pact";
import {
Validation,
canonicalUri, getSchema, toSchema,
getKeywordName, getKeyword, getKeywordByName
} from "../lib/experimental.js";
export const URI = "uri", UUID = "uuid";
const defaultOptions = {
alwaysIncludeDialect: false,
definitionNamingStrategy: URI,
externalSchemas: []
};
export const bundle = async (url, options = {}) => {
loadKeywordSupport();
const fullOptions = { ...defaultOptions, ...options };
const mainSchema = await getSchema(url);
const contextUri = mainSchema.document.baseUri;
const contextDialectId = mainSchema.document.dialectId;
const bundled = toSchema(mainSchema);
const externalIds = await Validation.collectExternalIds(new Set(), mainSchema, mainSchema);
// Bundle
const bundlingLocation = "/" + getKeywordName(contextDialectId, "https://json-schema.org/keyword/definitions");
if (JsonPointer.get(bundlingLocation, bundled) === undefined && externalIds.size > 0) {
JsonPointer.assign(bundlingLocation, bundled, {});
}
for (const uri of externalIds) {
if (fullOptions.externalSchemas.includes(uri)) {
continue;
}
const externalSchema = await getSchema(uri);
const embeddedSchema = toSchema(externalSchema, {
contextUri: externalSchema.document.baseUri.startsWith("file:") ? contextUri : undefined,
includeDialect: fullOptions.alwaysIncludeDialect ? "always" : "auto",
contextDialectId: contextDialectId
});
let id;
if (fullOptions.definitionNamingStrategy === URI) {
const idToken = getKeywordName(externalSchema.document.dialectId, "https://json-schema.org/keyword/id")
|| getKeywordName(externalSchema.document.dialectId, "https://json-schema.org/keyword/draft-04/id");
id = embeddedSchema[idToken];
} else if (fullOptions.definitionNamingStrategy === UUID) {
id = uuid();
} else {
throw Error(`Unknown definition naming stragety: ${fullOptions.definitionNamingStrategy}`);
}
const pointer = JsonPointer.append(id, bundlingLocation);
JsonPointer.assign(pointer, bundled, embeddedSchema);
}
return bundled;
};
Validation.collectExternalIds = curry(async (visited, parentSchema, schema) => {
const uri = canonicalUri(schema);
if (visited.has(uri) || Browser.typeOf(schema) === "boolean") {
return new Set();
}
visited.add(uri);
const externalIds = await pipe(
Browser.entries(schema),
asyncMap(async ([keyword, keywordSchema]) => {
const keywordHandler = getKeywordByName(keyword, schema.document.dialectId);
return "collectExternalIds" in keywordHandler
? await keywordHandler.collectExternalIds(visited, schema, keywordSchema)
: new Set();
}),
asyncFlatten,
asyncCollectSet
);
if (parentSchema.document.baseUri !== schema.document.baseUri
&& (!(schema.document.baseUri in parentSchema.document.embedded) || schema.document.baseUri in parentSchema._cache)
) {
externalIds.add(schema.document.baseUri);
}
return externalIds;
});
const collectExternalIdsFromArrayOfSchemas = (visited, parentSchema, schema) => pipe(
Browser.iter(schema),
asyncMap(Validation.collectExternalIds(visited, parentSchema)),
asyncFlatten,
asyncCollectSet
);
const collectExternalIdsFromObjectOfSchemas = async (visited, parentSchema, schema) => pipe(
Browser.values(schema),
asyncMap(Validation.collectExternalIds(visited, parentSchema)),
asyncFlatten,
asyncCollectSet
);
const loadKeywordSupport = () => {
// Stable
const additionalProperties = getKeyword("https://json-schema.org/keyword/additionalProperties");
if (additionalProperties) {
additionalProperties.collectExternalIds = Validation.collectExternalIds;
}
const allOf = getKeyword("https://json-schema.org/keyword/allOf");
if (allOf) {
allOf.collectExternalIds = collectExternalIdsFromArrayOfSchemas;
}
const anyOf = getKeyword("https://json-schema.org/keyword/anyOf");
if (anyOf) {
anyOf.collectExternalIds = collectExternalIdsFromArrayOfSchemas;
}
const contains = getKeyword("https://json-schema.org/keyword/contains");
if (contains) {
contains.collectExternalIds = Validation.collectExternalIds;
}
const dependentSchemas = getKeyword("https://json-schema.org/keyword/dependentSchemas");
if (dependentSchemas) {
dependentSchemas.collectExternalIds = collectExternalIdsFromObjectOfSchemas;
}
const if_ = getKeyword("https://json-schema.org/keyword/if");
if (if_) {
if_.collectExternalIds = Validation.collectExternalIds;
}
const then = getKeyword("https://json-schema.org/keyword/then");
if (then) {
then.collectExternalIds = Validation.collectExternalIds;
}
const else_ = getKeyword("https://json-schema.org/keyword/else");
if (else_) {
else_.collectExternalIds = Validation.collectExternalIds;
}
const items = getKeyword("https://json-schema.org/keyword/items");
if (items) {
items.collectExternalIds = Validation.collectExternalIds;
}
const not = getKeyword("https://json-schema.org/keyword/not");
if (not) {
not.collectExternalIds = Validation.collectExternalIds;
}
const oneOf = getKeyword("https://json-schema.org/keyword/oneOf");
if (oneOf) {
oneOf.collectExternalIds = collectExternalIdsFromArrayOfSchemas;
}
const patternProperties = getKeyword("https://json-schema.org/keyword/patternProperties");
if (patternProperties) {
patternProperties.collectExternalIds = collectExternalIdsFromObjectOfSchemas;
}
const prefixItems = getKeyword("https://json-schema.org/keyword/prefixItems");
if (prefixItems) {
prefixItems.collectExternalIds = collectExternalIdsFromArrayOfSchemas;
}
const properties = getKeyword("https://json-schema.org/keyword/properties");
if (properties) {
properties.collectExternalIds = collectExternalIdsFromObjectOfSchemas;
}
const propertyNames = getKeyword("https://json-schema.org/keyword/propertyNames");
if (propertyNames) {
propertyNames.collectExternalIds = Validation.collectExternalIds;
}
const ref = getKeyword("https://json-schema.org/keyword/ref");
if (ref) {
ref.collectExternalIds = Validation.collectExternalIds;
}
const unevaluatedItems = getKeyword("https://json-schema.org/keyword/unevaluatedItems");
if (unevaluatedItems) {
unevaluatedItems.collectExternalIds = Validation.collectExternalIds;
}
const unevaluatedProperties = getKeyword("https://json-schema.org/keyword/unevaluatedProperties");
if (unevaluatedProperties) {
unevaluatedProperties.collectExternalIds = Validation.collectExternalIds;
}
// Draft-04
const additionalItems4 = getKeyword("https://json-schema.org/keyword/draft-04/additionalItems");
if (additionalItems4) {
additionalItems4.collectExternalIds = Validation.collectExternalIds;
}
const dependencies = getKeyword("https://json-schema.org/keyword/draft-04/dependencies");
if (dependencies) {
dependencies.collectExternalIds = (visited, parentSchema, schema) => pipe(
Browser.values(schema),
asyncFilter((subSchema) => Browser.typeOf(subSchema) === "object"),
asyncMap(Validation.collectExternalIds(visited, parentSchema)),
asyncFlatten,
asyncCollectSet
);
}
const items4 = getKeyword("https://json-schema.org/keyword/draft-04/items");
if (items4) {
items4.collectExternalIds = (visited, parentSchema, schema) => Browser.typeOf(schema) === "array"
? collectExternalIdsFromArrayOfSchemas(visited, parentSchema, schema)
: Validation.collectExternalIds(visited, parentSchema, schema);
}
// Draft-06
const contains6 = getKeyword("https://json-schema.org/keyword/draft-06/contains");
if (contains6) {
contains6.collectExternalIds = Validation.collectExternalIds;
}
// Draft-2019-09
const contains19 = getKeyword("https://json-schema.org/keyword/draft-2019-09/contains");
if (contains19) {
contains19.collectExternalIds = Validation.collectExternalIds;
}
// Extensions
const propertyDependencies = getKeyword("https://json-schema.org/keyword/propertyDependencies");
if (propertyDependencies) {
propertyDependencies.collectExternalIds = (visited, parentSchema, schema) => pipe(
Browser.values(schema),
asyncMap((mapping) => Browser.values(mapping)),
asyncFlatten,
asyncMap(Validation.collectExternalIds(visited, parentSchema)),
asyncFlatten,
asyncCollectSet
);
}
const conditional = getKeyword("https://json-schema.org/keyword/conditional");
if (conditional) {
conditional.collectExternalIds = collectExternalIdsFromArrayOfSchemas;
}
const itemPattern = getKeyword("https://json-schema.org/keyword/itemPattern");
if (itemPattern) {
itemPattern.collectExternalIds = (visited, parentSchema, schema) => pipe(
Browser.iter(schema),
asyncFilter((item) => Browser.typeOf(item) === "object"),
asyncMap(Validation.collectExternalIds(visited, parentSchema)),
asyncFlatten,
asyncCollectSet
);
}
};