declare module ObjectMakr { export interface IObjectMakrClassInheritance { [i: string]: string | IObjectMakrClassInheritance; } export interface IObjectMakrClassProperties { [i: string]: any; } export interface IObjectMakrSettings { inheritance: any; properties?: { [i: string]: any }; doPropertiesFull?: boolean; indexMap?: any; onMake?: string; } export interface IObjectMakrClassFunction { new (); } export interface IObjectMakr { getInheritance(): any; getProperties(): any; getPropertiesOf(title: string): any; getFullProperties(): any; getFullPropertiesOf(title: string): any; getFunctions(): any; getFunction(name: string): Function; hasFunction(name: string): boolean; getIndexMap(): any; make(name: string, settings?: any): any; } } module ObjectMakr { "use strict"; /** * An Abstract Factory for JavaScript classes that automates the process of * setting constructors' prototypal inheritance. A sketch of class inheritance * and a listing of properties for each class is taken in, and dynamically * accessible function constructors are made available. */ export class ObjectMakr implements IObjectMakr { /** * The sketch of class inheritance, keyed by name. */ private inheritance: IObjectMakrClassInheritance; /** * Type properties for each class. */ private properties: IObjectMakrClassProperties; /** * The actual Functions for the classes to be made. */ private functions: { [i: string]: IObjectMakrClassFunction; }; /** * Whether a full property mapping should be made for each type. */ private doPropertiesFull: boolean; /** * If doPropertiesFull is true, a version of properties that contains the * sum properties for each type (rather than missing inherited ones). */ private propertiesFull: any; /** * Optionally, how properties can be mapped from an Object to keys. */ private indexMap: any; /** * Optionally, a String index for each generated Object's Function to * be run when made. */ private onMake: string; /** * @param {IObjectMakrSettings} settings */ constructor(settings: IObjectMakrSettings) { if (typeof settings === "undefined") { throw new Error("No settings object given to ObjectMakr."); } if (typeof settings.inheritance === "undefined") { throw new Error("No inheritance given to ObjectMakr."); } this.inheritance = settings.inheritance; this.properties = settings.properties || {}; this.doPropertiesFull = settings.doPropertiesFull; this.indexMap = settings.indexMap; this.onMake = settings.onMake; this.functions = {}; if (this.doPropertiesFull) { this.propertiesFull = {}; } if (this.indexMap) { this.processProperties(this.properties); } this.processFunctions(this.inheritance, Object, "Object"); } /* Simple gets */ /** * @return {Object} The complete inheritance mapping Object. */ getInheritance(): any { return this.inheritance; } /** * @return {Object} The complete properties mapping Object. */ getProperties(): any { return this.properties; } /** * @return {Object} The properties Object for a particular class. */ getPropertiesOf(title: string): any { return this.properties[title]; } /** * @return {Object} The full properties Object, if doPropertiesFull is on. */ getFullProperties(): any { return this.propertiesFull; } /** * @return {Object} The full properties Object for a particular class, if * doPropertiesFull is on. */ getFullPropertiesOf(title: string): any { return this.doPropertiesFull ? this.propertiesFull[title] : undefined; } /** * @return {Object} The full mapping of class constructors. */ getFunctions(): any { return this.functions; } /** * @param {String} name The name of a class to retrieve. * @return {Function} The constructor for the given class. */ getFunction(name: string): Function { return this.functions[name]; } /** * @param {String} type The name of a class to check for. * @return {Boolean} Whether that class exists. */ hasFunction(name: string): boolean { return this.functions.hasOwnProperty(name); } /** * @return {Mixed} The optional mapping of indices. */ getIndexMap(): any { return this.indexMap; } /* Core usage */ /** * Creates a new instance of the given type and returns it. * If desired, any settings are applied to it (deep copy using proliferate). * @param {String} type The type for which a new object of is being made. * @param {Objetct} [settings] Additional attributes to add to the newly * created Object. * @return {Mixed} */ make(name: string, settings: any = undefined): any { var output: any; // Make sure the type actually exists in functions if (!this.functions.hasOwnProperty(name)) { throw new Error("Unknown type given to ObjectMakr: " + name); } // Create the new object, copying any given settings output = new this.functions[name](); if (settings) { this.proliferate(output, settings); } // onMake triggers are handled respecting doPropertiesFull. if (this.onMake && output[this.onMake]) { if (this.doPropertiesFull) { output[this.onMake]( output, name, this.properties[name], this.propertiesFull[name] ); } else { output[this.onMake]( output, name, this.properties[name], this.functions[name].prototype ); } } return output; } /* Core parsing */ /** * Parser that calls processPropertyArray on all properties given as arrays * * @param {Object} properties The object of function properties * @remarks Only call this if indexMap is given as an array */ private processProperties(properties: any): void { var name: string; // For each of the given properties: for (name in properties) { if (this.properties.hasOwnProperty(name)) { // If it's an array, replace it with a mapped version if (this.properties[name] instanceof Array) { this.properties[name] = this.processPropertyArray(this.properties[name]); } } } } /** * Creates an output properties object with the mapping shown in indexMap * * @param {Array} properties An array with indiced versions of properties * @example indexMap = ["width", "height"]; * properties = [7, 14]; * output = processPropertyArray(properties); * // output is now { "width": 7, "height": 14 } */ private processPropertyArray(properties: any[]): any { var output: any = {}, i: number; // For each [i] in properties, set that property as under indexMap[i] for (i = properties.length - 1; i >= 0; --i) { output[this.indexMap[i]] = properties[i]; } return output; } /** * Recursive parser to generate each function, starting from the base. * * @param {Object} base An object whose keys are the names of functions to * made, and whose values are objects whose keys are * for children that inherit from these functions * @param {Function} parent The parent function of the functions about to * be made * @param {String} parentName The name of the parent Function to be * inherited from. * @remarks This may use eval, which is evil and almost never a good idea, * but here it's the only way to make functions with dynamic names. */ private processFunctions(base: any, parent: any, parentName: string): void { var name: string, ref: string; // For each name in the current object: for (name in base) { if (base.hasOwnProperty(name)) { this.functions[name] = (new Function()); // This sets the function as inheriting from the parent this.functions[name].prototype = new parent(); this.functions[name].prototype.constructor = this.functions[name]; // Add each property from properties to the function prototype for (ref in this.properties[name]) { if (this.properties[name].hasOwnProperty(ref)) { this.functions[name].prototype[ref] = this.properties[name][ref]; } } // If the entire property tree is being mapped, copy everything // from both this and its parent to its equivalent if (this.doPropertiesFull) { this.propertiesFull[name] = {}; if (parentName) { for (ref in this.propertiesFull[parentName]) { if (this.propertiesFull[parentName].hasOwnProperty(ref)) { this.propertiesFull[name][ref] = this.propertiesFull[parentName][ref]; } } } for (ref in this.properties[name]) { if (this.properties[name].hasOwnProperty(ref)) { this.propertiesFull[name][ref] = this.properties[name][ref]; } } } this.processFunctions(base[name], this.functions[name], name); } } } /* Utilities */ /** * Proliferates all members of the donor to the recipient recursively, as * a deep copy. * * @param {Object} recipient An object receiving the donor's members. * @param {Object} donor An object whose members are copied to recipient. * @param {Boolean} [noOverride] If recipient properties may be overriden * (by default, false). */ private proliferate(recipient: any, donor: any, noOverride: boolean = false): void { var setting: any, i: string; // For each attribute of the donor for (i in donor) { // If noOverride is specified, don't override if it already exists if (noOverride && recipient.hasOwnProperty(i)) { continue; } // If it's an object, recurse on a new version of it setting = donor[i]; if (typeof setting === "object") { if (!recipient.hasOwnProperty(i)) { recipient[i] = new setting.constructor(); } this.proliferate(recipient[i], setting, noOverride); } else { // Regular primitives are easy to copy otherwise recipient[i] = setting; } } return recipient; } } }