UNPKG

win-schema

Version:

WIN module that handles all things schema related! Schema validation, schema loading, schema remote loads, etc etc etc

654 lines (530 loc) 22.5 kB
//pull in traverse object for this guy var traverse = require('optimuslime-traverse'); var schemaSpec = require('./schemaSpec'); module.exports = extendAddSchema; function extendAddSchema(self) { var pathDelim = self.pathDelimiter; var defaultWINAdd = { wid : "string", dbType : "string", parents : { type: "array", items : { type : "string" } } } var winTypeRegExp = []; for(var key in defaultWINAdd) { winTypeRegExp.push(key); } self.log("--All WIN keywords: ", winTypeRegExp); winTypeRegExp = new RegExp("\\b" + winTypeRegExp.join("\\b|\\b") + "\\b"); //everything we need to do to add a schema inside //this requires checking if it's properly formatted, pulling references, and moving //around things if it's not formatted but we would like to make it less wordy to make schema self.internalAddSchema = function(type, schemaJSON, options, finished) { if(typeof options == "function") { finished = options; options = {}; } else options = options || {}; if((schemaJSON.type == "array" || schemaJSON.items) && !options.skipWINAdditions) { finished("Array-types for schema cannot have WIN additions. It doesn't make any sense. The object must be an array, but also have a wid property? Failed: " + type); return; } //make a clone of the object schemaJSON = JSON.parse(JSON.stringify(schemaJSON)); //force all types to lower case -- always -- deal with weird validation errors otherwise traverse(schemaJSON).forEach(function(node) { if(this.key == "type" && typeof this.node == "string") this.update(this.node.toLowerCase()); }) //we add or move objects inside the schema to make it conform to expected v4 JSON schema validation appendSchemaInformation(schemaJSON, options); //check our schema for wacky errors! var schemaCheck = checkSchemaErrors(schemaJSON); if(schemaCheck && schemaCheck.errors) { finished("Improper schema format for " + type + " - " + JSON.stringify(schemaCheck)); return; } if(schemaCheck && schemaCheck.warnings) { self.log("Warnings: ".yellow, schemaCheck.warnings); } //save it in our map self.allSchema[type] = schemaJSON; if(!schemaJSON.id || schemaJSON.id != type) schemaJSON.id = type; if(!schemaJSON['$schema']) schemaJSON['$schema'] = "http://json-schema.org/draft-04/schema#"; if(!schemaJSON.type) schemaJSON.type = "object"; //add the schema to our validator -- this does most heavy lifting for us self.validator.addSchema(schemaJSON); //failed to add schema for some reason? if(self.validator.error){ finished(self.validator.error); } else { //no error from validator, store the references inside storeSchemaReferences(type, schemaJSON); //when we create it setSchemaProperties(type, schemaJSON, options); //take what you want, and give nothing back! The pirates way for us! finished(); } } function setSchemaProperties(type, schemaJSON, options) { var props = {}; if(options.skipWINAdditions) props.isWIN = false; else props.isWIN = true; var primePaths = {}; var tJSON = traverse(schemaJSON); var references = self.requiredReferences[type]; var refMap = {}; for(var refType in references) { var locations = references[refType]; for(var l =0; l < locations.length; l++) { var refInfo = locations[l]; refMap[refInfo.typePath] = refInfo; } } // self.log("Refmap: ", refMap); function isRef(path){ return refMap[path.join(pathDelim)]} tJSON.forEach(function(node) { if(this.isRoot || this.isLeaf) return; //kill the future investigation of references if(isRef(this.path)) this.keys = []; //if we are a known keyword -- that's not properties or items, we skip you! if(this.key != "properties" && this.key != "items" && self.keywordRegExp.test(this.key)) this.keys = []; //we also ignore this as well if(winTypeRegExp.test(this.key)) this.keys = []; // self.log("Isref?".green, isRef(this.path)); // if(this.keys.length) // self.log("Potential PrimePath: ".green, this.key, " node: ", this.node); if(this.keys.length){ var objPath = self.stripObjectPath(this.path); //we're an array, or we're inisde an array! if(this.node.type == "array" || this.node.items || this.key =="items") { //we are an array, we'll pull the array info -- and then we close off this array -- forever! //remember, primary paths are all about the objects, and the FIRST layer of array primePaths[objPath] = {type: "array"}; this.keys = []; } else { //you must be a properties object //either you have a type, or you're an object primePaths[objPath] = {type: this.node.type || "object"}; } } }) // self.log("\n\tprimaryPaths: ".cyan, primePaths); self.primaryPaths[type] = primePaths; self.typeProperties[type] = props; } function hasNonKeywords(obj) { var hasNonKeywords = false; if(Array.isArray(obj)) { //loop through object to grab keys for(var i=0; i < obj.length; i++) { var iKey = obj[i]; //check if you're not a keyword if(!self.keywordRegExp.test(iKey)) { //just one is enough hasNonKeywords = true; break; } } } else { for(var iKey in obj) { if(!self.keywordRegExp.test(iKey)) { //just one is enough hasNonKeywords = true; break; } } } return hasNonKeywords; } //handle everything associated with adding a schema function checkSchemaErrors(schemaJSON) { //check against the proper schema definition // var vck = self.validator.validateMultiple(schemaJSON, schemaSpec, true); var valCheck = self.validateFunction.apply(self.validator, [schemaJSON, schemaSpec, true]); //grab all possible errors var checkErrors = {length: 0}; var checkWarnings = {length: 0}; //if we're valid -- which we almost certainly are -- just keep going if(!valCheck.valid) { //let it be known -- this is a weird error self.log("Invalid from v4 JSON schema perspective: ", valCheck[errorKey]); checkErrors["root"] = valCheck[errorKey]; checkErrors.length++; //not valid, throw it back return checkErrors; } //make sure we have some properties -- otherwise there is literally no validation/ //during the move process, this is overridden, but it's a good check nonetheless if(!schemaJSON.properties && !schemaJSON.items) { checkErrors["root"] = "No properties/items defined at root. Schema has no validation without properties!"; checkErrors.length++; } //going to need to traverse our schema object var tJSON = traverse(schemaJSON); tJSON.forEach(function(node) { //skip the root please if(this.isRoot || this.path.join(pathDelim).indexOf('required') != -1) return; //this should be a warning if(!self.requireByDefault && !this.isLeaf && !this.node.required) { //if you don't have a required object, then you're gonna have a bad time //this is a warning checkWarnings[this.path.join(pathDelim)] = "warning: if you disable requireByDefault and don't put require arrays, validation will ignore those properties."; checkWarnings.length++; } if(this.key == "properties" && this.node.properties) { checkErrors[this.path.join(pathDelim)] = "Properties inside properties is meaningless."; checkErrors.length++; } if(this.key == "type" && typeof this.node != "string") { //for whatever reason, there is a type defined, but not a string in it's place? Waa? checkErrors[this.path.join(pathDelim)] = "Types must be string"; checkErrors.length++; } if(this.key == "type" && !self.typeRegExp.test(this.node.toLowerCase())) { checkErrors[this.path.join(pathDelim)] = "Types must be one of " + self.validTypes + " not " + this.node; checkErrors.length++; } if(this.isLeaf) { //if you don't have a type, and there is no ref object if(!this.parent.node.properties && (this.key != "type" && this.key != "$ref") && !this.parent.node.type && !this.parent.node["$ref"]) { checkErrors[this.path.join(pathDelim)] = "Object doesn't have any properties, a valid type, or a reference, therefore it is invalid in the WIN spec."; checkErrors.length++; } } //not a leaf, you don't have a reference if(!self.allowAnyObjects && !this.isLeaf && !this.node["$ref"] ) { //special case for items -- doesn't apply if(this.node.type == "object" && this.key != "items") { //we're going to check if the list of keys to follow have any non keywords //for instance if {type: "object", otherThing: "string"} keys = type, otherThing //if instead it's just {type : "object", required : []}, keys = type, required //notice that the top has non-keyword keys, and the bottom example does not //we're looking for the bottom example and rejecting it var bHasNonKeywords = hasNonKeywords(this.keys); //if you ONLY have keywords -- you don't have any other object types //you are a violation of win spec and you allow any object or array to be passed in if(!bHasNonKeywords){ // self.log("Current: ".magenta, this.key, " Keys: ".cyan, this.keys || "none, node: " + this.node, " has non? ".red + bHasNonKeywords); checkErrors[this.path.join(pathDelim)] = "AllowAnyObjects is off, therefore you cannot simple have an 'object' type with no inner properties"; checkErrors.length++; } } else if(this.node.type == "array") { //if you are an array and you have no items -- not allowed! if(!this.node.items){ // self.log("Current: ".magenta, this.key, " Keys: ".cyan, this.keys || "none, node: " + this.node, " has non? ".red + bHasNonKeywords); checkErrors[this.path.join(pathDelim)] = "AllowAnyObjects is off, therefore you cannot simple have an 'array' type with no inner items"; checkErrors.length++; } else { //if you have a ref -- you're okay for us! var bIemsHaveNonKey = this.node.items["$ref"] || this.node.items["type"] || hasNonKeywords(this.node.items.properties || {}); if(!bIemsHaveNonKey){ // self.log("Current: ".magenta, this.key, " Keys: ".cyan, this.keys || "none, node: " + this.node, " has non? ".red + bHasNonKeywords); checkErrors[this.path.join(pathDelim)] = "AllowAnyObjects is off, therefore you cannot simple have an 'array' type with no non-keyword inner items"; checkErrors.length++; } } } } //if you're an array if(this.node.type == "array") { //grab your items var items = this.node.items; if(!items && !self.allowAnyObjects) { checkErrors[this.path.join(pathDelim)] = "AllowAnyObjects is off for arrays, therefore you cannot simple have an 'array' type with no inner items"; checkErrors.length++; } else { items = items || {}; //we have items -- we shouldn't have a reference type && other items if(items.properties && items["$ref"]) { checkErrors[this.path.join(pathDelim)] = "Array items in WIN cannot have properties AND a reference type. One or the other."; checkErrors.length++; } } } }); if(checkErrors.length || checkWarnings.length) return {errors: checkErrors, warnings: checkWarnings}; else return null; } self.stripObjectPath = function(path) { //obj path will be returned var objectPath = []; //travere this path, yo traverse(path).forEach(function() { //no routes including properties or items -- made up schema info! if(!this.isRoot && (this.node != "properties" && this.node != "items")) objectPath.push(this.node); }); return objectPath.join(pathDelim); } //storing the references inside of a schema object (if we don't already know them) function parseSchemaReferences(schemaJSON) { //first we wrap our object with traverse methods var tJSON = traverse(schemaJSON); var references = {}; self.log('-- Parsing refs -- '); // self.log(schemaJSON); //now we step through pulling the path whenever we hit a reference tJSON.forEach(function(node) { //we are at a reference point //we make an exception for arrays -- since the items object can hold references! if(this.node["$ref"] && (this.key == "items" || !self.keywordRegExp.test(this.key))) // if(this.isLeaf && this.key == "$ref") { //todo logic for when it's "oneOf" or other valid JSON schema things var fullPath = this.path.join(pathDelim);//this.path.slice(0, this.path.length-1).join(pathDelim); var referenceType = this.node["$ref"]; var objectPath = self.stripObjectPath(this.path); //pull the "items" piece out of the path -- otherwise, if you're just a normal object -- it's the same as fullPath var typePath = this.key == "items" ? this.path.slice(0, this.path.length-1).join(pathDelim) : fullPath; if(references[fullPath]) { throw new Error("Not yet supported reference behavior, arrays of references: ", fullPath); } //assuming type is defined here! references[fullPath] = {schemaType: referenceType, schemaPath: fullPath, objectPath: objectPath, typePath: typePath}; self.log(self.log.testing, 'Reference detected @ '+fullPath+': ', references[fullPath]); } }); self.log("-- Full refs -- ", references); return references; } function storeSchemaReferences(type, schemaJSON) { self.schemaReferences[type] = parseSchemaReferences(schemaJSON); self.requiredReferences[type] = {}; for(var path in self.schemaReferences[type]) { var schemaInfo = self.schemaReferences[type][path]; var refType = schemaInfo.schemaType; var aReqRefs = self.requiredReferences[type][refType]; if(!aReqRefs) { aReqRefs = []; self.requiredReferences[type][refType] = aReqRefs; } //value is the reference type aReqRefs.push(schemaInfo); } //now we know all the references, their paths, and what type needs what references } function moveAllToProperties(tJSON) { tJSON.forEach(function(node) { // self.log("Investigating: ", this.key, " @ ", this.path.join(pathDelim), " all keys: ", this.keys); //for all non-arrays and non-leafs and non-properties object -- move to a properties object if not a keyword! if(!this.isLeaf && this.key != "properties" && !Array.isArray(this.node)) { //movement dpeends on what type you are -- arrays move to items, while objects move to properties var moveLocation = "properties"; if(this.node.type == "array") moveLocation = "items"; // self.log('Movement: ', this.key, " @ ", this.path.join(pathDelim) + " : ", this.node); // self.log("Move to : ".green + moveLocation); // self.log("Move innitiated: ".magenta, this.node); // self.log('Original node: '.green, node); var empty = true; var move = {}; //any key that isn't one of our keywords is getting moved inside! for(var key in this.node){ if(!self.keywordRegExp.test(key)){ // self.log('Moving key @ ', this.path.join(pathDelim) || "Is root? ", " : ", this.key || this.isRoot); move[key] = this.node[key]; empty = false; } } //don't move nothing derrr if(!empty) { // self.log('Moving: '.red, move); //create proeprties if it doesn't exist node[moveLocation] = node[moveLocation] || {}; for(var key in move) { //move to its new home node[moveLocation][key] = move[key]; //remove from previous location delete node[key]; } //make sure to update, thank you this.update(node); //we need to investigate the newly created properties/items object -- to continue down the rabbit hole this.keys.push(moveLocation); } } }); } function addWINTypes(schemaJSON, options) { for(var key in defaultWINAdd) { var winAdd = defaultWINAdd[key]; //if it's just a shallow string -- add it directly if(typeof winAdd == "string") schemaJSON[key] = winAdd; else //otehrwise, we should clone the larger object schemaJSON[key] = traverse(defaultWINAdd[key]).clone(); } } function appendSchemaInformation(schemaJSON, options) { //add in default win types if(!options.skipWINAdditions) addWINTypes(schemaJSON, options); //build a traverse object for navigating and updating the object var tJSON = traverse(schemaJSON); //step one convert string to types tJSON.forEach(function(node) { var needsUpdate = false; //if you are a leaf -- and you only dictate the type e.g. string/number/array etc -- we'll convert you to proper type if(this.isLeaf && typeof this.node == "string") { //if the key is not a known keyword, and the node string is a proper type if(!self.keywordRegExp.test(this.key) && self.typeRegExp.test(node.toLowerCase())) { //node is a type! make sure it's a lower case type being stored. node = {type: node.toLowerCase()}; needsUpdate = true; } } if(this.node) { if(this.node.items && !this.node.type) { this.node.type = "array"; needsUpdate = true; } //rewrite {type : "array", "$ref" : "something"} => {type : "array", items : {"$ref" : "something"}} if(this.node.type == "array" && this.node["$ref"]) { this.node.items = this.node.items || {}; this.node.items["$ref"] = this.node["$ref"]; delete this.node["$ref"]; needsUpdate = true; } } if(needsUpdate) this.update(node); }) //update location of objects to match validation issues //json schema won't validate outside of properties object -- which some people may forget //this is basically a correct method moveAllToProperties(tJSON); // var util = require('util'); // self.log("Post move schema: ".cyan, util.inspect(schemaJSON, false, 10)); tJSON.forEach(function(node) { var needsUpdate = false; //if we aren't a leaf object, we are a full object //therefore, we must have required (since we're in default mode) //since we cover the properties object inside, we don't need to go indepth for that key too! if(self.requireByDefault && !this.isLeaf && !this.node.required && !Array.isArray(this.node)) { //the require needs to be filled iwth all the properties of this thing, except //for anything defined by v4 json schema -- so we run a regex to reject those keys var reqProps = []; // self.log("Not leaf: ".magenta, this.node, " Key : ", this.key); //do not do this if you're in the properties object //since the prop keys belong to the PARENT not the node if(this.key != "properties") { for(var key in this.node){ if(!self.keywordRegExp.test(key)){ // self.log('Key added: '.red, key); reqProps.push(key); } } // self.log('Post not props: '.blue, reqProps); } //for every object, you can also have a properties object too //required applies to the subling property object as well //so we loop through the properties object as well for(var key in this.node.properties){ if(!self.keywordRegExp.test(key)){ reqProps.push(key); } } if(reqProps.length) { node.required = reqProps; needsUpdate = true; } } if(needsUpdate){ // self.log('New required - : ', this.node, ' : ', reqProps); this.update(node); } }); // self.log("--post traverse -- ", schemaJSON); } return self; }