From a023a87f3423ef0c013d0b28ef7868624b61ef19 Mon Sep 17 00:00:00 2001 From: Thorsten Suckow-Homberg Date: Fri, 2 Dec 2022 00:19:06 +0100 Subject: [PATCH] refactor: add proxies for ioc refs conjoon/extjs-app-webmail#255 --- src/ioc/Bindings.js | 102 +++++++++++++ src/ioc/Proxy.js | 86 +++++++++++ src/ioc/sencha/AbstractProxy.js | 175 ++++++++++++++++++++++ src/ioc/sencha/CreateProxy.js | 101 +++++++++++++ src/ioc/sencha/FactoryProxy.js | 132 ++++++++++++++++ tests/groups.config.js | 13 ++ tests/src/ioc/BindingsTest.js | 67 +++++++++ tests/src/ioc/sencha/AbstractProxyTest.js | 175 ++++++++++++++++++++++ tests/src/ioc/sencha/CreateProxyTest.js | 112 ++++++++++++++ tests/src/ioc/sencha/FactoryProxyTest.js | 84 +++++++++++ 10 files changed, 1047 insertions(+) create mode 100644 src/ioc/Bindings.js create mode 100644 src/ioc/Proxy.js create mode 100644 src/ioc/sencha/AbstractProxy.js create mode 100644 src/ioc/sencha/CreateProxy.js create mode 100644 src/ioc/sencha/FactoryProxy.js create mode 100644 tests/src/ioc/BindingsTest.js create mode 100644 tests/src/ioc/sencha/AbstractProxyTest.js create mode 100644 tests/src/ioc/sencha/CreateProxyTest.js create mode 100644 tests/src/ioc/sencha/FactoryProxyTest.js diff --git a/src/ioc/Bindings.js b/src/ioc/Bindings.js new file mode 100644 index 0000000..e52a916 --- /dev/null +++ b/src/ioc/Bindings.js @@ -0,0 +1,102 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * Object for managing bindings used by the ioc-container. + * This class should not be used directly. Instead, the owning ioc-container should manage it. + * + * @example + * const bindings = Ext.create("coon.core.ioc.Bindings", {}); + * const data = bindings.getData(); // {} + * + * bindigs.merge({"com.acme": {requestConfigurator: "coon.core.data.request.Configurator"}}); + * bindings.getData(); // {"com.acme": {requestConfigurator: "coon.core.data.request.Configurator"}} + * + * bindigs.merge({"com.acme.Class": {requestConfigurator: "com.acme.RequestConfigurator"}}); + * bindings.getData(); // { + * // "com.acme.Class": {requestConfigurator: "com.acme.RequestConfigurator"}, + * // "com.acme": {requestConfigurator: "coon.core.data.request.Configurator"} + * // } + * + */ +Ext.define("coon.core.ioc.Bindings", { + + + /** + * @type {Object} data + */ + constructor (data) { + this.data = {}; + this.merge(data || {}); + }, + + + getData () { + return this.data; + }, + + + /** + * Merge bindings. + * Existing data is being overwritten with new data. + * Will sort the entries after data has been merged to make sure entries with more + * namespace information appear first, and more general entries are found at the end ot the + * data-container. + * + * @param {Object} data An object containing key-value pairs, with keys representing class-names/namespaced, + * and values representing types or type configuration that need to get resolved by the implemention ioc.container + */ + merge (data) { + + const me = this; + + me.data = Object.assign(me.data, data); + + me.data = Object.fromEntries(Object.entries(me.data).sort((lft, rt) => { + + lft = lft[0]; + rt = rt[0]; + + let lftParts = lft.split("."); + let rtParts = rt.split("."); + + if(lftParts.length < rtParts.length) { + return 1; + } + + if(lftParts.length > rtParts.length) { + return -1; + } + + return 0; + + + })); + + return me.data; + } + +}); diff --git a/src/ioc/Proxy.js b/src/ioc/Proxy.js new file mode 100644 index 0000000..2df22f5 --- /dev/null +++ b/src/ioc/Proxy.js @@ -0,0 +1,86 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * + * + */ +Ext.define("coon.core.ioc.Proxy", { + + + requires: [ + // @define "l8" + "l8", + "Ext.Factory", + "coon.core.ioc.sencha.FactoryProxy", + "coon.core.ioc.sencha.CreateProxy" + ], + + /** + * @type {coon.core.ioc.sencha.FactoryProxy} FactoryProxy + * @private + */ + + /** + * @type {coon.core.ioc.sencha.CreateProxy} CreateProxy + * @private + */ + + /** + * @type {Boolean} booted + * @private + */ + + + /** + * + * @param {coon.core.ioc.Bindings} bindings + */ + constructor ({bindings}) { + const me = this; + + me.boot(bindings || {}); + }, + + + /** + * @private + */ + boot (bindings) { + const me = this; + + if (!me.booted) { + + me.factoryProxy = Ext.create("coon.core.ioc.sencha.FactoryProxy", {bindings}); + me.createProxy = Ext.create("coon.core.ioc.sencha.CreateProxy", {bindings}); + + Ext.Factory = new Proxy(Ext.Factory, me.factoryProxy); + Ext.create = new Proxy(Ext.create, me.createProxy); + me.booted = true; + } + } + +}); diff --git a/src/ioc/sencha/AbstractProxy.js b/src/ioc/sencha/AbstractProxy.js new file mode 100644 index 0000000..e8fa2a4 --- /dev/null +++ b/src/ioc/sencha/AbstractProxy.js @@ -0,0 +1,175 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * AbstractProxy providing functionality for working with Sencha Ext JS specific classes. + * Implementing classes can delegate calls that involve resolving dependencies to resolveDependencies() + * + * @abstract + */ +Ext.define("coon.core.ioc.sencha.AbstractProxy", { + + requires: [ + // @define "l8" + "l8", + "coon.core.ioc.Bindings" + ], + + + /** + * @type {coon.core.ioc.Bindings} bindings + * @private + */ + + + /** + * Constructor. + * + * @param {coon.core.ioc.Bindings} bindings + * + * @throws if bindings is not an instance of coon.core.ioc.Bindings + */ + constructor (bindings) { + if (!(bindings instanceof coon.core.ioc.Bindings)) { + throw new Error("\"bindings\" must be an instance of coon.core.ioc.Bindings"); + } + this.bindings = bindings; + }, + + + /** + * Tries to find a binding configuration for "targetClass" by matching it against + * a list of available namespaces. If an entry is found, the entry must be configured + * as {[requiredType]: [specific]}. If such an entry exists, "specific" will be + * returned. If no entry is found, "requiredType" is returned. + * + * @example + * + * // available bindings + * // { + * // "com.acme.data": { + * // "org.project.impl.IClass": "org.project.impl.Specific" + * // } + * proxy.resolveToSpecific("com.acme.data.message.Editor", "org.project.impl.IClass"); + * // returns "org.project.impl.IClass" + * + * @param {String} targetClass + * @param {String} defaultrequiredTypeClass + * + * @returns {String} + * + * @private + */ + resolveToSpecific (targetClass, requiredType) { + + const + me = this, + targets = Object.entries(me.bindings.getData()); + + let cfg = targets.filter(([target]) => target.toLowerCase() === targetClass.toLowerCase()); + + if (!cfg.length) { + cfg = targets.filter(([target]) => targetClass.toLowerCase().indexOf(target.toLowerCase()) === 0); + if (!cfg.length) { + return requiredType; + } + } + + const availableClasses = Object.entries(cfg[0][1]); + let specific; + + availableClasses.some(([cfgClassName, specInst]) => { + + if (cfgClassName.toLowerCase() === requiredType.toLowerCase()) { + specific = specInst; + return true; + } + + }); + + return specific || requiredType; + }, + + + /** + * Will resolve dependencies available with "requires" for "targetClass". + * "requires" must be an object where the keys are property-names defined by "targetClass", and the values + * are type hints in form of class names. + * + * @example + * // "com.acme.BaseProxy" has a dependency to "coon.core.data.request.Configurator", + * // with its "requestConfigurator" property + * // const requires = {"requestConfigurator": "coon.core.data.request.Configurator"}; + * const resolved = resolveDependencies("com.acme.BaseProxy", requires); + * // resolveDependencies will delegate to "resolveToSpecific()" to see if a more specific type was bound + * // to com.acme.BaseProxy`s "requestConfigurator" property, otherwise, it will return an instance of + * // "coon.core.data.request.Configurator" + * console.log(resolved); // {"requestConfigurator": INSTANCE_OF[coon.core.data.request.Configurator]} + * + * @param {String} targetClass The class name for which the dependencies get resolved + * @param {Object} requires Definitions of the dependencies required by forClass. + * + * @returns {{}} + * + * @see resolveToSpecific + * + * @throws if a resolved class name cannot be instantiated + */ + resolveDependencies ( targetClass, requires) { + + const + me = this, + dependencies = Object.entries(requires), + deps = {}, + scope = me.getScope(); + + dependencies.forEach(([prop, requiredType]) => { + if (!deps[prop]) { + const specific = me.resolveToSpecific(targetClass, requiredType); + + if (!l8.unchain(specific, scope)) { + throw new Error(`${specific} bound to ${targetClass}.${prop}, but was not found in the scope available`); + } + + deps[prop] = Ext.create(specific); + } + }); + + return deps; + }, + + + /** + * Scope where the defined classes should be defined. + * Defaults to the "window"-object. + * @private + */ + getScope () { + return window; + } + + +}); diff --git a/src/ioc/sencha/CreateProxy.js b/src/ioc/sencha/CreateProxy.js new file mode 100644 index 0000000..b7bcecb --- /dev/null +++ b/src/ioc/sencha/CreateProxy.js @@ -0,0 +1,101 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * Handler for proxying Ext.create. + * + * @example + * + * // ioc-bindings available with "bindings" + * Ext.create = new Proxy( + * Ext.create, Ext.create("coon.core.ioc.sencha.CreateProxy", bindings) +* ); + * + */ +Ext.define("coon.core.ioc.sencha.CreateProxy", { + + extend: "coon.core.ioc.sencha.AbstractProxy", + + requires: [ + // @define "l8" + "l8" + ], + + /** + * meta property with a class that provides information about + * dependencies. Defaults to "require". + * @type {String} reqioreProperty + * @private + */ + requireProperty: "require", + + /** + * Apply handler for proxying calls to Ext.create(). + * Checks if the class name available with argumentsList[0] is defined + * and will then resolve dependencies with the help of resolveDependencies(), + * if the arguments found in argumentsList[1] do not already contain + * configurations for these dependencies. + * + * @example + * // acme.Request.require = {requestor: "acme.RequestConfigurator"} + * proxy.apply({}, {}, ["acme.Request", {}]; + * // config has no "requestor" configured with arguments, will resolve + * // dependencies using available bindings by calling resolveDependencies() + * + * proxy.apply({}, {}, ["acme.Request", {requestor: {}]; + * // config has "requestor" configured with arguments, will not resolve + * // dependencies + * + * @param target + * @param thisArg + * @param argumentsList + * @returns {*} + */ + apply (target, thisArg, argumentsList) { + + const me = this; + + let [className, cfg] = argumentsList; + + if (l8.isString(className)) { + const cls = Ext.ClassManager.get(className); + if (cls) { + const requireCfg = cls[me.requireProperty]; + if (l8.isObject(requireCfg)) { + cfg = Object.assign( + cfg || {}, + me.resolveDependencies(Ext.ClassManager.getName(cls), requireCfg) + ); + } + } + } + + argumentsList[1] = cfg; + + return Reflect.apply(target, thisArg, argumentsList); + } + +}); diff --git a/src/ioc/sencha/FactoryProxy.js b/src/ioc/sencha/FactoryProxy.js new file mode 100644 index 0000000..3d6e620 --- /dev/null +++ b/src/ioc/sencha/FactoryProxy.js @@ -0,0 +1,132 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * Handler for proxying Ext.Factory["proxy", "store", "controller", ...] + * + * @example + * + * // ioc-bindings available with "bindings" + * Ext.Factory = new Proxy( + * Ext.Factory, Ext.create("coon.core.ioc.sencha.FactoryProxy", bindings) + * ); + * + */ +Ext.define("coon.core.ioc.sencha.FactoryProxy", { + + extend: "coon.core.ioc.sencha.AbstractProxy", + + requires: [ + // @define "l8" + "l8" + ], + + /** + * meta property with a class that provides information about + * dependencies. Defaults to "require". + * @type {String} reqioreProperty + * @private + */ + requireProperty: "require", + + + /** + * Apply handler for proxying calls to Ext.Factory["proxy", "store", "controller", ...] + * factory methods are usually invoked with aliases instead of class names which are + * then resolved by Ext JS' underlying class system. The handler will utilize this + * functionality and assume the alias is available with argumentsList[0], + * inspect the "requireProperty" available with the resolved class and then + * resolve dependencies with the help of resolveDependencies(), + * if the arguments found in argumentsList[1] do not already contain + * configurations for these dependencies. + * + * @example + * // acme.Request.require = {requestor: "acme.RequestConfigurator"} + * // alias: "acme-request" + * proxy.apply({}, {}, ["acme-request", {}]; + * // config has no "requestor" configured with arguments, will resolve + * // dependencies using available bindings by calling resolveDependencies() + * + * proxy.apply({}, {}, ["acme-request", {requestor: {}]; + * // config has "requestor" configured with arguments, will not resolve + * // dependencies + * + * @param target + * @param thisArg + * @param argumentsList + * @returns {*} + */ + apply (target, thisArg, argumentsList) { + + const me = this; + + let cfg = argumentsList[0] || {}; + + // cfg : "alias", {type: "alias"} + const type = cfg.type ? cfg.type : (l8.isString(cfg) ? cfg : undefined); + + if (type && target.instance?.aliasPrefix) { + const cls = Ext.ClassManager.getByAlias(`${target.instance.aliasPrefix}${type}`); + if (cls.require) { + cfg = Object.assign( + cfg, + me.resolveDependencies(cls.require, Ext.ClassManager.getName(cls)) + ); + cfg.type = cfg.type || type; + argumentsList[0] = cfg; + } + } + + return Reflect.apply(target, thisArg, argumentsList); + }, + + + /** + * Will proxy the property with *this* as the handler if the property + * represents a method. + * + * @param target + * @param prop + * @param receiver + * @returns {any} + */ + get (target, prop, receiver) { + + if (l8.isFunction(target[prop])) { + return new Proxy(target[prop], this.handler()); + } + + return Reflect.get(...arguments); + }, + + + /** + * @private + */ + handler () { + return this; + } +}); diff --git a/tests/groups.config.js b/tests/groups.config.js index c982fb2..5906161 100644 --- a/tests/groups.config.js +++ b/tests/groups.config.js @@ -178,6 +178,19 @@ export default [{ "src/data/writer/FormDataTest.js" ] }] +}, { + group: "ioc", + items: [{ + "group": "sencha", + "items": [ + "src/ioc/sencha/AbstractProxyTest.js", + "src/ioc/sencha/CreateProxyTest.js", + "src/ioc/sencha/FactoryProxyTest.js" + ] + }, + "src/ioc/BindingsTest.js" + ] + }, { group: "env", items: [ diff --git a/tests/src/ioc/BindingsTest.js b/tests/src/ioc/BindingsTest.js new file mode 100644 index 0000000..6108a09 --- /dev/null +++ b/tests/src/ioc/BindingsTest.js @@ -0,0 +1,67 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +StartTest(t => { + + const superclass = "coon.core.ioc.Bindings"; + const create = data => Ext.create("coon.core.ioc.Bindings", data); + + t.it("sanity", t => { + const b = create(); + t.isInstanceOf(b, superclass); + + t.expect(b.getData()).toEqual({}); + }); + + t.it("data", t => { + const b = create({"namespace": {"foo": "bar"}}); + t.isInstanceOf(b, superclass); + + t.expect(b.getData()).toEqual({"namespace": {"foo": "bar"}}); + }); + + + t.it("merge()", t => { + const b = create(); + + t.expect(b.merge({"com.acme": {requestConfigurator: "coon.core.data.request.Configurator"}})).toBe(b.getData()); + t.expect(b.getData()).toEqual({"com.acme": {requestConfigurator: "coon.core.data.request.Configurator"}}); + + b.merge({"com.acme.Class": {requestConfigurator: "com.acme.RequestConfigurator"}}); + t.expect(b.getData()).toEqual({ + "com.acme.Class": {requestConfigurator: "com.acme.RequestConfigurator"}, + "com.acme": {requestConfigurator: "coon.core.data.request.Configurator"} + }); + + b.merge({"com.acme.Class": {requestConfigurator: "moreSpecific"}}); + + t.expect(b.getData()).toEqual({ + "com.acme.Class": {requestConfigurator: "moreSpecific"}, + "com.acme": {requestConfigurator: "coon.core.data.request.Configurator"} + }); + + }); + +}); diff --git a/tests/src/ioc/sencha/AbstractProxyTest.js b/tests/src/ioc/sencha/AbstractProxyTest.js new file mode 100644 index 0000000..9a09177 --- /dev/null +++ b/tests/src/ioc/sencha/AbstractProxyTest.js @@ -0,0 +1,175 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +StartTest(t => { + + const superclass = "coon.core.ioc.sencha.AbstractProxy"; + const className = "coon.core.ioc.sencha.AbstractProxy"; + + const create = cfg => { + + if (!cfg.bindings) { + cfg.bindings = createBindings(); + } + + return Ext.create(className, cfg.bindings); + }; + + const createBindings = data => Ext.create("coon.core.ioc.Bindings", data); + + t.it("sanity", t => { + const + bindings = createBindings(), + proxy = create({bindings}); + + t.isInstanceOf(proxy, superclass); + t.expect(proxy.bindings).toBe(bindings); + }); + + + t.it("constructor throws exception", t => { + + try { + Ext.create(className); + t.fail(); + } catch (e) { + t.expect(e.message).toContain("must be an instance of"); + } + }); + + + t.it("resolveToSpecific()", t => { + + const bindings = createBindings({ + "com.acme.data": { + "org.project.impl.IClass": "org.project.impl.Specific" + }, + "com.acme.data.addendum": { + "org.project.impl.IClass": "org.project.impl.AddendumSpecific" + } + }); + + const proxy = create({bindings}); + + const tests = [{ + // class matching namespace returns specific + targetClass: "com.acme.data.message.Editor", + requiredType: "org.project.impl.IClass", + result: "org.project.impl.Specific" + }, { + // class not matching namespace returns default + targetClass: "message.Editor", + requiredType: "org.project.impl.IClass", + result: "org.project.impl.IClass" + }, { + // namespace more narrow + // class matching namespace returns specific + targetClass: "com.acme.data.addendum.message.Editor", + requiredType: "org.project.impl.IClass", + result: "org.project.impl.AddendumSpecific" + }, { + // class matching namespace, no specific configured + targetClass: "com.acme.data.message.Editor", + requiredType: "missing.class", + result: "missing.class" + }]; + + tests.map(({targetClass, requiredType, result}) => { + + t.expect(proxy.resolveToSpecific( + targetClass, + requiredType + )).toBe(result); + + }); + }); + + + t.it("getScope()", t=> { + + const proxy = create({}); + t.expect(proxy.getScope()).toBe(window); + + }); + + + t.it("resolveDependencies()", t => { + + const proxy = create({}); + + const clsContainer = { + "resolved_configurator": {}, + "view_model_specific": {} + }; + + const resolveToSpecific = (targetClass, requiredType) => { + + if (targetClass === "com.acme.BaseProxy") { + if (requiredType === "coon.core.data.request.Configurator") { + return "resolved_configurator"; + } + if (requiredType === "view.Model") { + return "view_model_specific"; + } + if (requiredType === "type") { + return "notDefined"; + } + + } + + if (targetClass === "com.acme.BaseProxy" && requiredType === "coon.core.data.request.Configurator") { + return "resolved"; + } + + }; + + const + resolveToSpecificSpy = t.spyOn(proxy, "resolveToSpecific").and.callFake(resolveToSpecific), + createSpy = t.spyOn(Ext, "create").and.callFake(args => args), + scopeSpy = t.spyOn(proxy, "getScope").and.callFake(() => clsContainer), + deps = proxy.resolveDependencies( + "com.acme.BaseProxy", + {"requestConfigurator": "coon.core.data.request.Configurator", + "viewModel": "view.Model"} + ); + + t.expect(deps).toEqual({ + "requestConfigurator": "resolved_configurator", + "viewModel": "view_model_specific" + }); + + // exception, class not loaded + try { + proxy.resolveDependencies("com.acme.BaseProxy", {"prop": "type"}); + t.fail(); + } catch (e) { + t.expect(e.message).toContain("was not found in the scope"); + } + + [resolveToSpecificSpy, createSpy, scopeSpy].map(spy => spy.remove()); + }); + + +}); diff --git a/tests/src/ioc/sencha/CreateProxyTest.js b/tests/src/ioc/sencha/CreateProxyTest.js new file mode 100644 index 0000000..d2eea5c --- /dev/null +++ b/tests/src/ioc/sencha/CreateProxyTest.js @@ -0,0 +1,112 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +StartTest(t => { + + const superclass = "coon.core.ioc.sencha.AbstractProxy"; + const className = "coon.core.ioc.sencha.CreateProxy"; + + const create = cfg => { + + if (!cfg.bindings) { + cfg.bindings = createBindings(); + } + + return Ext.create(className, cfg.bindings); + }; + + const createBindings = data => Ext.create("coon.core.ioc.Bindings", data); + + t.it("sanity", t => { + const + bindings = createBindings(), + proxy = create({bindings}); + + t.isInstanceOf(proxy, superclass); + t.expect(proxy.bindings).toBe(bindings); + }); + + + t.it("apply()", t => { + + const proxy = create({}); + + let defaultClass = {}; + + const classManagerSpy = t.spyOn(Ext.ClassManager, "get").and.callFake(() => defaultClass); + const getNameSpy = t.spyOn(Ext.ClassManager, "getName").and.callFake(() => "className"); + const reflectSpy = t.spyOn(Reflect, "apply").and.callFake(() => "reflect"); + const resolveDependenciesSpy = t.spyOn(proxy, "resolveDependencies").and.callFake(() => ({"prop": "resolved"})); + const target = {}, thisArg = {}; + + const assertReflectSpy = (thirdArg) => { + t.expect(reflectSpy.calls.mostRecent().args[0]).toBe(target); + t.expect(reflectSpy.calls.mostRecent().args[1]).toBe(thisArg); + + if (thirdArg !== undefined) { + t.expect(reflectSpy.calls.mostRecent().args[2]).toEqual(thirdArg); + } + }; + + + // no string passed as 1 argument + t.expect(proxy.apply(target, thisArg, [{}])).toBe(reflectSpy.calls.mostRecent().returnValue); + t.expect(resolveDependenciesSpy.calls.count()).toBe(0); + assertReflectSpy(); + + + // class cannot be resolved + defaultClass = undefined; + t.expect(proxy.apply(target, thisArg, ["foo"])).toBe(reflectSpy.calls.mostRecent().returnValue); + t.expect(resolveDependenciesSpy.calls.count()).toBe(0); + assertReflectSpy(); + + + // target class has no requireCfg + defaultClass = {}; + t.expect(proxy.apply(target, thisArg, ["foo"])).toBe(reflectSpy.calls.mostRecent().returnValue); + t.expect(resolveDependenciesSpy.calls.count()).toBe(0); + assertReflectSpy(); + + + // target class requireCfg + const requireConfig = {"config": {}}; + defaultClass = {}; + defaultClass[proxy.requireProperty] = requireConfig; + + t.expect(proxy.apply(target, thisArg, ["foo", {width: 800, height: 600}])).toBe(reflectSpy.calls.mostRecent().returnValue); + t.expect(resolveDependenciesSpy.calls.count()).toBe(1); + t.expect(resolveDependenciesSpy.calls.mostRecent().args[0]).toBe( + getNameSpy.calls.mostRecent().returnValue + ); + t.expect(resolveDependenciesSpy.calls.mostRecent().args[1]).toBe(requireConfig); + assertReflectSpy(["foo", {width: 800, height: 600, prop: "resolved"}]); + + [reflectSpy, resolveDependenciesSpy, getNameSpy, classManagerSpy].map(spy => spy.remove()); + + }); + + +}); diff --git a/tests/src/ioc/sencha/FactoryProxyTest.js b/tests/src/ioc/sencha/FactoryProxyTest.js new file mode 100644 index 0000000..19a407e --- /dev/null +++ b/tests/src/ioc/sencha/FactoryProxyTest.js @@ -0,0 +1,84 @@ +/** + * coon.js + * extjs-lib-core + * Copyright (C) 2022 Thorsten Suckow-Homberg https://github.com/coon-js/extjs-lib-core + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +StartTest(t => { + + const superclass = "coon.core.ioc.sencha.AbstractProxy"; + const className = "coon.core.ioc.sencha.FactoryProxy"; + + const create = cfg => { + + if (!cfg.bindings) { + cfg.bindings = createBindings(); + } + + return Ext.create(className, cfg.bindings); + }; + + const createBindings = data => Ext.create("coon.core.ioc.Bindings", data); + + t.it("sanity", t => { + const + bindings = createBindings(), + proxy = create({bindings}); + + t.isInstanceOf(proxy, superclass); + t.expect(proxy.bindings).toBe(bindings); + }); + + + // private + t.it("handler()", t => { + const proxy = create({}); + t.expect(proxy.handler()).toBe(proxy); + }); + + + t.it("get()", t => { + + const proxy = create({}); + + const target = {foo: function (){}}; + const handler = {apply: function () {}}; + + proxy.handler = () => handler; + + const applySpy = t.spyOn(handler, "apply").and.callThrough(); + + target.foo(); + t.expect(applySpy.calls.count()).toBe(0); + target.foo = proxy.get(target, "foo"); + target.foo(); + t.expect(applySpy.calls.count()).toBe(1); + }); + + + t.it("apply()", t => { + + t.fail(); + }); + + +});