Last active
June 25, 2016 02:40
-
-
Save deansheather/4bccb070c121dfc009e1f1c65ef5bdd9 to your computer and use it in GitHub Desktop.
Schema validator for Javascript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
/** | |
* Output a string representation of the type of an object. | |
* | |
* @param {*} a - Input object. | |
* @return {string} - Correct input object type. | |
*/ | |
var typeTest = function (a) { | |
switch (a) { | |
case String: | |
return 'string' | |
case Number: | |
return 'number' | |
case Array: | |
return 'array' | |
case Object: | |
return 'object' | |
case Boolean: | |
return 'boolean' | |
case RegExp: | |
return 'regex' | |
case null: | |
return 'null' | |
case undefined: | |
return 'undefined' | |
default: | |
if (Array.isArray(a)) { | |
return 'array' | |
} else if (a instanceof RegExp) { | |
return 'regex' | |
} else { | |
return typeof a | |
} | |
} | |
} | |
/** | |
* Compare the type of two objects. | |
* | |
* @param {*} type - Type to compare object to. | |
* @param {*} test - Object to test. | |
* | |
* @return {boolean} Success or failure status of test. | |
*/ | |
var compareTypes = function (type, test) { | |
return typeTest(type) === typeTest(test) | |
} | |
/** | |
* Internal keys for each level in the schema definition train. | |
*/ | |
var requiredKeys = [ | |
'type', | |
'required' | |
] | |
var internalKeys = [ | |
'type', | |
'required', | |
'test', | |
'length' | |
] | |
/** | |
* Schema definition forced type "tests" for some schema-related keys in the schema tree. | |
*/ | |
var typeTests = { | |
type: [String, Number, Boolean, RegExp, Object, Array], | |
required: [Boolean], | |
test: [RegExp] | |
} | |
/** | |
* Schema class. Used to create Schema objects and test objects against them. | |
*/ | |
class Schema { | |
/** | |
* Class constructor. Creates a new instance of Schema. | |
* | |
* @param {object} schemaDefinition - Definition of the schema to use for this instance of Schema. | |
*/ | |
constructor (schemaDefinition) { | |
this.definition = schemaDefinition | |
if (!this.validateSchema()) { | |
throw new Error('invalid schema definition passed to Schema constructor') | |
} | |
} | |
/** | |
* Validate the schema definition provided in the constructor, and output a | |
* true or false value depending on whether it is a valid schema definition or | |
* not. | |
* | |
* @return {boolean} Whether the schema definition is a valid schema definition or not. | |
*/ | |
validateSchema () { | |
// Not an object | |
if (this.definition === null || typeof this.definition !== 'object') return false | |
var definition = this.definition | |
// Iteration function, takes a tree and iterates | |
var iterate = function (tree) { | |
// Only run these tests if the tree isn't the root of the schema | |
if (tree !== definition) { | |
// Not an object | |
if (tree === null || typeof tree !== 'object') return false | |
// Iterate through required keys | |
for (var i = 0; i < requiredKeys.length; i++) { | |
// Missing a required value in the object | |
if (!tree.hasOwnProperty(requiredKeys[i])) return false | |
} | |
// Test to make sure given internal variable for the parser are of correct type | |
for (var p in typeTests) { | |
if (tree.hasOwnProperty(p)) { | |
var passed = false | |
for (i = 0; i < typeTests[p].length; i++) { | |
if (compareTypes(typeTests[p][i], tree[p])) passed = true | |
} | |
// Didn't pass types test - unsupported variable type | |
if (!passed) return false | |
} | |
} | |
} | |
// Iterate through trees of objects | |
if (tree.type === Object) { | |
for (p in tree) { | |
if (internalKeys.indexOf(p) === -1) { | |
// One of the nested iterations failed... fail the whole function | |
if (!iterate(tree[p])) return false | |
} | |
} | |
} | |
// Object survived previous tests | |
return true | |
} | |
// Return the result of the iterations | |
return iterate(definition) | |
} | |
/** | |
* Checks if the given object matches the schema. | |
* | |
* @param {object} input - Object to test against the schema. | |
* | |
* @return {boolean} Whether or not the object matched the schema. | |
*/ | |
test (input) { | |
// Sanity checks | |
if (!this.validateSchema()) throw new Error('invalid schema definition passed to Schema constructor') | |
if (input === null || typeof input !== 'object') throw new TypeError('input is not an object') | |
// Iteration function, takes a tree and iterates | |
var iterate = function (schemaTree, inputTree) { | |
for (var p in schemaTree) { | |
if (schemaTree[p].required) { | |
// Missing required property | |
if (!inputTree.hasOwnProperty(p)) return false | |
} else if (!inputTree.hasOwnProperty(p)) continue | |
// Compare types | |
if (!compareTypes(schemaTree[p].type, inputTree[p])) return false | |
// Test string regexes | |
if (schemaTree[p].type === String && schemaTree[p].hasOwnProperty('test')) { | |
if (!schemaTree[p].test.test(inputTree[p])) return false | |
} | |
// Test string/array length | |
if ((schemaTree[p].type === String || schemaTree[p].type === Array) && schemaTree[p].hasOwnProperty('length') && (schemaTree[p].length !== null && typeof schemaTree[p].length === 'object')) { | |
if (schemaTree[p].length.hasOwnProperty('min')) { | |
// Too small | |
if (inputTree[p].length < schemaTree[p].length.min) return false | |
} | |
if (schemaTree[p].length.hasOwnProperty('max')) { | |
// Too large | |
if (inputTree[p].length > schemaTree[p].length.max) return false | |
} | |
} | |
// Reiterate if object | |
if ((inputTree[p] !== null && typeof inputTree[p] === 'object') && (schemaTree[p] !== null && schemaTree[p].type === Object)) { | |
if (!iterate(schemaTree[p], inputTree[p])) return false | |
} | |
} | |
for (p in inputTree) { | |
// Hang on, only the input has this key. Not in the schema | |
if (!schemaTree.hasOwnProperty(p)) return false | |
} | |
// Object survived previous tests | |
return true | |
} | |
return iterate(this.definition, input) | |
} | |
} | |
// Expose Schema | |
module.exports = Schema |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
// Required modules | |
const Schema = require('./Schema') | |
// Test schema | |
var songSchema = new Schema(require('./TestSchema')) | |
// Test input | |
var test = { | |
kind: 'DiscordFM#Song', | |
id: '6134674743690788864', | |
metadata: { | |
duration: 234, | |
artist: 'Madeon', | |
title: 'The City', | |
musicbrainzId: '1027696f-2b39-4f9d-8eea-ede67e6ce9e9' | |
}, | |
source { | |
url: 'https://www.youtube.com/watch?v=gEABPD4wNCg', | |
identifier: 'gEABPD4wNCg', | |
service: 'YouTubeVideo' | |
}, | |
audio: { | |
inStorage: false | |
} | |
} | |
// Run test | |
if (songSchema.test(test)) { | |
console.log('Test succeeded.') | |
} else { | |
console.log('Test failed.') | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = { | |
kind: { | |
type: String, | |
required: false, | |
test: /^DiscordFM#Song$/ | |
}, | |
id: { | |
type: String, | |
required: true | |
}, | |
metadata: { | |
type: Object, | |
required: true, | |
musicbrainzId: { | |
type: String, | |
required: false, | |
test: /^([a-fA-F0-9]{8}(:?-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12})$/ | |
}, | |
title: { | |
type: String, | |
required: true, | |
length: { | |
min: 1, | |
max: 50 | |
} | |
}, | |
artist: { | |
type: String, | |
required: true, | |
length: { | |
min: 1, | |
max: 50 | |
} | |
}, | |
duration: { | |
type: Number, | |
required: false | |
}, | |
album: { | |
type: Object, | |
required: false, | |
title: { | |
type: String, | |
required: true, | |
length: { | |
min: 1, | |
max: 50 | |
} | |
}, | |
releaseYear: { | |
type: Number, | |
required: true | |
}, | |
artURL: { | |
type: String, | |
required: false | |
} | |
}, | |
lastUpdated: { | |
type: String, | |
required: false | |
} | |
}, | |
source: { | |
type: Object, | |
required: true, | |
service: { | |
type: String, | |
required: true | |
}, | |
identifier: { | |
type: String, | |
required: true | |
}, | |
url: { | |
type: String, | |
required: false, | |
test: /^(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})$/ | |
} | |
}, | |
audio: { | |
type: Object, | |
required: false, | |
inStorage: { | |
type: Boolean, | |
required: true | |
}, | |
streamEndpoint: { | |
type: String, | |
required: false | |
} | |
}, | |
statistics: { | |
type: Object, | |
required: false, | |
totalPlays: { | |
type: Number, | |
required: true | |
}, | |
totalListens: { | |
type: Number, | |
required: true | |
}, | |
likes: { | |
type: Object, | |
required: true, | |
count: { | |
type: Number, | |
required: true | |
}, | |
users: { | |
type: Array, | |
required: true | |
} | |
}, | |
dislikes: { | |
type: Object, | |
required: true, | |
count: { | |
type: Number, | |
required: true | |
}, | |
users: { | |
type: Array, | |
required: true | |
} | |
} | |
}, | |
discordfm: { | |
type: Object, | |
required: false, | |
libraries: { | |
type: Array, | |
required: true | |
}, | |
playlists: { | |
type: Array, | |
required: true | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment