sebastian-software/core

View on GitHub
source/class/core/property/Simple.js

Summary

Maintainability
D
1 day
Test Coverage
/*
==================================================================================================
  Core - JavaScript Foundation
  Copyright 2010-2012 Zynga Inc.
  Copyright 2012-2014 Sebastian Werner
==================================================================================================
*/

"use strict";

(function(undef)
{
    /** {Map} Maps simple property names to global property IDs */
    var propertyNameToId = {};

    /** {=String} Field where the data is stored */
    var store = "$$data";


    /**
     * Property handling for simple key/value like properties which might have an optional init value.
     *
     * Supports the following configuration keys:
     *
     * - `type`: Check the incoming value for the given type or function.
     * - `apply`: Link to function to call after a new value has been stored. The signature of the method is
     * `function(newValue, oldValue)`
     * - `fire`: Event to fire after a new value has been stored (and apply has been called). The listeners
     * are called with 3 parameters: value, old value and property name.
     * - `init`: Init value for the property. If no value is set or the property gets reset, the getter
     * will return the `init` value.
     * - `nullable`: Whether the property is able to store null values. This also allows the system to
     * return `null` when no other value is available. Otherwise an error is thrown whenever no value is
     * available.
     * - `validate`: Link to function which should return a bool based on whether the valud passed detailed
     * value validation. Especially useful for using properties inside data models (MVC, MVP, etc.)
     *
     * #break(core.property.Debug)
     * #break(core.property.Event)
     */
    core.Module("core.property.Simple",
    {
        /**
         * {Map} Creates a new set of member methods for the given property @config {Map}.
         *
         * Please note that you need to define one of "init" or "nullable". Otherwise you
         * might get errors during runtime function calls.
         */
        create : function(config)
        {
            /*
            ---------------------------------------------------------------------------
                INTRO
            ---------------------------------------------------------------------------
            */

            // Shorthands: Better compression/obfuscation/performance
            var propertyName = config.name;
            var propertyNullable = config.nullable;
            var propertyInit = config.init;
            var propertyType = config.type;
            var propertyFire = config.fire;
            var propertyApply = config.apply;
            var propertyCast = config.cast;
            var propertyValidate = config.validate;

            // Validation
            if (jasy.Env.isSet("debug"))
            {
                core.Assert.doesOnlyHaveKeys(config, "name,nullable,init,type,fire,apply,cast,validate",
                    "Unallowed keys in property: " + propertyName + "!");
                core.Assert.isType(propertyName, "String");

                if (propertyNullable !== undef) {
                    core.Assert.isType(propertyNullable, "Boolean");
                }

                if (propertyType) {
                    core.property.Debug.isValidType(propertyType);
                }

                if (propertyFire) {
                    core.Assert.isType(propertyFire, "String");
                }

                if (propertyApply) {
                    core.Assert.isType(propertyApply, "Function");
                }

                if (propertyCast != null)
                {
                    core.Assert.isType(propertyCast, "Boolean");

                    /**
                     * When enabled the `type` should refer to a `core.Class`
                     *
                     * #break(core.Class)
                     */
                    if (propertyCast && !core.Class.isClass(propertyType))
                    {
                        throw new Error("Property declaration of " + propertyName +
                            " contains invalid configuration: Casting support requires a core.Class for the type of the property!");
                    }
                }

                if (propertyValidate) {
                    core.Assert.isType(propertyValidate, "Function");
                }
            }

            // Generate property ID
            // Identically named property might store data on the same field
            // as in this case this is typicall on different classes.
            var propertyId = propertyNameToId[propertyName];
            if (!propertyId) {
                propertyId = propertyNameToId[propertyName] = (core.property.Core.ID++);
            }

            // Prepare return value
            var members = {};



            /*
            ---------------------------------------------------------------------------
                METHODS :: GET
            ---------------------------------------------------------------------------
            */

            members.get = function()
            {
                var data, value;
                var context = this;

                if (jasy.Env.isSet("debug")) {
                    core.property.Debug.checkGetter(context, config, arguments);
                }

                data = context[store];
                if (data) {
                    value = data[propertyId];
                }

                if (value === undef)
                {
                    if (propertyInit !== undef) {
                        return propertyInit;
                    }

                    if (jasy.Env.isSet("debug"))
                    {
                        if (!propertyNullable) {
                            context.error("Missing value for: " + propertyName + " (during get())");
                        }
                    }

                    value = null;
                }

                return value;
            };



            /*
            ---------------------------------------------------------------------------
                METHODS :: INIT
            ---------------------------------------------------------------------------
            */

            if (propertyInit !== undef)
            {
                members.init = function()
                {
                    var context = this;
                    var data = context[store];

                    // Check whether there is already local data (which is higher prio than init data)
                    if (!data || data[propertyId] === undef)
                    {
                        // Call apply
                        if (propertyApply) {
                            propertyApply.call(context, propertyInit, undef);
                        }

                        // Fire event
                        if (propertyFire)
                        {
                            var eventObject = core.property.Event.obtain(propertyFire, propertyInit, undef, propertyName);
                            context.dispatchEvent(eventObject);
                            eventObject.release();
                        }
                    }
                };
            }



            /*
            ---------------------------------------------------------------------------
                METHODS :: SET
            ---------------------------------------------------------------------------
            */

            members.set = function(value)
            {
                var context=this, data, old;

                // Wrap plain types to match property type
                // Modifying `value` should also modify the arguments object which
                // is required for value tests happening via checkSetter
                if (config.cast && core.Main.isTypeOf(value, "Plain"))
                {
                    value = new config.type(value);

                    if (jasy.Env.isSet("debug"))
                    {
                        // arguments object is not updated in strict mode anymore, fix this
                        if (arguments[0] !== value) {
                            arguments[0] = value;
                        }
                    }
                }

                // Check types
                if (jasy.Env.isSet("debug")) {
                    core.property.Debug.checkSetter(context, config, arguments);
                }

                data = context[store];
                if (!data) {
                    data = context[store] = {};
                } else {
                    old = data[propertyId];
                }

                if (value !== old)
                {
                    if (old === undef && propertyInit !== undef) {
                        old = propertyInit;
                    }

                    data[propertyId] = value;

                    if (propertyApply) {
                        propertyApply.call(context, value, old);
                    }

                    if (propertyFire)
                    {
                        var eventObject = core.property.Event.obtain(propertyFire, value, old, propertyName);
                        context.dispatchEvent(eventObject);
                        eventObject.release();
                    }
                }

                return value;
            };



            /*
            ---------------------------------------------------------------------------
                METHODS :: RESET
            ---------------------------------------------------------------------------
            */

            members.reset = function()
            {
                var context, data, old, value;
                context = this;

                if (jasy.Env.isSet("debug")) {
                    core.property.Debug.checkResetter(context, config, arguments);
                }

                data = context[store];
                if (!data) {
                    return;
                }

                old = data[propertyId];
                value = undef;

                if (old !== value)
                {
                    data[propertyId] = value;

                    if (propertyInit !== undef) {
                        value = propertyInit;
                    }
                    else if (jasy.Env.isSet("debug"))
                    {
                        // Still no value. We warn about that the property is not nullable.
                        if (!propertyNullable) {
                            context.error("Missing value for: " + propertyName + " (during reset())");
                        }
                    }

                    if (propertyApply) {
                        propertyApply.call(context, value, old);
                    }

                    if (propertyFire)
                    {
                        var eventObject = core.property.Event.obtain(propertyFire, value, old, propertyName);
                        context.dispatchEvent(eventObject);
                        eventObject.release();
                    }
                }
            };



            /*
            ---------------------------------------------------------------------------
                METHODS :: ISVALID
            ---------------------------------------------------------------------------
            */

            if (propertyValidate)
            {
                members.isValid = function()
                {
                    var data, value;
                    var context = this;

                    if (jasy.Env.isSet("debug")) {
                        core.property.Debug.checkIsValid(context, config, arguments);
                    }

                    data = context[store];
                    if (data) {
                        value = data[propertyId];
                    }

                    if (value === undef)
                    {
                        if (propertyInit !== undef)
                        {
                            value = propertyInit;
                        }
                        else if (!propertyNullable)
                        {
                            context.error("Missing value for: " + propertyName + " (during isValid())");
                            return false;
                        }
                    }

                    return propertyValidate.call(context, value);
                };
            }



            /*
            ---------------------------------------------------------------------------
                DONE
            ---------------------------------------------------------------------------
            */

            return members;
        }
    });
})();