Source: dto.js

var _ = require('lodash');

/**
 * A SnoozeJS DTO
 * @constructor
 * @param {string} nm - The name of the DTO
 * @param {object} module - The module the DTO will belong to
 */
var DTO = function(nm, module) {
	/**
	 * @access private
	 * @ignore
	 * The SnoozeJS NPM Module
	 */
	var snooze = require('./snooze');

	/**
	 * @access private
	 * @ignore
	 * The name of the DTO
	 */
	var _name = null;

	/**
	 * @access private
	 * @ignore
	 * The properties of the DTO
	 */
	var _properties = [];

	/**
	 * @access private
	 * @ignore
	 * The custom methods for the DTO
	 */
	var _methods = {};

	/**
	 * @access private
	 * @ignore
	 * The recorded injected services
	 */
	var _services = [];

	/**
	 * @access private
	 * @ignore
	 * The recorded injected dtos
	 */
	var _dtos = [];

	/**
	 * @access private
	 * @ignore
	 * The recorded injected daos
	 */
	var _daos = [];

	/**
	 * @access private
	 * @ignore
	 * Boolean if the DTO is in strict mode
	 */
	var _strict = false;

	/**
	 * @access private
	 * @ignore
	 * Boolean if the DTO is should practice delayed injection
	 */
	var _delayMethodInjection = true;

	/**
	 * @access private
	 * @ignore
	 * Array of delayed injectable DTOs
	 */
	var _delayedDTOs = [];

	if(nm === null || nm === undefined || nm.length < 1) {
		snooze.fatal(new snooze.exceptions.DTONameNotDefinedException());
	} else {
		_name = nm;
	}

	/**
	 * DTOInstance
	 * An instance of the defined DTO
	 * @constructor
	 * @alias DTOInstance
	 */
	var _DTOInstance = function() {
		/**
		 * @access private
		 * @ignore
		 * The actual values of the instance
		 */
		var _values = {};

		/**
		 * Initializes instance values to an empty object
		 */
		var $startNewInstance = function() {
			_values = {};
		};

		/**
		 * Creates a new DTO instance from the supplied JSON object.
		 * @param {object} obj - A JSON object
		 * return {object} A JSON Object.
		 */
		var $create = function(obj) {
			_startNewInstance();

			for(var key in obj) {
				_set(key, obj[key]);
			}

			_defaults(_values);

			for(var i = 0; i < _properties.length; i++) {
				var Prop = _properties[i];
				var name = Prop.name;

				if(Prop.required === true) {
					if(obj[name] === undefined) {
						snooze.fatal(new snooze.exceptions.DTOMissingPropertyException(nm, key));
					}
				}
			}

			return _get();
		};

		/**
		 * Runs the provided data through the DTO applying the defaults.
		 * If the JSON object supplied has values for the defaults the
		 * defaults will not overwrite the existing values. Does not manipulate
		 * the instance itself.
		 *
		 * @param {object} obj - A JSON object
		 */
		var $defaults = function(obj) {
			for(var i = 0; i < _properties.length; i++) {
				var property = _properties[i];
				var key = property.name;
				var val = obj[key];
				if(val === undefined) {
					var defVal = property.default;
					if(defVal !== undefined && defVal !== null) {
						obj[key] = defVal;
					}
				}
			}
		}

		/**
		 * Initializes the instance _values with defaults
		 */
		var $init = function() {
			for(var i = 0; i < _properties.length; i++) {
				var property = _properties[i];

				var def = property.default;
				var key = property.name;

				if(def) {
					_set(key);
				}
			}
		};

		/**
		 * @access private
		 * @ignore
		 * Sets a key of the instance to value
		 * @param {string} key - The key of the instance to set
		 * @param {string} value - The value of the key
		 */
		var _set = function(key, value) {
			if(value === undefined) {
				value = _getDefValue(key);
			}

			value = _normalizeType(key, value);
			
			if(_keyExists(key) && value !== undefined) {
				if(_typeSupported(key, value)) {
					_values[key] = value;
				} else {
					snooze.fatal(new snooze.exceptions.DTOUnsupportedTypeException(nm, key, _getType(key), value));
				}
			} else {
				if(_strict === true) {
					snooze.fatal(new snooze.exceptions.DTOUndefinedKeyException(nm, key));
				}
			}
		};

		/**
		 * @access private
		 * @ignore
		 * Gets the default value of the supplied key
		 * @param {string} key - The key
		 * @return {string} The default value (or undefined)
		 */
		var _getDefValue = function(key) {
			for(var i = 0; i < _properties.length; i++) {
				var prop = _properties[i];

				if(prop.name === key) {
					return prop.default;
				}
			}

			return undefined;
		};

		/**
		 * @access private
		 * @ignore
		 * Gets the _values
		 * @return {object} The JSON object of the instance
		 */
		var _get = function() {
			return _values;
		};

		/**
		 * Tests if the JSON object is valid for the DTO.
		 * Returns an exception if unable to conver the JSON object
		 * to a DTO or null if no problems are found.
		 *
		 * @param {object} obj - The JSON Objecct
		 * @return {object} The exception (or null)
		 */
		var $test = function(obj) {
			for(var key in obj) {
				var value = obj[key];
				value = _normalizeType(key, value);

				if(_keyExists(key) && value !== undefined) {
					if(_typeSupported(key, value) === false) {
						return new snooze.exceptions.DTOUnsupportedTypeException(nm, key, _getType(key), value);
					}
				} else {
					if(_strict === true) {
						return new snooze.exceptions.DTOUndefinedKeyException(nm, key);
					}
				}
			}

			for(var i = 0; i < _properties.length; i++) {
				var Prop = _properties[i];
				var name = Prop.name;

				if(Prop.required === true) {
					if(obj[name] === undefined) {
						return new snooze.exceptions.DTOMissingPropertyException(nm, key);
					}
				}
			}

			return null;
		};

		/**
		 * @access private
		 * @ignore
		 *
		 * Attempts to normalize the value to the correct type.
		 * Ex: If Age is an int and '30' is supplied it will be converted
		 * to the int value 30. If the value can be normalized the
		 * normalized value is return. Otherwise the original value
		 * is returned.
		 *
		 * @param {string} key - The key of the value
		 * @param {string} value - The value you want to normalize
		 * @return {mixed} The normalized value
		 */
		var _normalizeType = function(key, value) {
			if(_keyExists(key)) {
				var type = _getType(key);

				switch(type) {
					case 'string':
						if(_.isString(value)) {
							return value;
						} else if(_.isNumber(value)) {
							return value + '';
						} else {
							return value;
						}
					case 'int':
						if(_.isString(value)) {
							if(_.isNaN(parseInt(value)) === false) {
								return parseInt(value);
							}
						} else if(_.isNumber(value)) {
							return value;
						} else {
							return value;
						}
					case 'number':
						if(_.isString(value)) {
							if(_.isNaN(parseFloat(value)) === false) {
								return parseFloat(value);
							}
						} else if(_.isNumber(value)) {
							return value;
						} else {
							return value;
						}
					case 'boolean':
						return Boolean(value);
					case 'array':
						return value;
					case 'object':
						return value;
				}
			}
			return value;
		};

		this.$defaults = $defaults;
		this.$create = $create;
		this.$test = $test;
		this.$init = $init;
		this.$startNewInstance = $startNewInstance;
	};

	/**
	 * DTOProperty
	 * @constructor
	 * @alias DTOProperty
	 */
	var _DTOProperty = function() {
		/**
		 * The name of the property
		 */
		this.name = '';
		/**
		 * The type of the property
		 */
		this.type = '';
		/**
		 * The description of the property
		 */
		this.description = '';
		/**
		 * The example of the property
		 */
		this.example = '';
		/**
		 * The default value of the property
		 */
		this.default = '';
		/**
		 * Boolean if the property is required or not
		 */
		this.required = false;
	};

	/**
	 * Gets the name of the Controller
	 * @return {string} The name of the Controller
	 */
	var getName = function() {
		return _name;
	};

	/**
	 * Adds a new property to the DTO
	 * @param {string} name - The name (or key) of the property
	 * @params {string} type - The type of the property
	 * @params {mixed} def - The default value of the property
	 * @params {string} description - The description of the property
	 * @params {mixed} example - An exmple of the property
	 * @params {boolean} required - If the property is required
	 */
	var addProperty = function(name, type, def, description, example, required) {
		var Prop = new _DTOProperty();
		
		if(_typeExists(type)) {
			Prop.name = name;
			Prop.type = type;
			Prop.default = def;
			Prop.description = description;
			Prop.example = example;
			Prop.required = required;

			_properties.push(Prop);

			if(!_typeSupported(name, def)) {
				snooze.fatal(new snooze.exceptions.DTOUnsupportedTypeException(nm, name, type, def));
			}
		} else {
			snooze.fatal(new snooze.exceptions.DTOTypeNotFoundException(nm, name, type));
		}
	};

	/**
	 * Gets the defined properties
	 * @return {array} Properties
	 */
	var getProperties = function() {
		return _properties;
	};

	/**
	 * Inject function gets a DTO Instance
	 * @return {object} A DTO Instance
	 */
	var $get = function() {
		var _DTO = new _DTOInstance();

		if(_delayMethodInjection === true) {
			_delayedDTOs.push(_DTO);
		} else {
			_injectMethods(_DTO);
			_DTO.$init();
		}

		return _DTO;
	};

	/**
	 * @access private
     * @ignore
	 * Injects custom methods for the DTO in a DTO Instance
	 * @param {object} _DTO - The DTO Instance
	 */
	var _injectMethods = function(_DTO) {
		for(var method in _methods) {
			_DTO[method] = function() {
				return _DTO.$create(_methods[method].apply(null, arguments));
			};

			_DTO[method].toString = function() {
				return _methods[method]+'';
			};
		}
	};

	/**
	 * @access private
     * @ignore
	 * Checks if the key exists
	 * @param {string} nm - The name of the key
	 * @return {boolean} Returns true if the key exists
	 */
	var _keyExists = function(nm) {
		for(var i = 0; i < _properties.length; i++) {
			if(_properties[i].name === nm) {
				return true;
			}
		}

		return false;
	};

	/**
	 * @access private
     * @ignore
	 * Gets the property definition of the supplied key
	 * @param {string} nm - The key of the property to get
	 * @return {object} The property definition JSON
	 */
	var _getProperty = function(nm) {
		for(var i = 0; i < _properties.length; i++) {
			if(_properties[i].name === nm) {
				return _properties[i];
			}
		}

		snooze.fatal(new Error('unkown property ' + nm + ' in DTO ' + _name));
	};

	/**
	 * @access private
     * @ignore
	 * Checks if the type is a DTO (noted by @)
	 * @param {string} type - The string type Ex: 'int', 'string', '@UserDTO'
	 * @return {boolean} Returns true if the type is a DTO
	 */
	var _isDTO = function(type) {
		if(typeof type === 'string') {
			if(type[0] === '@') {
				var DTO = module.getDTO(type.substr(1));
				if(DTO !== undefined) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * @access private
     * @ignore
	 * Checks if the type is a valid type
	 * @param {string} type - The string type Ex: 'int', 'string', '@UserDTO'
	 * @return {boolean} Returns true if the type is valid
	 */
	var _typeExists = function(type) {
		if(_isDTO(type)) {
			return true;
		}

		switch(type) {
			case 'string':
				return true;
			case 'int':
				return true;
			case 'number':
				return true;
			case 'array':
				return true;
			case 'object':
				return true;
			case 'boolean':
				return true;
		}

		return false;
	}

	/**
	 * @access private
     * @ignore
	 * Checks if the type of the key and its value is valid
	 * @param {string} key - The key of the property to check
	 * @param {mixed} value - The value to test
	 * @return {boolean} Returns true if the value is valid for the key
	 */
	var _typeSupported = function(key, value) {
		var property = _getProperty(key);

		if(value === null) {
			return true;
		}

		if(property !== undefined) {
			switch(property.type) {
				case 'string':
					if(_.isString(value)) {
						return true;
					}

					return false;
				case 'int':
					if(_.isNumber(value)) {
						if((value + '').indexOf('.') === -1) {
							return true;
						}
					}

					return false;
				case 'number':
					if(_.isNumber(value)) {
						return true;
					}

					return false;
				case 'array':
					if(_.isArray(value)) {
						return true;
					}

					return false;
				case 'object':
					if(_.isObject(value)) {
						return true;
					}

					return false;
				case 'boolean':
					if(_.isBoolean(value)) {
						return true;
					}

					return false;
				default:
					if(_isDTO(property.type)) {
						if(_.isObject(value)) {
							return true;
						}
					}
			}
		}

		return false;
	};

	/**
	 * @access private
     * @ignore
	 * Gets the type defined for the key
	 * @param {string} nm - The key of the property to check
	 * @return {string} The type of the property
	 */
	var _getType = function(nm) {
		return _getProperty(nm).type;
	};

	/**
	 * Sets the custom methods for all DTO instances created
	 * from this DTO
	 *
	 * @param {array} methods - An array of methods (post injection)
	 */
	var setMethods = function(methods) {
		for(var key in methods) {
			_methods[key] = function() {
				try {
					return methods[key].apply(null, arguments);
				} catch(e) {
					snooze.fatal(e);
				}
			};

			_methods[key].toString = function() {
				return methods[key]+'';
			};
		}
	};

	/**
     * @ignore
	 * Records an injected Service in the DTO
	 */
	var __addSrv = function(srv) {
		_services.push(srv);
	};

	/**
	 * @ignore
	 * Records an injected DTO in the DTO
	 */
	var __addDTO = function(dto) {
		_dtos.push(dto);
	};

	/**
	 * @ignore
	 * Records an injected DAO in the DTO
	 */
	var __addDAO = function(dao) {
		_daos.push(dao);
	};

	/**
	 * @ignore
	 * Gets the recorded Services for this DTO
	 * @return {array} An array of Services
	 */
	var __getServices = function() {
		return _services;
	};

	/**
	 * @ignore
	 * Gets the recorded DTOs for this DTO
	 * @return {array} An array of DTOs
	 */
	var __getDTOs = function() {
		return _dtos;
	};

	/**
	 * @ignore
	 * Gets the recorded DAOs for this DTO
	 * @return {array} An array of DAOs
	 */
	var __getDAOs = function() {
		return _daos;
	};

	/**
	 * Sets or returns the strict mode of the DTO
	 * @param {boolean} [bool] - Optional boolean
	 *
	 * @return {boolean} If no bool param is defined
	 * returns true if the DTO is in strict mode
	 */
	var isStrict = function(bool) {
		if(bool !== undefined) {
			_strict = bool;
		} else {
			return _strict;
		}
	};

	/**
	 * @ignore
	 * If compilation hasn't completed delay method injection
	 */
	var delayMethodInjection = function(bool) {
		_delayedDTOs.splice(0);
		_delayMethodInjection = bool;
	};

	/**
	 * @ignore
	 * Complete method injection
	 */
	var completeDTOInjection = function() {
		_.each(_delayedDTOs, function(_DTO) {
			_injectMethods(_DTO);
		});
		delayMethodInjection(false);
	}

	return {
		getName: getName,
		getProperties: getProperties,
		addProperty: addProperty,
		setMethods: setMethods,
		isStrict: isStrict,
		delayMethodInjection: delayMethodInjection,
		completeDTOInjection: completeDTOInjection,
		__getServices: __getServices,
		__getDTOs: __getDTOs,
		__getDAOs: __getDAOs,
		__addSrv: __addSrv,
		__addDTO: __addDTO,
		__addDAO: __addDAO,
		$get: $get
	}
}

module.exports = DTO;