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.

230 lines (229 loc) 9.51 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Walker = void 0; const clone_1 = __importDefault(require("clone")); const json_schema_ref_parser_1 = __importDefault(require("@apidevtools/json-schema-ref-parser")); const visited = Symbol("visited"); const NEXT_SCHEMA_KEYWORD = Symbol("NEXT_SCHEMA_KEYWORD"); const NEXT_LDO_KEYWORD = Symbol("NEXT_LDO_KEYWORD"); /** * 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) { delete input.$ref; return { ...definition, ...input }; } } } } return input; }; class Walker { constructor() { this.loadSchema = async (schema, options) => { const { cloneSchema = true, dereference = false, dereferenceOptions } = options || {}; this.rootSchema = cloneSchema ? (0, clone_1.default)(schema) : schema; if (dereference) { const parser = new json_schema_ref_parser_1.default(); this.rootSchema = (await parser.dereference(handleRootReference(schema), dereferenceOptions || {})); } }; this.walk = async (processor, vocabulary) => { this.vocabulary = vocabulary ?? this.vocabularies.DRAFT_07; this.walker = processor; this.walker(this.rootSchema); await this.subschemaWalk(this.rootSchema); // clean up the symbols we injected to check for circular references this.cleanupVisited(this.rootSchema); }; this.cleanupVisited = (schema) => { for (const entry of Object.values(schema)) { if (entry && typeof entry === "object" && entry[visited]) { delete entry[visited]; this.cleanupVisited(entry); } } }; this.isValidSubSchema = (schema) => (schema instanceof Object && !Array.isArray(schema)) || typeof schema === "boolean"; this.applyUserProcessor = (schema, key) => { const schemaElement = schema[key]; if (typeof schemaElement !== "object") { return; } schemaElement[visited] = true; this.walker(schemaElement); this.subschemaWalk(schemaElement); }; this.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 this.processSchemaKey = (schema, keyword) => { if (!schema[keyword] || typeof schema[keyword] !== "object") { return; } const processorFunction = this.vocabulary[keyword]; if (!processorFunction) { return; } schema[keyword][visited] = true; processorFunction(schema, keyword); }; this.processObjectOfSchemas = (schema, keyword) => { for (const prop of Object.getOwnPropertyNames(schema[keyword])) { const schemaElem = schema[keyword][prop]; if (typeof schemaElem === "object" && schemaElem) { this.applyUserProcessor(schema[keyword], prop); } } }; this.processArrayOfSchemas = (schema, keyword) => { for (const prop of Object.getOwnPropertyNames(schema[keyword])) { const schemaElem = schema[keyword][prop]; if (schemaElem && typeof schemaElem === "object") { this.applyUserProcessor(schema[keyword], prop); } } for (let i = 0; i < schema[keyword].length; i++) { this.applyUserProcessor(schema[keyword], i); } }; this.processSingleOrArrayOfSchemas = (schema, keyword) => { if (this.isValidSubSchema(schema[keyword])) { this.processSingleSchema(schema, keyword); } else { this.processArrayOfSchemas(schema, keyword); } }; this.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. */ this.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 this.initVocabulary = () => { const DRAFT_04 = { properties: this.processObjectOfSchemas, patternProperties: 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.processObjectOfSchemas, }; 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, }; }; this.initVocabulary(); } } exports.Walker = Walker;