UNPKG

json-schema-walker

Version:

A system that visits all schema objects in a JSON Schema document and makes callbacks before visiting all of the current schema object's subschemas.

243 lines (242 loc) 9.06 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Walker = void 0; const json_schema_ref_parser_1 = require("@apidevtools/json-schema-ref-parser"); const NEXT_SCHEMA_KEYWORD = Symbol("NEXT_SCHEMA_KEYWORD"); const NEXT_LDO_KEYWORD = Symbol("NEXT_LDO_KEYWORD"); const cloneSchemaValue = (value) => { const structuredCloneFn = globalThis .structuredClone; if (structuredCloneFn) { return structuredCloneFn(value); } return JSON.parse(JSON.stringify(value)); }; /** * This is a hotfix and really only a partial solution as it does not cover all cases. * * But it's the best we can do until we find or build a better library to handle references. * * original source https://github.com/asyncapi/modelina/pull/829/files */ const handleRootReference = (input) => { //Because of https://github.com/APIDevTools/json-schema-ref-parser/issues/201 the tool cannot handle root references. //This really is a bad patch to fix an underlying problem, but until a full library is available, this is best we can do. const hasRootRef = input.$ref !== undefined; if (hasRootRef) { //When we encounter it, manually try to resolve the reference in the definitions section const hasDefinitionSection = input.definitions !== undefined; if (hasDefinitionSection) { const definitionLink = "#/definitions/"; const referenceLink = input.$ref.slice(0, definitionLink.length); const referenceIsLocal = referenceLink === definitionLink; if (referenceIsLocal) { const definitionName = input.$ref.slice(definitionLink.length); const definition = input.definitions[String(definitionName)]; const definitionExist = definition !== undefined; if (definitionExist) { return { ...definition, definitions: input.definitions }; } } } } return input; }; class Walker { rootSchema; vocabulary; vocabularies; walker; visitedSchemas = new WeakSet(); constructor() { this.initVocabulary(); } loadSchema = async (schema, options) => { const { cloneSchema = true, dereference = false, dereferenceOptions } = options || {}; this.rootSchema = cloneSchema ? cloneSchemaValue(schema) : schema; if (dereference) { const parser = new json_schema_ref_parser_1.$RefParser(); this.rootSchema = (await parser.dereference(handleRootReference(this.rootSchema), dereferenceOptions || {})); } }; loadSchemaSync = (schema, options) => { const { cloneSchema = true } = options || {}; this.rootSchema = cloneSchema ? cloneSchemaValue(schema) : schema; }; walk = async (processor, vocabulary) => { this.vocabulary = vocabulary ?? this.vocabularies.DRAFT_07; this.walker = processor; this.visitedSchemas = new WeakSet(); if (this.rootSchema && typeof this.rootSchema === "object") { this.visitedSchemas.add(this.rootSchema); } this.walker(this.rootSchema); await this.subschemaWalk(this.rootSchema); }; walkSync = (processor, vocabulary) => { this.vocabulary = vocabulary ?? this.vocabularies.DRAFT_07; this.walker = processor; this.visitedSchemas = new WeakSet(); if (this.rootSchema && typeof this.rootSchema === "object") { this.visitedSchemas.add(this.rootSchema); } this.walker(this.rootSchema); this.subschemaWalk(this.rootSchema); }; isValidSubSchema = (schema) => (schema instanceof Object && !Array.isArray(schema)) || typeof schema === "boolean"; applyUserProcessor = (schema, key) => { const schemaElement = schema[key]; if (typeof schemaElement !== "object") { return; } if (this.visitedSchemas.has(schemaElement)) { return; } this.visitedSchemas.add(schemaElement); this.walker(schemaElement); this.subschemaWalk(schemaElement); }; subschemaWalk = (schema) => { for (const keyword in schema) { try { this.processSchemaKey(schema, keyword); } catch (e) { if (e !== NEXT_SCHEMA_KEYWORD) { throw e; } } } }; // These are the processors processSchemaKey = (schema, keyword) => { if (!schema[keyword] || typeof schema[keyword] !== "object") { return; } const processorFunction = this.vocabulary[keyword]; if (!processorFunction) { return; } processorFunction(schema, keyword); }; processObjectOfSchemas = (schema, keyword) => { for (const prop of Object.getOwnPropertyNames(schema[keyword])) { const schemaElem = schema[keyword][prop]; if (this.isValidSubSchema(schemaElem) && !Array.isArray(schemaElem)) { this.applyUserProcessor(schema[keyword], prop); } } }; processArrayOfSchemas = (schema, keyword) => { for (let i = 0; i < schema[keyword].length; i++) { this.applyUserProcessor(schema[keyword], i); } }; processSingleOrArrayOfSchemas = (schema, keyword) => { if (this.isValidSubSchema(schema[keyword])) { this.processSingleSchema(schema, keyword); } else { this.processArrayOfSchemas(schema, keyword); } }; processSingleSchema = (schema, keyword) => { this.applyUserProcessor(schema, keyword); }; /** * Loop over the links and apply the callbacks, while * handling LDO keyword deletions by catching NEXT_LDO_KEYWORD. */ getProcessLinks = (ldoVocabulary) => { return (schema, keyword) => { for (const ldo of schema.links) { for (const key in ldo) { try { ldoVocabulary[keyword]?.(schema, key); } catch (e) { if (e !== NEXT_LDO_KEYWORD) { throw e; } } } } }; }; // vocabulary initialization initVocabulary = () => { const DRAFT_04 = { properties: this.processObjectOfSchemas, patternProperties: this.processObjectOfSchemas, definitions: this.processObjectOfSchemas, additionalProperties: this.processSingleSchema, dependencies: this.processObjectOfSchemas, items: this.processSingleOrArrayOfSchemas, additionalItems: this.processSingleSchema, allOf: this.processArrayOfSchemas, anyOf: this.processArrayOfSchemas, oneOf: this.processArrayOfSchemas, not: this.processSingleSchema, if: this.processSingleSchema, then: this.processSingleSchema, else: this.processSingleSchema, }; /** * LDO keywords call _apply directly as they have a different * mapping from the schema keyword into the path that _apply * expects. This is done in the function returned from * _getProcessLinks(); */ const DRAFT_04_HYPER_LDO = { schema: this.applyUserProcessor, targetSchema: this.applyUserProcessor, }; const DRAFT_04_HYPER = { ...DRAFT_04, links: this.getProcessLinks(DRAFT_04_HYPER_LDO), }; const DRAFT_06 = { ...DRAFT_04, propertyNames: this.processSingleSchema, contains: this.processSingleSchema, }; const DRAFT_06_HYPER_LDO = { hrefSchema: this.applyUserProcessor, targetSchema: this.applyUserProcessor, submissionSchema: this.applyUserProcessor, }; const DRAFT_06_HYPER = { ...DRAFT_06, links: this.getProcessLinks(DRAFT_06_HYPER_LDO), }; const DRAFT_07 = { ...DRAFT_06 }; const DRAFT_07_HYPER_LDO = { ...DRAFT_06_HYPER_LDO, headerSchema: this.applyUserProcessor, }; const DRAFT_07_HYPER = { ...DRAFT_07, links: this.getProcessLinks(DRAFT_07_HYPER_LDO), }; const CLOUDFLARE_DOCA = { ...DRAFT_04, links: this.getProcessLinks({ ...DRAFT_04_HYPER_LDO, ...DRAFT_07_HYPER_LDO, }), }; this.vocabularies = { DRAFT_04, DRAFT_04_HYPER, DRAFT_04_HYPER_LDO, DRAFT_06, DRAFT_06_HYPER, DRAFT_06_HYPER_LDO, DRAFT_07, DRAFT_07_HYPER, DRAFT_07_HYPER_LDO, CLOUDFLARE_DOCA, }; }; } exports.Walker = Walker;