UNPKG

@shinymayhem/json-schema-generator

Version:

Generate/compile .yaml files (not .yml at the moment) into a single JSON schema file. Resolves references to other schema docuements in the same directory

196 lines (182 loc) 6.35 kB
/* eslint-env node */ /* eslint no-sync: 0 */ "use strict"; // Include external dependencies var fs = require('fs'); var path = require('path'); var yaml = require('js-yaml'); var clone = require('clone'); // Include local modules // Setup /** * Generate a schema from a .yaml file * * References are resolved using the component before the '#' in a '$ref' * property, using that component as the file name, searching the directory of * the .yaml file generating the schema. Subschemas definitions are compiled * into the top level schema, preventing circular references and nested and * duplicated schemas */ class Generator { /** * Constructor * * @param {object} schema JSON schema object * @param {object|null} topLevelSchema Schema to compile references into * @param {string} subpath Path to current schema from top level * @param {string} schemaDir Directory to find schema files in */ constructor(schema, topLevelSchema, subpath, schemaDir) { this.schemaDir = schemaDir; // Keep a list of references and their replacements this.references = {}; // Clone it so the original object isn't modified (useful if used elsewhere) this.compiled = clone(schema); // If no top level schema specified, this is the top level this.topLevelSchema = this.compiled; if (topLevelSchema) { this.topLevelSchema = topLevelSchema; } this.references[schema.shortName] = '#'; if (subpath) { this.references[schema.shortName] += subpath; } // preserve uncompiled schema? this.original = schema; } resolveReferences(callback) { recursiveMap(this.compiled, [], (stack, key, value) => { // 'this' bound to most recently called context. arrow function // preserves it for when called outside the class this.compile(stack, key, value); }); //log({compiled: this.compiled}); callback(null, this.compiled); } /** * Resolve references to external schemas and compile them into the top * level schema * * @param {array} stack * @param {string} key * @param {mixed} value */ compile(stack, key, value) { if (key === "$ref") { let reference = parseReference(value); if (reference.object === '#') { // references self. don't allow this, force absolute references let e = new Error("Found reference to self at '" + stack.join(".") + "'. Use object id prefix for all references"); e.detail = reference; throw e; } else if (this.references[reference.object]) { // Reference is already compiled into main schema // Replace reference to external schema with one to local definition this.replaceReference(stack, reference); } else { // Fetch reference and compile into main schema let data = fs.readFileSync(path.join(this.schemaDir, reference.object + ".yaml")); let schema = yaml.safeLoad(data); //this.compiled.definitions[reference.object] = schema; //TODO replace with compiled schema let generator = new Generator(schema, this.topLevelSchema, "/definitions/" + schema.shortName, this.schemaDir); generator.resolveReferences((err, compiled) => { // Add compiled schema to definitions of top level schema if (!this.topLevelSchema.definitions) { this.topLevelSchema.definitions = {}; } // Remove subschema's id, as it is now part of the compiled schema delete compiled.id; this.topLevelSchema.definitions[reference.object] = compiled; // Store new path in list of resolved references this.references[schema.shortName] = "#/definitions/" + schema.shortName; // Replace reference to external schema with one to local definition this.replaceReference(stack, reference); }); } } } /** * Replace string reference to external schema with one pointing to local definition * * @param {string[]} stack Properties path to reference * @param {object} reference Parsed reference */ replaceReference(stack, reference) { let updatable = this.compiled; for (let i = 0; i < stack.length; i++) { updatable = updatable[stack[i]]; } //updatable.$ref = "#/definitions/" + reference.object; updatable.$ref = this.references[reference.object]; if (reference.path) { updatable.$ref += reference.path; } } } /** * Send each key/value pair to a function * * @param {object} object Object to recurse * @param {array} stack Path to current node in object * @param {function} fn Function to send key/values to */ function recursiveMap(object, stack, fn) { for (let i in object) { if (object.hasOwnProperty(i)) { if (typeof object[i] === "object") { let newStack = clone(stack); newStack.push(i); recursiveMap(object[i], newStack, fn); } else { fn(stack, i, object[i]); } } } } /** * Parse a reference into component parts and properties * * @param {string} value * * @returns {object} */ function parseReference(value) { var parsed = { original: value }; var parts = value.split('#'); parsed.parts = parts; if (parts[0] === "" && parts.length === 2) { // Reference self parsed.object = "#"; parsed.path = parts[1]; } else if (parts.length === 1) { // Reference external top level object parsed.object = parts[0]; } else if (parts.length === 2) { // Reference external object's sub-object parsed.object = parts[0]; parsed.path = parts[1]; } else { // More than one '#' found. This shouldn't happen if yaml files are valid throw new Error("More than one '#' found in reference value: " + value); } return parsed; } //function log() { //console.dir.call(null, ...arguments, {depth: null}); //} // Public module.exports = { generate: function generate(fileName, callback) { fs.readFile(fileName, function onFileRead(err, data) { if (err) { return callback(err); } var schema = yaml.safeLoad(data); var schemaDir = path.dirname(fileName); var generator = new Generator(schema, null, null, schemaDir); return generator.resolveReferences(callback); }); } };