/** * Created with WebStorm. * Project: MoaJs * User: Sergii Danilov * Date: 10/31/13 * Time: 6:10 PM */ /*global define:true, module:true*/ /** * @module Moa * * @desc MoaJs micro library for easiest implementation of prototype inheritance, * closure for base prototype, mixins, static methods and mixins, * simple declaration for singleton behavior of type in JavaScript. * MoaJs contains IoC container for resolving declared types as * field or constructor injection to instance */ (function () { "use strict"; if (!Object.create) { Object.create = (function () { function F() {} return function (o) { if (arguments.length !== 1) { throw new Error('Object.create implementation only accepts one parameter.'); } F.prototype = o; return new F(); }; }()); } var undef, map = {}, mixins = {}, extend = function (target, source) { var prop; if (source) { for (prop in source) { if (source.hasOwnProperty(prop)) { target[prop] = source[prop]; } } //Some Object methods are not enumerable on Internet Explorer target.toString = source.toString; target.valueOf = source.valueOf; target.toLocaleString = source.toLocaleString; } return target; }, fastExtend = function (target, source) { var prop; for (prop in source) { target[prop] = source[prop]; } return target; }, throwWrongParamsErr = function (method, param) { var msg = 'Wrong parameters in ' + method; if (param) { msg = 'Wrong parameter ' + param + ' in ' + method; } throw new Error(msg, 'Moa'); }, throwWrongType = function (obj, extendType, isMixin) { var type = 'Type '; if (obj === undef) { if (isMixin === true) { type = 'Mixin type '; } throw new Error(type + extendType + ' not found', 'Moa'); } }, addMixins = function ($proto, $mixin) { var prop, value, MixFn; for (prop in $mixin) { value = $mixin[prop]; MixFn = mixins[value]; throwWrongType(MixFn, value, true); MixFn.call($proto); $proto[prop] = new MixFn(); } return $proto; }, build = function (type, base, definition) { var basetype, $staticMixin, $single = definition.$single, $static = definition.$static, $mixin = definition.$mixin, $ctor = definition.$ctor, $di = definition.$di, $base = {}; if ($ctor !== undef) { delete definition.$ctor; } else { $ctor = function () {}; } delete definition.$single; delete definition.$static; delete definition.$mixin; delete definition.$extend; if ($static !== undef) { $staticMixin = $static.$mixin; if ($staticMixin !== undef) { delete $static.$mixin; addMixins($ctor, $staticMixin); } extend($ctor, $static); } if ($mixin !== undef) { definition = extend(addMixins({}, $mixin), definition); } if (base !== undef) { basetype = base.$type; definition = extend(Object.create(base.$ctor.prototype), definition); } definition.getType = function () { return type; }; extend($base, definition); $ctor.prototype = definition; $ctor.prototype.constructor = $ctor; if ($single !== undef && $single === true) { (function () { var instance = new $ctor(); $ctor = function () { return instance; }; $ctor.getInstance = function () { return instance; }; }()); } $base.$ctor = $ctor; return { $type: type, $basetype: basetype, $mixin: $mixin, $di: $di, $ctor: $ctor, $base: $base }; }, resolveDeclaration = function (type, diConfiguration, owner) { var configurationProperty, configurationValue, configurationValueType, typeObj, propFlag = false; diConfiguration = diConfiguration || {}; if (!diConfiguration.$current) { diConfiguration.$current = type; } for (configurationProperty in diConfiguration) { configurationValue = diConfiguration[configurationProperty]; configurationValueType = typeof configurationValue; switch (configurationProperty) { case '$current': switch (configurationValueType) { case 'string': configurationValue = { type: configurationValue, instance: 'item', lifestyle: 'transient' }; break; case 'object': configurationValue.type = type; if (!configurationValue.instance) { configurationValue.instance = 'item'; } if (!configurationValue.lifestyle && configurationValue.instance !== 'ctor') { configurationValue.lifestyle = 'transient'; } break; default: } break; case '$ctor': configurationValue = resolveDeclaration(type, configurationValue, configurationProperty); delete configurationValue.$current; break; case '$proto': configurationValue = resolveDeclaration(type, configurationValue, configurationProperty); delete configurationValue.$current; break; case '$prop': configurationValue = resolveDeclaration(type, configurationValue, configurationProperty); delete configurationValue.$current; break; default: propFlag = true; switch (configurationValueType) { case 'string': typeObj = map[configurationValue]; if (typeObj) { if (owner === '$proto') { configurationValue = fastExtend({}, typeObj.$di.$current); configurationValue.lifestyle = 'singleton'; } else { configurationValue = typeObj.$di.$current; } } break; case 'object': if (configurationValue.type) { if (configurationValue.instance === 'ctor') { delete configurationValue.lifestyle; } else { if (!configurationValue.instance) { configurationValue.instance = 'item'; } if (!configurationValue.lifestyle) { configurationValue.lifestyle = 'transient'; } } } break; default: } } if (owner || !propFlag) { diConfiguration[configurationProperty] = configurationValue; } else { propFlag = false; if (!diConfiguration.$prop) { diConfiguration.$prop = {}; } diConfiguration.$prop[configurationProperty] = configurationValue; delete diConfiguration[configurationProperty]; } } return diConfiguration; }, Moa = { /** * Clear all defined types and mixins * @method clear */ clear: function () { var clearObj = function (obj) { var prop; for (prop in obj) { delete obj[prop]; } }; clearObj(map); clearObj(mixins); }, /** * Declaration configuration for type * @typedef {object} DeclarationConf * @property {function} [$ctor] - constructor of type * @property {string} [$extend] - inheritable type name * @property {DiConf} [$di] - configuration for IoC container * @property {object} [$mixin] - literal with mixins declaration * @property {object} [$static] - literal with properties and function that applied to constructor * @property {boolean} [$single] - setup type as singleton */ /** * Declaration configuration for type * @typedef {function} DeclarationFn * @param {object} $base - prototype of inheritable type with $base.$ctor - constructor of inheritable type * @return {DeclarationConf} object that uses $base closure for access to inheritable type implementation constructor and methods */ /** * Define new or inherited type * @method define * @param {string} type - name of type * @param {(DeclarationConf|DeclarationFn)} [definition] - see {@link DeclarationConf} or {@link DeclarationFn}. * If it is null - delete declared object * @return {function} constructor of defined object type * * @example <caption>Declaration without <code>$base</code> closure</caption> * var constructor = Moa.define('baseObj', { * $ctor: function (name) { * this.name = name; * }, * getName: function() { * return this.name; * } * }); * * @example <caption>Declaration inheritance and <code>$base</code> closure</caption> * var constructor = Moa.define('child', function ($base) { * // $base - containe reference to prototype of 'baseObj' * return { * $extend: 'baseObj', * $ctor: function (name, age) { * this.age = age; * $base.$ctor.call(this, name); * }, * getAge: function () { * return this.age; * } * }; * }); * * @example <caption>Delete type declaration</caption> * Moa.define('base', {}); // new type declaration * Moa.define('base', null); // delete type declaration * * @example <caption>Declaration <code>$base</code> closure</caption> * var childItem, * base = Moa.define('base', function ($base) { * // $base - undefined * return { * $ctor: function (name) { * this.name = name; * }, * getName: function() { * return this.name; * } * }; * }), * child = Moa.define('child', function ($base) { * // $base - reference to 'base' type * return { * $extend: 'base', * $ctor: function (name, age) { * this.age = age; * $base.$ctor.call(this, name); * }, * // override base implementation * getName: function() { * return 'Child: ' + $base.getName.call(this); * }, * getAge: function () { * return this.age; * } * }; * }); * * @example <caption>Using instance</caption> * childItem = new child('Pet', 7); * childItem.getName(); // 'Child: Pet' * childItem.getAge(); // 7 * * @example <caption>Declaration static methods</caption> * var baseCtor, item, * strMix = function () { * this.add = function () { * return (this.a.toString() + this.b.toString()); * }; * } * base = { * $ctor: function () { * }, * $static: { * // Also you can declare static mixins in usual way * $mixin: { * str: 'strMix' * }, * getMsg: function () { * return 'Static!'; * }, * a: 15, * b: 17 * } * }; * Moa.mixin('strMix', strMix); * Moa.define('base', base); * * @example <caption>Using static methods</caption> * baseCtor = Moa.define('base'); * baseCtor.getMsg(); // 'Static!' - static method * baseCtor.add(); // '15' + '17' => '1517' - static mixin * Ctor.str.add.call(Ctor); // '1517' * * @example <caption>Declaration singleton</caption> * var itemA, itemB, ItemC, * singeltonConstructor = Moa.define('singleExample', { * $single: true, * $ctor: function () { * this.name = 'Moa'; * }, * getName: function () { * return this.name; * } * }) * * @example <caption>Using singleton</caption> * // Unfortunately it can not have constructor parameters * itemA = new singeltonConstructor(); * itemB = singeltonConstructor(); * itemC = singeltonConstructor.getInstance(); * // itemA equal itemB equal itemC */ define: function (type, definition) { var mapObj, baseType, base, len = arguments.length; switch (len) { case 1: mapObj = map[type]; throwWrongType(mapObj, type); break; case 2: switch (typeof definition) { case 'function': baseType = definition().$extend; if (baseType !== undef) { base = map[baseType]; throwWrongType(base, baseType); mapObj = build(type, base, definition(base.$base)); } else { mapObj = build(type, undef, definition(undef)); } break; case 'object': if (definition !== null) { baseType = definition.$extend; if (baseType !== undef) { base = map[baseType]; throwWrongType(base, baseType); } mapObj = build(type, base, definition); } else { delete map[type]; return undef; } break; default: throwWrongParamsErr('define', 'definition'); } map[type] = mapObj; map[type].$di = resolveDeclaration(type, mapObj.$di); break; default: throwWrongParamsErr('define'); } return mapObj.$ctor; }, /** * Declaration of dependency injection behavior * @typedef {object} InjectionConf * @property {string} type - name of type for injection. * Not available for $current in {@link DiConf} * @property {string} instance - Injected instance. * Values: 'item' or 'ctor'. Default value: 'item'. * @property {string} lifestyle - Life style for 'item' instance. Not used for 'ctor'. * Values: 'transient' or 'singleton'. Default value: 'transient'. */ /** * Configuration of dependency injection. Used as $di parameter in type declaration. * @typedef {object} DiConf * @property {object} [$current] - set default injection behavior for declared type * @property {object} [$ctor] - literal declare types that inject to constructor * @property {object} [$prop] - literal declare types that inject to instance properties * @property {object} [$proto] - literal declare types that inject to prototype of instance properties. * BE CAREFUL! It resolved one time after use 'resolve' method and override exist properties and methods in prototype. * Resolved properties and methods available in prototype of constructor type for all places where constructor uses ('define' method for example). * @property {*} - properties that injected as instance properties. All string values try to resolve as declared types * @example * { * $ctor: { * fieldA: { * type: 'typeA', * instance: 'item', * lifestyle: 'transient' * }, * field: 'typeB' // try to resolve as 'typeB' otherwise use as a string * }, * $prop: { * propA: 'typeA' // resolved like fieldA to instance field * }, * $proto: { * protoProp: { // resolve constructor of 'typeB' to instance prototype * type: 'typeB', * instance: 'ctor' // if instance is 'item' it has lifestyle as 'singleton' * } * }, * propC: { // resolve the same instance of 'typeC' for every instance in $prop literal * type: 'typeA', * instance: 'item', * lifestyle: 'singleton' * }, * prop: 2315 // copy number field to resolved instance * } */ /** * Resolve new instance of type with field and constructor injection. * Resolving logic based on $di configuration of type declaration. * @method resolve * @param {string} type - name of type * @param {object} [paramsObj] - constructor parameter for resolved type * @return {object} instance of type */ resolve: function (type, paramsObj) { var item, //depthRecursion = 64, cntRecursion = 0, mapObj = map[type], len = arguments.length, fnResolveListConf = function (target, config, fnResolveObjConf) { var prop, propValue; for (prop in config) { propValue = config[prop]; if (typeof propValue === 'object') { target[prop] = fnResolveObjConf(propValue, fnResolveListConf); } else { target[prop] = propValue; } } return target; }, createItem = function (declaration, obj, fnResolveObjConf, cParams) { var item, conf, proto; cParams = cParams || {}; conf = declaration.$ctor; if (conf) { cParams = fnResolveListConf(cParams, conf, fnResolveObjConf); } conf = declaration.$proto; if (conf) { if (!conf.resolved) { proto = fnResolveListConf({}, conf, fnResolveObjConf); fastExtend(obj.$ctor.prototype, proto); conf.resolved = true; } } item = new obj.$ctor(cParams); conf = declaration.$prop; if (conf) { item = fnResolveListConf(item, conf, fnResolveObjConf); } return item; }, fnResolveObjConf = function (declaration, ctorParams) { var resolvedObj, current = declaration.$current; /*========================================================== if you have problem with IoC, just uncomment 3 rows bellow and second row in 'resolve' function =========================================================*/ // cntRecursion += 1; // if (cntRecursion > depthRecursion) { // throw new Error('Loop of recursion', 'moa'); // } if (current) { resolvedObj = map[current.type]; } else { current = declaration; resolvedObj = map[current.type]; if (resolvedObj) { declaration = resolvedObj.$di; } else { return declaration; } } switch (current.instance) { case 'item': switch (current.lifestyle) { case 'transient': item = createItem(declaration, resolvedObj, fnResolveObjConf, ctorParams); break; case 'singleton': if (!current.item) { current.item = createItem(declaration, resolvedObj, fnResolveObjConf, ctorParams); } item = current.item; break; default: throwWrongParamsErr('resolve', type + '::$di::$current::lifestyle'); } break; case 'ctor': item = resolvedObj.$ctor; break; default: throwWrongParamsErr('resolve', type + '::$di::$current::instance'); } return item; }; if (len !== 1 && len !== 2) { throwWrongParamsErr('resolve'); } throwWrongType(mapObj, type); item = fnResolveObjConf(mapObj.$di, paramsObj); return item; }, /** * Declare mixin * @method mixin * @param {string} mixType - name of mixin type * @param {function} definition - implementation of behavior for mixin. * If it is null - delete declared mixin * * @example <caption>Declaration mixin</caption> * var numMix = function () { * this.add = function () { * return (this.a + this.b); * }; * this.sub = function () { * return (this.a - this.b); * }; * this.mul = function () { * return (this.a * this.b); * }; * }; * Moa.mixin('numMix', numMix); * * @example <caption>Using mixin</caption> * var item, Ctor, * base = { * $ctor: function (a, b) { * this.a = a; * this.b = b; * }, * $mixin: { * nummix: 'numMix' * }, * mul: function () { * return 'a*b=' + this.nummix.mul.call(this); * } * }; * Ctor = Moa.define('base', base); * item = new Ctor(3, 4); * item.add(); // 7 * item.mul(); // 'a*b=12' * * @example <caption>Multiple mixins example</caption> * var Ctor, item, * base = { * $ctor: function (a, b) { * this.a = a; * this.b = b; * }, * $mixin: { * num: 'numMix', * str: 'strMix' * } * }, * numMix = function () { * this.add = function () { * return (this.a + this.b); * }; * }, * strMix = function () { * this.add = function () { * return (this.a.toString() + this.b.toString()); * }; * }; * Moa.mixin('numMix', numMix); * Moa.mixin('strMix', strMix); * Ctor = Moa.define('base', base); * item = new Ctor(10, 12); * item.add(); // '1012' * item.num.add.call(item); // 22 * item.str.add.call(item); //'1012' * * @example <caption>Delete mixin declaration</caption> * Moa.mixin('mix', function () {}); // new mixin declaration * Moa.mixin('mix', null); // delete mixin declaration * * @example <caption>Static mixin declaration</caption> * var base = { * $mixin: { * num: 'numMix' * }, * $static: { * $mixin: { * str: 'strMix' * } * } * } */ mixin: function (mixType, definition) { if (definition !== null) { if (typeof definition !== 'function') { throwWrongParamsErr('mixin', 'definition'); } mixins[mixType] = definition; } else { delete mixins[mixType]; } }, /** * Get all available types and mixins * @method getRegistry * @return {object} object with arrays declared types and mixins * @example * { * type: ['type1', 'type2', ...], * mixin: ['mixin1', 'mixin2', ...] * } */ getRegistry: function () { var iterate = function (obj) { var prop, arr = []; for (prop in obj) { arr.push(prop); } return arr; }; return { type: iterate(map), mixin: iterate(mixins) }; }, /** * Get internal information about type * @method getTypeInfo * @param {string} type - name of type * @return {object} object with information about type, * base type, applied mixins and configuration for dependency injection * @example * { * $type: 'child', * $basetype: 'base', * $mixin: { * mixA: 'mixinA', * mixB: 'mixinB' * }, * $di: { * $current: { * type: 'child', * instance: 'item', * lifestyle: 'transient' * }, * $prop: { * a: { * type: 'base', * instance: 'item', * lifestyle: 'transient' * }, * b: 'child' * } * } * } */ getTypeInfo: function (type) { var result, mapObj = map[type]; throwWrongType(mapObj, type); result = extend({}, mapObj); delete result.$ctor; delete result.$base; delete result.$di; result.$di = JSON.parse(JSON.stringify(mapObj.$di)); return result; } }; // Return as AMD module or attach to head object if (typeof define !== 'undefined') { define([], function () { return Moa; }); } else if (typeof window !== 'undefined') { window.Moa = Moa; } else { module.exports = Moa; } }());