documents/schema.js

"use strict";
/**
 * @fileOverview Enables a schema and validation feature set to your document or other object.
 * @module documents/schema
 * @requires base
 * @requires jjv
 * @require lodash
 */
var sys = require( "lodash" );
var Validator = require( "jjv" );
var Base = require( "../base" );
/**
 * The validator mixin provides access to the features of the JSON validation system
 * @exports documents/schema
 * @mixin
 */
var Schema = Base.compose( [Base], /** @lends documents/schema# */{
	constructor : function () {
		/**
		 * The schema that defines the validation rules. This should probably be defined at the prototype for each
		 * object or model classification. It can be an anonymous schema defined right here, or this can be
		 * registered schema names to use, or just a single name
		 *
		 * @type {object}
		 * @memberOf documents/schema#
		 * @name schema
		 */

		/**
		 * If you want to register multiple schemas, use this property instead
		 *
		 * @type {object}
		 * @memberOf documents/schema#
		 * @name schemas
		 */

		/**
		 * The validation environment
		 * @private
		 * @type {jjv}
		 */
		var env = new Validator();

		/**
		 * The default name of the scheman when you use anonymous schemas. You can define this at the prototype for classified
		 * schemas. The can also
		 *
		 * @type {string|function():{string}}
		 * @memberOf documents/schema#
		 * @name _defaultSchemaName
		 */
		this._defaultSchemaName = sys.result( this, "_defaultSchemaName" ) || sys.uniqueId( "schema" );

		/**
		 * The options to pass to the validator when it runs
		 * @type {object|function():{object}}
		 * @name validationOptions
		 * @memberOf documents/schema#
		 */
		this.validationOptions = sys.defaults( {}, sys.result( this, 'validationOptions' ), {checkRequired : true} );

		/**
		 * Validate an object against the schema
		 * @returns {object?}
		 * @method
		 * @name validate
		 * @memberOf documents/schema#
		 * @param {object=} record The record to validate
		 * @param {string|object=} schemaName The name of a previously registered schema
		 * @param {object=} options Options to pass to the validator
		 * @example
		 * // This supports these signatures:
		 *
		 * instance.validate(record, schemaName, options);
		 *
		 *
		 * instance.validate(); // this, this._defaultSchemaName, this.validationOptions
		 * instance.validate(record); // record, this._defaultSchemaName, this.validationOptions
		 * instance.validate(schemaName); //this, schemaName, this.validationOptions
		 * instance.validate(record, schemaName); //record, schemaName, this.validationOptions
		 * instance.validate(schemaName, options); //this, schemaName, this.validationOptions
		 */
		this.validate = function ( record, schemaName, options ) {
			if ( arguments.length === 0 ) {
				record = this;
				schemaName = this._defaultSchemaName;
				options = this.validationOptions;
			} else {
				if ( sys.isString( record ) ) {
					schemaName = record;
					record = this;
				}
				if ( sys.isEmpty( options ) ) {
					options = this.validationOptions;
				}
			}

			return env.validate( schemaName, record, options );
		};

		/**
		 * Initialize the schema collection by registering the with the handler. You can call this at any time and as often as you like. It will be called once
		 * by the constructor on any instance schemas
		 * @method
		 * @name registerSchemas
		 * @memberOf documents/schema#
		 * @param {hash} schemas A hash of schemas where the key is the name of the schema
		 */
		this.registerSchemas = function ( schemas ) {
			var schema = sys.result( this, "schema" );
			var schemas = schemas || sys.result( this, "schemas" );
			if ( !sys.isEmpty( schema ) ) {
				env.addSchema( this._defaultSchemaName, schema );
			}
			if ( !sys.isEmpty( schemas ) ) {
				sys.each( schemas, function ( val, key ) {
					env.addSchema( val, key );
				} );
			}
		};

		/**
		 * Extracts only the elements of the object that are defined in the schema
		 * @memberOf documents/schema#
		 * @name extract
		 * @param {object=} record The record to extract from
		 * @param {string=} schema The name of the schema to attach
		 * @method
		 */
		this.extract = function ( record, schema ) {
			if ( arguments.length === 0 ) {
				record = this;
				schema = this._defaultSchemaName;
			}
			if ( sys.isString( record ) ) {
				schema = record;
				record = this;
			}
		};

		/**
		 * Create a type to be used in your schemas to define new validators
		 * @memberOf documents/schema#
		 * @name addType
		 * @method
		 * @param {string} name The name of the type
		 * @param {function(object)} operation What to do with the type.
		 * @param {object} operation.value The value to validation
		 * @returns {boolean}
		 */
		this.addType = env.addType;

		/**
		 * It is also possible to add support for additional string formats through the addFormat function.
		 * @memberOf documents/schema#
		 * @name addFormat
		 * @method
		 * @param {string} name The name of the formatter
		 * @param {function(object)} formatter How to format it
		 * @param {object} formatter.value The value to format
		 * @returns {boolean}
		 */
		this.addFormat = env.addFormat;

		/**
		 * It is possible to add support for custom checks (i.e., minItems, maxItems, minLength, maxLength, etc.) through the addCheck function
		 * @memberOf documents/schema#
		 * @name addCheck
		 * @method
		 * @param {string} name The name of the check
		 * @param {function(...object)} formatter Perform the check
		 * @param {object} formatter.value The value to check followed by any parameters from the schema
		 * @returns {boolean}
		 */
		this.addCheck = env.addCheck;

		/**
		 * Custom coercion rules
		 *
		 * @memberOf documents/schema#
		 * @name addTypeCoercion
		 * @method
		 * @param {string} name The name of the coercion
		 * @param {function(object)} coercer Perform the coercion
		 * @param {object} coercer.value The value to coerce
		 * @returns {boolean}
		 */
		this.addTypeCoercion = env.addTypeCoercion;

		/**
		 * Get a registered schema by name
		 * @param {string=} schemaName
		 * @returns {object?}
		 * @memberOf documents/schema#
		 * @name getSchema
		 * @method
		 */
		this.getSchema = function ( schemaName ) {
			if ( sys.isEmpty( schemaName ) || !sys.isString() ) {
				schemaName = this._defaultSchemaName;
			}
			return env.schema[schemaName];
		}
	},
	/**
	 * This method will create a new object that contains only the fields and no methods or other artifacts. This is useful
	 * for creating objects to pass over the wire or save in a table. This is not deeply copied, so changes made to the
	 * extracted object will be represented in this class for reference objects.
	 *
	 * @param {string=} schema The schema name to use
	 * @param {object=} src The object to extract fields from
	 * @return {object} Data-only version of the class instance.
	 */
	extract     : function ( schemaName, src ) {
		if ( sys.isObject( schemaName ) ) {
			src = schema;
			schemaName = this._defaultSchemaName;
		}

		if ( sys.isEmpty( src ) ) {
			src = this;
		}

		if ( sys.isFunction( src.toJSON ) ) {
			src = src.toJSON();
		}
		var schema = this.getSchema( schemaName ) || {};
		var newobj = {};
		sys.each( schema.properties, function ( prop, propname ) {
			if ( prop.properties && !sys.isUndefined( src[ propname ] ) ) {
				newobj[ propname ] = this.extract( prop, src[propname] );
			} else if ( !sys.isUndefined( src[ propname ] ) ) {
				newobj[ propname ] = src[ propname ];
			}
		}, this );

		return newobj;
	},
	/**
	 * Builds a default document based on the schema. What this does is create a document from schema and for each property
	 * that has a default value or is required, the resultant object will contain that property. It is useful for extending
	 * values from some source that may be incomplete, like options or some such.
	 * @param {json-schema} schema A schema to use to create the default document
	 * @returns {object?}
	 * @name defaultDoc
	 * @memberOf documents/schema#
	 * @method
	 */
	defaultDoc  : function ( schemaName ) {
		if ( sys.isEmpty( schemaName ) ) {
			schemaName = this._defaultSchemaName;
		}
		var newdoc = {};
		var schema;

		if ( sys.isObject( schemaName ) ) {
			schema = schemaName;
		} else {
			schema = this.getSchema( schemaName ) || {};
		}
		sys.each( schema.properties, function ( val, key ) {

			var def = val[ "default" ]; // keyword and all that
			if ( val.type === "object" && !sys.isEmpty( val.properties ) ) {
				newdoc[ key ] = this.defaultDoc( val );
			} else {
				if ( sys.isFunction( def ) || sys.isBoolean( def ) || sys.isNumber( def ) || !sys.isEmpty( def ) ) {

					if ( sys.isFunction( def ) ) {
						newdoc[ key ] = def( schema );
					} else {
						newdoc[ key ] = def;
					}
				} else if ( val.required ) {
					if ( val.type === 'string' ) {
						newdoc[ key ] = null;
					} else if ( val.type === 'object' ) {
						newdoc[ key ] = {};
					} else if ( val.type === 'array' ) {
						newdoc[ key ] = [];
					} else {
						newdoc[ key ] = null;
					}
				}
			}
		}, this );

		return newdoc;
	}
} );

module.exports = Schema;