Skip to content

Instantly share code, notes, and snippets.

@deansheather
Last active June 25, 2016 02:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save deansheather/4bccb070c121dfc009e1f1c65ef5bdd9 to your computer and use it in GitHub Desktop.
Save deansheather/4bccb070c121dfc009e1f1c65ef5bdd9 to your computer and use it in GitHub Desktop.
Schema validator for Javascript.
'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
'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.')
}
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