draft
Version:
Construct Object schemas and models
225 lines (185 loc) • 27.4 kB
JavaScript
!function (global) {
/**
* hasOwnProperty.
*/
var has = Object.prototype.hasOwnProperty;
/**
* Require the given path.
*
* @param {String} path
* @return {Object} exports
* @api public
*/
function require(path, parent, orig) {
var resolved = require.resolve(path);
// lookup failed
if (null == resolved) {
orig = orig || path;
parent = parent || 'root';
var err = new Error('Failed to require "' + orig + '" from "' + parent + '"');
err.path = orig;
err.parent = parent;
err.require = true;
throw err;
}
var module = require.modules[resolved];
// perform real require()
// by invoking the module's
// registered function
if (!module.exports) {
module.exports = {};
module.client = module.component = true;
module.call(this, module.exports, require.relative(resolved), module);
}
return module.exports;
}
/**
* Registered modules.
*/
require.modules = {};
/**
* Registered aliases.
*/
require.aliases = {};
/**
* Resolve `path`.
*
* Lookup:
*
* - PATH/index.js
* - PATH.js
* - PATH
*
* @param {String} path
* @return {String} path or null
* @api private
*/
require.resolve = function(path) {
if (path.charAt(0) === '/') path = path.slice(1);
var index = path + '/index.js';
var paths = [
path,
path + '.js',
path + '.json',
path + '/index.js',
path + '/index.json'
];
for (var i = 0; i < paths.length; i++) {
var path = paths[i];
if (has.call(require.modules, path)) return path;
}
if (has.call(require.aliases, index)) {
return require.aliases[index];
}
};
/**
* Normalize `path` relative to the current path.
*
* @param {String} curr
* @param {String} path
* @return {String}
* @api private
*/
require.normalize = function(curr, path) {
var segs = [];
if ('.' != path.charAt(0)) return path;
curr = curr.split('/');
path = path.split('/');
for (var i = 0; i < path.length; ++i) {
if ('..' == path[i]) {
curr.pop();
} else if ('.' != path[i] && '' != path[i]) {
segs.push(path[i]);
}
}
return curr.concat(segs).join('/');
};
/**
* Register module at `path` with callback `definition`.
*
* @param {String} path
* @param {Function} definition
* @api private
*/
require.register = function(path, definition) {
require.modules[path] = definition;
};
/**
* Alias a module definition.
*
* @param {String} from
* @param {String} to
* @api private
*/
require.alias = function(from, to) {
if (!has.call(require.modules, from)) {
throw new Error('Failed to alias "' + from + '", it does not exist');
}
require.aliases[to] = from;
};
/**
* Return a require function relative to the `parent` path.
*
* @param {String} parent
* @return {Function}
* @api private
*/
require.relative = function(parent) {
var p = require.normalize(parent, '..');
/**
* lastIndexOf helper.
*/
function lastIndexOf(arr, obj) {
var i = arr.length;
while (i--) {
if (arr[i] === obj) return i;
}
return -1;
}
/**
* The relative require() itself.
*/
function localRequire(path) {
var resolved = localRequire.resolve(path);
return require(resolved, parent, path);
}
/**
* Resolve relative to the parent.
*/
localRequire.resolve = function(path) {
var c = path.charAt(0);
if ('/' == c) return path.slice(1);
if ('.' == c) return require.normalize(p, path);
// resolve deps by returning
// the dep in the nearest "deps"
// directory
var segs = parent.split('/');
var i = lastIndexOf(segs, 'deps') + 1;
if (!i) i = 0;
path = segs.slice(0, i + 1).join('/') + '/deps/' + path;
return path;
};
/**
* Check if module is defined at `path`.
*/
localRequire.exists = function(path) {
return has.call(require.modules, localRequire.resolve(path));
};
return localRequire;
};
require.register("draft/index.js", Function("exports, require, module",
"/**\n * Module dependencies\n */\n\nvar define = Object.defineProperty\n , freeze = Object.freeze\n , isFrozen = Object.isFrozen\n , isArray = Array.isArray\n , toString = Object.prototype.toString\n\n/**\n * Exports\n */\n \nmodule.exports = draft;\ndraft.Draft = Draft;\ndraft.Schema = Schema\ndraft.Tree = Tree;\ndraft.Type = Type;\ndraft.Model = Model;\n\n\n/**\n * Merges two or more objects together. Also performs deep merging\n *\n * @see http://stackoverflow.com/a/383245/1408668\n * @api private\n * @param {Object} object \n * @param {Object} objectN\n */\n\nfunction merge(obj1, obj2) {\n\n for (var p in obj2) {\n try {\n // Property in destination object set; update its value.\n if ( obj2[p].constructor==Object ) {\n obj1[p] = merge(obj1[p], obj2[p]);\n\n } else {\n obj1[p] = obj2[p];\n\n }\n\n } catch(e) {\n // Property in destination object not set; create it and set its value.\n obj1[p] = obj2[p];\n\n }\n }\n\n return obj1;\n}\n\n\n/**\n * Checks whether the input is a plain object\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isPlainObject (input) {\n if (input !== null && typeof input === 'object' && input.constructor === Object) return true;\n else return false;\n}\n\n\n/**\n * Checks whether the input is a function\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isFunction (input) {\n return (typeof input === 'function');\n}\n\n/**\n * Checks whether the input is a boolean\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isBoolean (input) {\n return (typeof input === 'boolean');\n}\n\n\n/**\n * Checks whether the input is a undefined\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isUndefined (input) {\n return (typeof input === 'undefined');\n}\n\n\n/**\n * Checks whether the input is a null\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isNull (input) {\n return (input === null);\n}\n\n\n/**\n * Checks whether the input is a string\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isString (input) {\n return (typeof input === 'string');\n}\n\n\n/**\n * Checks whether the input is a NaN\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction isTrueNaN (input) {\n return (isNaN(input) && input !== NaN && typeof input === 'number');\n}\n\n\n/**\n * Converts an array like object to an array\n *\n * @api private\n * @param {Mixed} input\n */\n\nfunction toArray (input) {\n return Array.prototype.slice.call(arguments, 0);\n}\n\n\n/**\n * CHecks whether a given input is in an array\n *\n * @api private\n * @param {Array} array\n * @param {Mixed} needle\n */\n\nfunction inArray (array, needle) {\n return !!~array.indexOf(needle);\n}\n\n\n/**\n * Draft object\n *\n * @api public\n * @constructor Draft\n * @param {Object} descriptor\n * @param {Object} options\n */\n\nfunction Draft (descriptor, options) {\n this.schema = new Schema(descriptor, options);\n this.Model = this.schema.createModel();\n}\n\n\n/**\n * @namespace draft\n * @api public\n * @function draft\n * @param {Object} descriptor\n * @param {Object} options\n */\n\nfunction draft (descriptor, options) {\n return (new Draft(descriptor, options)).Model;\n}\n\n/**\n * Creates a schema\n *\n * @api public\n * @function draft.createSchema\n * @param {Object} descriptor\n * @param {Object} options\n */\n\ndraft.createSchema = function (descriptor, options) {\n return new Schema(descriptor, options);\n}\n\n\n/**\n * Creates a model from a schema\n *\n * @api public\n * @function draft.createModel\n * @param {Schema} schema\n * @param {Object} options\n */\n\ndraft.createModel = function (schema, options) {\n if (! (schema instanceof Schema)) \n throw new TypeError(\"draft.createModel expects an instance of Schema. Got '\"+ typeof schema +\"'\");\n else return schema.createModel(options);\n}\n\n\n/**\n * Creats an object schema\n *\n * @constructor Schema\n * @api public\n * @param {Object} descriptor\n * @param {Object} options\n */\n\nfunction Schema (descriptor, options) {\n var self = this\n // we must use plain objects\n if (typeof descriptor !== 'undefined' && !isPlainObject(descriptor)) \n throw new TypeError(\"Schema only expects an object as a descriptor. Got '\"+ typeof descriptor +\"'\");\n // create tree instance with an empty object\n this.tree = new Tree({});\n // add descriptor to tree\n this.add(descriptor);\n // attach options\n this.options = merge({ strict : true}, isPlainObject(options)? options : {});\n}\n\n\n/**\n * Adds an object to the schema tree\n *\n * @api public\n * @function Schema#add\n * @see Tree#add\n */\nSchema.prototype.add = function () {\n this.tree.add.apply(this.tree, arguments);\n};\n\n\n/**\n * Creates a static function for the created model\n *\n * @api public\n * @function Schema#.static\n * @param {String} name\n * @param {Function} func\n */\n\nSchema.prototype.static = function (name, func) {\n if (isPlainObject(name)) {\n for (func in name) {\n this.static(func, name[func]);\n }\n }\n else {\n if (!isString(name)) throw new TypeError(\"Schema#static exepects a string identifier as a function name\");\n else if (!isFunction(func)) throw new TypeError(\"Schema#static exepects a function as a handle\");\n this.add(name, { type: Function, static: true, value: func });\n }\n};\n\n\n/**\n * Creates a constructor from the defined schema\n *\n * @api public\n * @function Schema#createModel\n * @param {Object} options\n */\n\nSchema.prototype.createModel = Schema.prototype.toModel = function (options, proto) {\n var self = this\n options = (isPlainObject(options))? options : {};\n var instances = []\n /**\n * Private implementation of model\n */\n function InstanceModel () { \n instances.push(this);\n return Model.apply(this, arguments); \n }\n // set incoming prototype first\n if (typeof proto === 'object') {\n InstanceModel.prototype = proto;\n InstanceModel.prototype.__proto__ = Object.create(Model.prototype);\n }\n else {\n InstanceModel.prototype = Object.create(Model.prototype);\n }\n // reset constructor\n InstanceModel.prototype.constructor = InstanceModel;\n // attach schema instance\n InstanceModel.prototype.schema = this;\n // attach instances\n InstanceModel.prototype.instances = instances;\n // sugar to not use the 'new' operator\n InstanceModel.create = function (data, schema) { return new this(data, schema); }.bind(InstanceModel);\n // only scan top level\n for (var item in this.tree) {\n // prevent overrides\n if (!isUndefined(InstanceModel[item])) continue;\n // it must be defined and have a valid function value\n if (this.tree[item].static === true && !isUndefined(this.tree[item].value)) {\n InstanceModel[item] = (isFunction(this.tree[item].value))? this.tree[item].value.bind(InstanceModel) : this.tree[item].value;\n }\n }\n\n // if the user wants to alloq modifications \n if (options.freeze !== false) freeze(InstanceModel);\n return InstanceModel\n};\n\n\n/**\n * Accepts an object of data and passes it to the\n * Model constructor from the Schema instance\n *\n * @api public\n * @function Schema#new\n * @param {Object} data\n */\n\nSchema.prototype.new = function (data) {\n var model = this.createModel();\n return new model(data);\n};\n\n\n/**\n * Creates an object tree for a schema.\n * This is used for aggregating types\n *\n * @constructor Tree\n * @api public\n * @param {Object} descriptor\n * @param {Object} options\n */\n\nfunction Tree (descriptor, options) {\n var self = this\n // ensure we have an object\n if (!isArray(descriptor) && descriptor !== undefined && descriptor !== null && !isPlainObject(descriptor))\n throw new TypeError(\"Tree only expects a descriptor\");\n else this.add(descriptor);\n\n if (isPlainObject(options) && options.array === true) {\n var array = []\n array.__proto__ = this;\n array.type = new Type(options.type)\n return array;\n }\n}\n\n\n\n/**\n * Adds a key to the tree on a given parent tree. \n * Defaults to 'this' as the parent if one is not provided.\n *\n * @api public\n * @function Tree#add\n * @param {Tree} parent\n * @param {String} key\n * @param {Object} descriptor\n */\n\nTree.prototype.add = function (parent, key, descriptor) {\n // are they just passing in an object as one big descriptor?\n if (typeof parent === 'object' && arguments.length === 1) {\n for (var prop in parent) {\n this.add(this, prop, parent[prop]);\n }\n }\n else {\n parent = (parent instanceof Tree || isString(parent))? parent : this;\n // is this a reference to a child tree?\n if (parent instanceof Tree) {\n if (isPlainObject(descriptor)) {\n if (isFunction(descriptor.type)) {\n parent[key] = new Type(descriptor.type, descriptor);\n }\n else {\n parent[key] = new Tree(descriptor);\n }\n }\n else if (isFunction(descriptor)) {\n parent[key] = new Type(descriptor);\n }\n else if (isArray(descriptor)) {\n if (descriptor.length && isFunction(descriptor[0])) {\n parent[key] = new Tree(null, { array: true, type: descriptor[0] });\n }\n else {\n parent[key] = [];\n }\n }\n }\n else if (isString(parent) && key) {\n descriptor = key\n key = parent;\n this.add(this, key, descriptor);\n }\n }\n};\n\n\n/**\n * Creates a Type used in a Tree instance for a \n * Schema instance. It is meant to provide methods\n * for validation and coercion.\n *\n * @constructor Type\n * @api public\n * @param {Function} Constructor\n */\n\nfunction Type (Constructor, descriptor) {\n // ensure creation of Type\n if (!(this instanceof Type)) return new Type(Constructor, descriptor);\n // ensure descriptor object\n descriptor = (typeof descriptor === 'object')? descriptor : {};\n if (!isFunction(Constructor)) throw new TypeError(\"Type only expects a function\");\n // set the constructor for reference\n this.Constructor = Constructor;\n // remove type property from the descriptor if it was set there\n delete descriptor.type;\n // check for getter\n if (isFunction(descriptor.get)) (this.get = descriptor.get) && delete descriptor.get;\n // check for setter\n if (isFunction(descriptor.set)) (this.set = descriptor.set) && delete descriptor.set;\n // check if the values of this property are enumerable\n if (isArray(descriptor.enum)) (this.enum = descriptor.enum) && delete descriptor.enum;\n // check if strict mode\n if (isBoolean(descriptor.strict)) (this.strict = descriptor.strict) && delete descriptor.strict;\n // check if static\n if (isBoolean(descriptor.static)) (this.static = descriptor.static) && delete descriptor.static;\n // check if has set value\n if (descriptor.value) (this.value = descriptor.value) && delete descriptor.value;\n // check if has validator\n if (isFunction(descriptor.validator)) (this.validator = descriptor.validator) && delete descriptor.validator;\n // check if has default\n if (descriptor.default) {\n if (Constructor !== Function && 'function' === typeof descriptor.default) {\n this.default = descriptor.default();\n } else {\n this.default = descriptor.default;\n }\n\n delete descriptor.default;\n }\n}\n\n\n/**\n * Returns a string representation of a Type instance\n */\n\nType.prototype.toString = function () {\n return '[object Type]';\n};\n\n\n/**\n * Return original constructor let it handle valueOf\n */\n\nType.prototype.valueOf = function () {\n return this.Constructor.valueOf();\n};\n\n\n/**\n * Default getter that coerces a value\n *\n * @api public\n * @function Type#get\n * @param {Mixed} value\n */\n\nType.prototype.get = function (value) {\n return this.coerce(value);\n};\n\n\n/**\n * Default setter that coerces a value\n *\n * @api public\n * @function Type#set\n * @param {Mixed} value\n */\n\nType.prototype.set = function (value) {\n return this.coerce(value);\n};\n\n\n/**\n * Validates a defined type. \n * It performs instance of checks on values that are not primitive.\n * Primitive inputs are validated with a 'typeof' check\n *\n * @api public\n * @function Type#validate\n * @param {Mixed} input\n */\n\nType.prototype.validate = function (input) {\n var Constructor\n // validate with validator first if present\n if (isFunction(this.validator) && !this.validator(input)) return false;\n // if not strict mode then we don't need to validate anything\n if (this.strict === false) return true;\n // if its an object and the type constructor is\n // not an object then validate that it is an \n // actual instance of the type constructor \n if (typeof input === 'object' && this.Constructor !== Object)\n return (input instanceof this.Constructor);\n // check for enumerated values\n if (isArray(this.enum)) {\n return inArray(this.enum, input);\n }\n // check input for primitive types\n switch (typeof input) {\n case 'string': Constructor = String; break;\n case 'function': Constructor = Function; break;\n case 'boolean': Constructor = Boolean; break;\n case 'object': Constructor = Object; break;\n case 'number': Constructor = Number; break;\n }\n // compare Type Constructor with input Constructor\n return this.Constructor === Constructor;\n};\n\n\n/**\n * Coerces a given input with the set Constructor type\n *\n * @api public\n * @function Type#coerce\n * @param {Mixed} input\n */\n\nType.prototype.coerce = function (input) {\n try { return this.Constructor(input); }\n catch (e) { return input; }\n};\n\n\n/**\n * Base constructor for all created Model instances\n *\n * @constructor Model\n * @api public\n * @param {Object} data\n */\n\nfunction Model (data, schema) {\n if (! (this instanceof Model)) return new Model(data, schema);\n define(this, 'schema', {\n enumerable : false,\n writable : false,\n configurable : false,\n value: (schema instanceof Schema)? schema : this.schema\n });\n \n var self = this\n /** internal memory **/\n var table = {};\n // ensure an object if not undefined\n if (data !== undefined && typeof data !== 'object') throw new TypeError(\"Model expects an object. Got '\"+ typeof data +\"'\");\n // ensure the schema set\n if (!this.schema || !(this.schema instanceof Schema)) throw new TypeError(\".schema hasn't been set\");\n\n var build = function (data, tree, object) {\n tree = (tree instanceof Tree)? tree : self.schema.tree;\n object = (typeof object === 'object')? object : this;\n for (var prop in data) {\n // encapsulate each iteration in a scope\n !function (prop) {\n // if not in tree, return and continue on\n if (!tree[prop]) return;\n // if the property is an object, check if the tree property\n // is a Tree instance object too\n if (typeof data[prop] === 'object' && tree[prop] instanceof Tree) {\n // define setter for object\n define(data[prop], 'set', {\n writable : false,\n enumerable : false,\n configurable : false,\n value : function (value) {\n object[prop] = value; \n }\n });\n // define getter for object\n define(data[prop], 'get', {\n writable : false,\n enumerable : false,\n configurable : false,\n value : function () { return object[prop] }\n });\n\n build(data[prop], tree[prop], object[prop]);\n }\n // we've reached some kind of scalar value \n // that exists in the schema tree and the object\n else {\n object[prop] = data[prop];\n }\n }.call(this, prop);\n }\n }.bind(this);\n // overload refresh method on prototype\n var refresh = function () {\n if (isFrozen(this)) return false;\n var defineFromTree = function (tree, scope, table) {\n var item\n for (item in tree) {\n !function (item) {\n if (tree[item] === null || typeof tree[item] !== 'object') return;\n // we don't want this as a possible field\n if (tree[item].static) return\n // it must be an instance of Type\n if (tree[item] instanceof Type) {\n // only set on plain objects\n if (!isArray(scope)) {\n // if it doesn't exist in the internal table\n // then set it to undefined\n table[item] = table[item] || undefined;\n // create descriptor for property item on scope\n // from tree descriptor\n define(scope, item, {\n configurable : false,\n enumerable : true,\n get : function () { \n return table[item]? tree[item].get(table[item]) : undefined; \n },\n set : function (value) {\n if (isFunction(tree[item].validate) && tree[item].validate(value)) {\n table[item] = tree[item].set(value);\n return table[item];\n } else { \n return false;\n }\n }\n });\n }\n\n if (!isUndefined(tree[item].default)) {\n scope[item] = tree[item].default;\n }\n }\n // if it is a tree instance then we need\n // to do a recursive call to define the\n // descriptors needed for the object\n else if (tree[item] instanceof Tree || isArray(tree[item])) {\n table[item] = isArray(tree[item])? [] : {};\n\n if (isArray(tree[item])) {\n define(scope, item, {\n configurable: false,\n enumerable : true,\n writable: false,\n value: table[item]\n });\n }\n else {\n define(scope, item, {\n configurable: false,\n enumerable : true,\n writable : false,\n value : {}\n });\n }\n\n define(scope[item], 'get', {\n configurable: false,\n enumerable: false,\n writable: false,\n value: function (key) { return table[item][key]; }\n });\n\n define(scope[item], 'set', {\n configurable: false,\n enumerable: false,\n writable: false,\n value: function (value) { \n table[item][key] = value; \n return table[item];\n }\n });\n\n // overload array methods\n if (isArray(tree[item])) {\n [\n 'concat', 'every', 'filter', 'forEach', 'indexOf', 'join', 'lastIndexOf', 'map', 'pop',\n 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice',\n 'toString', 'unshift', 'valueOf'\n ].map(function (method) {\n if (isFunction([][method])) {\n define(scope[item], method, {\n configurable: false,\n enumerable: false,\n writable: false,\n value: function (value) {\n if (value !== undefined) {\n if (tree[item].type instanceof Type && isFunction(tree[item].type.validate)) {\n if (tree[item].type.validate(value)) {\n return [][method].apply(table[item], arguments)\n }\n else {\n return false;\n }\n }\n else {\n return [][method].apply(table[item], arguments)\n }\n }\n else {\n return [][method].apply(table[item], arguments);\n }\n }.bind(null)\n });\n }\n });\n }\n\n // recursive call to define descriptors\n defineFromTree.call(self, tree[item], scope[item], table[item]);\n }\n }.call(self, item);\n }\n \n if (this.schema.options.strict && scope !== this) \n freeze(scope)\n }.bind(this);\n // define\n defineFromTree(self.schema.tree, this, table);\n // free if in strict mode\n if (this.schema.options.strict) freeze(this);\n };\n // overload set method on prototype\n define(this, 'set', {\n configurable : false,\n enumerable : false,\n writable : true,\n value : function (key, value) {\n return this[key] = value;\n }\n });\n\n define(this, 'get', {\n configurable : false,\n enumerable : false,\n writable : false,\n value : function (key) {\n return table[key]\n }\n });\n\n define(this, 'refresh', {\n configurable : false,\n enumerable : false,\n writable : false,\n value : refresh\n });\n\n define(this, 'toObject', {\n configurable: false,\n enumerable : false,\n writable : false,\n value : function () {\n return table;\n }\n });\n\n define(this, 'toJSON', {\n configurable: false,\n enumerable : false,\n writable : false,\n value : function () {\n return table;\n }\n });\n\n // call a refresh to init schema\n this.refresh();\n // set data\n data && build(data);\n}\n\n\nModel.prototype._events;\nModel.prototype.domain;\n\n/**\n * A reference to the schema instance for the model\n *\n * @api public\n * @property {Schema} schema\n */\n\n\nModel.prototype.schema;\n\n\n/**\n * Refreshes the state of the model based on its schema\n *\n * @api public\n * @function Model#refresh\n * @interface \n */\n\nModel.prototype.refresh = function () {};\n\n\n/**\n * Sets data on the model based on the schema\n *\n * @api public\n * @function Model#set\n * @interface \n */\n\nModel.prototype.set = function () {};\n\n\n/**\n * Returns a plain object representation of the model\n *\n * @api public\n * @function Model#toObject\n * @interface \n */\n\nModel.prototype.toObject = function () {};\n\n\n/**\n * Called with JSON.stringify\n *\n * @api public\n * @function Model#toJSON\n * @interface \n */\n\nModel.prototype.toJSON = function () {};\n\n\n/**\n * Returns a string representation of a Model instance\n *\n * @api public\n * @function Model#toString\n * @interface \n */\n\nModel.prototype.toString = function () { return '[object Model]'; };\n\n\n/**\n * Returns a value representation of a Model instance\n *\n * @api public\n * @function Model#valueOf\n * @interface \n */\n\nModel.prototype.valueOf = function () { return this.toObject(); };\n\n//@ sourceURL=draft/index.js"
));
require.alias("draft/index.js", "draft/index.js");
if (typeof module === "object" && typeof module.exports === "object") {
module.exports = require('draft');
}
else {
if (typeof define === "function" && define.amd) {
define("draft", [], function () { return require('draft'); });
}
else if ( typeof window === "object" && typeof window.document === "object" ) {
window.draft = require('draft');
}
}
}();