API Docs for: 1.0.1

File: js\grape\class.js

//TODOv2 duplicated methods with different modifiers
define(['./utils'], function (Utils) {
    var nextId = 0;
    var registeredKeywords = {};

    /**
     * A fake class to represent default class methods
     *
     * @class Grape.Object
     */
    var classMethods = {
        /**
         * Tells whether the given class is a parent of the current class.
         *
         * @method extends
         * @static
         * @param {Class} clazz The class
         * @return {boolean} true, if the given class is a parent
         */
        extends: function (clazz) {
            return !!this.allParentId[clazz.id];
        },
        /**
         * Creates a new class, which extends this class. X.extend(a, b) is the same as Grape.Class(a,X,b)
         *
         * @method extend
         * @static
         * @param {String} [name] The class name
         * @param {Object} [methods] Class methods
         * @return {Class} The new class
         */
        extend: function (name, methods) {
            if (typeof name === 'string') { //name given
                if (methods) { //avoid undefined arguments
                    return Class(name, this, methods);
                } else {
                    return Class(name, this);
                }
            } else {
                if (name) { //avoid undefined arguments
                    return Class(this, name);
                } else {
                    return Class(this);
                }
            }
        }
    };

    var instanceMethods = {
        /**
         * Tells that the current instance is an instance of a class, or it's descendants.
         *
         * @method instanceOf
         * @param {Class} clazz
         * @return {boolean} true, if yes.
         */
        instanceOf: function (clazz) {
            return (this instanceof clazz) || !!this.getClass().allParentId[clazz.id];
        },
        /**
         * Creates a proxy for calling a parent method
         *
         * @method parent
         * @param {Class} clazz The parent, whose method will be called
         * @param {String} method Method name
         * @return {Function} Method proxy. When called, calls the parent method with the parameters, and original
         * context.
         */
        parent: function (clazz, method) {
            if (!this.instanceOf(clazz)) {
                throw new Error('Accessing parent member of not inherited class');
            }
            var m = clazz.prototype[method], that = this;
            if (Utils.isFunction(m)) {
                return function () {
                    return m.apply(that, arguments);
                };
            } else {
                return m;
            }
        },
        /**
         * Returns the instance's constructor class
         *
         * @method getClass
         * @return {Class}
         */
        getClass: function () {
            return this.constructor;
        }
    };

    function empty() {
    }

    /**
     * A static class for storing keyword related functions. To see how to create a class, check the Class method in the
     * Grape class.
     *
     * @class Grape.Class
     */

    /**
     * Creates a class by optionally copying prototype methods of one or more class.
     *
     * @for Grape
     * @method Class
     * @static
     * @param {String} [name] The name of the class (mainly for debugging purposes)
     * @param {Array|Class} [parents] Parent class or classes
     * @param {Object} methods An object containing methods. If method name contains space, the keyword parts are parsed
     * and keyword specific tasks are executed.
     * @return {*}
     */
    function Class(name, parents, methods) {
        var classInfo = {}, constructor, i, id = ++nextId;

        for (i = 0; i < arguments.length; i++) {
            if (typeof arguments[i] === 'undefined') {
                throw new Error('Argument is undefined: ' + i);
            }
        }
        //parameter transformations
        if (typeof name !== 'string') { //no name
            methods = parents;
            parents = name;
            name = 'Class #' + id;
        }
        if (!Utils.isArray(parents)) {
            if (Utils.isFunction(parents)) { //single parent
                parents = [parents];
            } else { //no parent
                methods = parents;
                parents = [];
            }

        }
        if (!methods) { //no methods
            methods = {};
        }


        /**
         * The name of the class if set, or a generated string.
         *
         * @for Grape.Object
         * @property className
         * @static
         * @type {String}
         */
        classInfo.className = name;

        /**
         * An unique number for the class, mainly for indexing purposes
         *
         * @for Grape.Object
         * @property id
         * @static
         * @type {Number}
         */
        classInfo.id = id;

        for (i in classMethods) { //plugins can use 'extends' check
            classInfo[i] = classMethods[i];
        }

        createParentInfo(classInfo, parents);
        createMethodDescriptors(classInfo, methods);

        initializeKeywords(classInfo);

        addParentMethods(classInfo); //left to right order
        addOwnMethods(classInfo);

        createConstructor(classInfo);

        finishKeywords(classInfo);

        constructor = classInfo.constructor;
        //extend prototype with methods
        for (i in classInfo.methods) {
            if (instanceMethods.hasOwnProperty(i)) {
                throw new Error('The method name "' + i + '" is reserved');
            }
            constructor.prototype[i] = classInfo.methods[i];
        }
        //extend constructor with class info
        for (i in classInfo) {
            constructor[i] = classInfo[i];
        }

        for (i in instanceMethods) {
            constructor.prototype[i] = instanceMethods[i];
        }

        constructor.prototype.init = constructor;
        constructor.toString = function () { //debug info
            return name;
        };

        constructor.prototype.constructor = constructor;

        return constructor;
    }

    function createParentInfo(classInfo, parents) {
        var i;
        classInfo.parents = parents;
        classInfo.allParent = getAllParent(parents);
        classInfo.allParentId = {};
        for (i = 0; i < classInfo.allParent.length; i++) {
            classInfo.allParentId[classInfo.allParent[i].id] = true;
        }
    }

    function createMethodDescriptors(classInfo, methods) {
        var methodDescriptors = {}, m;

        for (m in methods) {
            methodDescriptors[m] = parseMethod(m, methods[m], classInfo);
        }
        classInfo.methodDescriptors = methodDescriptors;
        classInfo.methods = {};
        classInfo.ownMethods = {};
        classInfo.init = null;
    }

    /*
     * We create a custom function for performance and debugging reasons.
     */
    function createConstructor(classInfo) {
        /*jslint evil: true */
        var name = classInfo.className, initMethods = [], factory = [], i, parent, constructor;
        //add parent init methods
        for (i = 0; i < classInfo.allParent.length; i++) {
            parent = classInfo.allParent[i];
            if (parent.init) {
                initMethods.push(parent.init);
            }
        }
        //add own init method
        if (classInfo.init) {
            initMethods.push(classInfo.init);
        }

        for (i = 0; i < initMethods.length; i++) {
            factory.push('var init' + i + ' = inits[' + i + '];'); //var init0 = inits[0];
        }

        //With this trick we can see the name of the class while debugging.
        factory.push('this["' + name + '"] = function(){'); //this["MyClass"] = function(){
        for (i = 0; i < initMethods.length; i++) {
            factory.push('init' + i + '.apply(this, arguments);'); //init0.apply(this, arguments)
        }
        factory.push('};');
        factory.push('return this["' + name + '"];'); //return this["MyClass"];
        constructor = (new Function('inits', factory.join('\n'))).call({}, initMethods);
        classInfo.constructor = constructor;
    }

    function initializeKeywords(classInfo) {
        var keyword;
        for (keyword in registeredKeywords) {
            (registeredKeywords[keyword].onInit || empty)(classInfo);
        }
    }

    function finishKeywords(classInfo) {
        var keyword;
        for (keyword in registeredKeywords) {
            (registeredKeywords[keyword].onFinish || empty)(classInfo);
        }
    }

    function addParentMethods(classInfo) {
        var i = 0, allParent = classInfo.allParent, parentsNum = allParent.length, parent, m;
        for (; i < parentsNum; i++) {
            parent = allParent[i];
            for (m in parent.ownMethods) {
                classInfo.methods[m] = parent.ownMethods[m];
            }
        }
    }

    function addOwnMethods(classInfo) {
        var m, methodDescriptors = classInfo.methodDescriptors, methodDescriptor, modifiers, i, j, modifier, canAdd;
        for (m in methodDescriptors) {
            methodDescriptor = methodDescriptors[m];
            if (methodDescriptor.init) {
                classInfo.init = methodDescriptor.method;
            } else {

                modifiers = methodDescriptor.modifiers;
                canAdd = true;
                for (i = 0; i < modifiers.length; i++) {
                    modifier = modifiers[i];
                    if (registeredKeywords[modifier]) {
                        //iterate over other modifiers checking compatibility
                        for (j = i + 1; j < modifiers.length; j++) {
                            if (modifier === modifiers[j]) {
                                throw new Error('Modifier "' + modifier + '" duplicated.');
                            }
                            if (!registeredKeywords[modifier].matches[modifiers[j]]) {
                                throw new Error('Modifier "' + modifier + '" cannot use with "' + modifiers[j] + '".');
                            }
                        }

                        if ((registeredKeywords[modifier].onAdd)(classInfo, methodDescriptor) === false) {
                            canAdd = false;
                        }
                    } else {
                        throw new Error('Unknown modifier "' + modifier + '"');
                    }
                }
                if (canAdd) {
                    classInfo.methods[methodDescriptor.name] = methodDescriptor.method;
                    classInfo.ownMethods[methodDescriptor.name] = methodDescriptor.method;
                }
            }
        }
    }

    function parseMethod(name, method, source) {
        var all = name.split(' '),
            modifiers = all.slice(0, -1),
            realName = all.slice(-1)[0],
            is = {},
            init = false,
            i;

        if (realName === 'init') {
            init = true;
            if (modifiers.length !== 0) {
                throw new Error('init method cannot be marked with any modifiers.');
            }
        }

        for (i = modifiers.length - 1; i >= 0; i--) {
            is[modifiers[i]] = true;
        }

        return {
            modifiers: modifiers,
            is: is,
            name: realName,
            method: method,
            source: source,
            init: init
        };
    }

    /*
     * Leftmost iteration of parent tree.
     */
    function getAllParent(parents, directly, acc) {
        var i, parentsNum = parents.length, parent;
        if (!directly) {
            directly = {};
            for (i = 0; i < parentsNum; i++) {
                parent = parents[i];
                if (!parent) {
                    throw new Error('Parent #' + (i + 1) + ' is ' + parent + '.');
                }
                directly[parent.id] = true;
            }
            acc = {
                list: [],
                set: {}
            };
        }
        for (i = 0; i < parentsNum; i++) { //add all parents recursively
            parent = parents[i];
            if (!acc.set[parent.id]) { //not added yet
                getAllParent(parent.parents, directly, acc);
                acc.list.push(parent);
                acc.set[parent.id] = true;
            } else if (directly[parent.id]) { //added directly
                throw new Error('Class "' + parent.className + '" is set as parent twice, or implied by a parent class'); //TODOv2 format global string
            }
        }
        return acc.list;
    }

    /**
     * Registers a new keyword (like 'final' or 'static').
     * Todov2 callback params
     *
     * @for Grape.Class
     * @method registerKeyword
     * @static
     * @param {String} name
     * @param {Object} handlers The functions called during the class creation
     * @param {Function} [handlers.onInit] Called when a new class is about to create
     * @param {Function} [handlers.onAdd] Called when a method with the keyword is added to the class
     * @param {Function} [handlers.onFinish] Called when the class is ready
     */
    function registerKeyword(name, handlers) {
        if (registeredKeywords[name]) {
            throw new Error('keyword "' + name + '" already registered');
        }
        handlers.matches = {};
        registeredKeywords[name] = handlers;
    }

    /**
     * Tells to the Grape class system that two keywords can be used together. If not explicitly told, a keyword cannot
     * be used with other ones. The order of keywords is irrelevant.
     *
     * @for Grape.Class
     * @static
     * @method registerKeywordMatching
     * @param {String} k1 Keyword 1
     * @param {String} k2 Keyword 2
     */
    function registerKeywordMatching(k1, k2) {
        registeredKeywords[k1].matches[k2] = true;
        registeredKeywords[k2].matches[k1] = true;
    }

    registerKeyword('static', {
        onAdd: function (classInfo, methodDescriptor) {
            if (classInfo[methodDescriptor.name] || classMethods[methodDescriptor.name]) {
                throw new Error('Static method "' + methodDescriptor.name + '" hides a reserved attribute.');
            }
            classInfo[methodDescriptor.name] = methodDescriptor.method;
            return false;
        }
    });

    registerKeyword('override', {
        onAdd: function (classInfo, methodDescriptor) {
            var i, j, parent;
            if (!classInfo.methods[methodDescriptor.name]) { //we are not overriding an implemented method

                //check for abstract methods
                for (i = 0; i < classInfo.allParent.length; ++i) {
                    parent = classInfo.allParent[i];
                    for (j in parent.abstracts) {
                        if (j === methodDescriptor.name) {
                            return;
                        }
                    }
                }
                //no abstract method found
                throw new Error('Method "' + methodDescriptor.name + '" does not override a method from its superclass');
            }
        }
    });

    registerKeyword('abstract', {
        onInit: function (classInfo) {
            classInfo.abstracts = {};
            classInfo.isAbstract = false;
        },
        onAdd: function (classInfo, methodDescriptor) {
            classInfo.abstracts[methodDescriptor.name] = methodDescriptor.method;
            classInfo.isAbstract = true;
            if (classInfo.methods[methodDescriptor.name]) { //inherited method with the same name
                throw new Error('Method "' + methodDescriptor.name + '" cannot be abstract, because it is inherited from a parent.');
            }
            return false;
        },
        onFinish: function (classInfo) {
            var i, j, parent, oldToString;
            if (classInfo.isAbstract) {
                //replace constructor, this happens before extending it with anything
                oldToString = classInfo.constructor.toString;
                classInfo.constructor = function () {
                    throw new Error('Abstract class "' + classInfo.className + '" cannot be instantiated.');
                };
                classInfo.constructor.toString = oldToString;
                classInfo.constructor.prototype.constructor = classInfo.constructor;
            }

            //check all abstract parent methods are implemented, inherited, or marked abstract
            for (i = 0; i < classInfo.allParent.length; ++i) {
                parent = classInfo.allParent[i];
                for (j in parent.abstracts) {
                    if (!classInfo.methods[j] && classInfo.abstracts[j] === undefined) {
                        throw new Error('Method "' + j + '" is not implemented, inherited, or marked abstract'); //TODOv2 source?
                    }
                }
            }
        }
    });

    registerKeyword('final', {
        onInit: function (classInfo) {
            var parent, i, j, parentFinals = {};
            //iterate over parent methods checking not overwrite a final method by inheriting
            for (i = 0; i < classInfo.allParent.length; ++i) {
                parent = classInfo.allParent[i];

                for (j in parent.methods) {
                    if (parentFinals.hasOwnProperty(j) && parentFinals[j] !== parent.methods[j]) { //overriding final method by inheriting
                        throw new Error('Method "' + j + '" is final and cannot be overridden by inheriting from "' + parent.className + '"');
                    }
                }

                for (j in parent.finals) {
                    parentFinals[j] = parent.finals[j];
                }
            }
            classInfo.parentFinals = parentFinals;
            classInfo.finals = {};
        },
        onAdd: function (classInfo, methodDescriptor) {
            classInfo.finals[methodDescriptor.name] = methodDescriptor.method;
        },
        onFinish: function (classInfo) {
            var i;

            for (i in classInfo.parentFinals) {
                if (classInfo.methods[i] !== classInfo.parentFinals[i]) {
                    throw new Error('Overriding final method "' + i + '"');
                }
            }
        }
    });

    registerKeywordMatching('final', 'override');


    Class.registerKeyword = registerKeyword;
    Class.registerKeywordMatching = registerKeywordMatching;

    return Class;
});