Home Reference Source Test Repository

src/index.js

"use strict";

/**
 * @ignore
 */
let Decorator = require("./decorator.js").Decorator;

/**
 * @access public
 */
class Jsimple {
    /**
     * Builds a new Jsimple instance
     */
    constructor() {
        /**
         * @type {Map<String, *>}
         * @access protected
         */
        this.values = new Map();

        /**
         * @type {Map<String, Set>}
         * @access protected
         */
        this.tagmap = new Map();

        /**
         * @type {Map<String, *>}
         * @access protected
         */
        this.shared = new Map();

        /**
         * @type {Set<String>}
         * @access protected
         */
        this.frozen = new Set();

        Object.freeze(this);

        Decorator.setJsimple(this);
    }

    /**
     *
     * @param {Array<String>|Function(deps: *, container: Jsimple): *} deps   List of dependencies to inject or executable function
     * @param {Function(deps: *, container: Jsimple): *}               [code] Executable function
     *
     * @returns {*} Result of executing the provided function as code
     */
    use(deps, code) {
        if (deps.constructor.name === "Array") {
            deps = deps || [];
            deps.forEach((value, key) => deps[key] = this.get(value));
            deps.push(this);

            return code.apply(null, deps);
        }

        if (typeof deps === "function") {
            return deps(this);
        } else {
            return code(this);
        }
    }

    /**
     * Sets a parameter or an object factory
     *
     * @param {String}                           name   The unique identifier for the parameter or factory
     * @param {*|Function(container: Jsimple): *} value  The parameter value or a factory function
     * @param {Array<String>}                    [tags] An array of tags to associate to the parameter or factory
     *
     * @returns {Jsimple} The current Jsimple instance
     */
    define(name, value, tags) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.define must be a string identifier")
        }

        if (this.frozen.has(name)) {
            throw new Error("Cannot override an already executed factory or fetched service");
        }

        if (this.values.has(name)) {
            (this.values.get(name).tags || []).forEach(tag => {
                this.tagmap.get(tag).delete(name);
            });

            this.values.delete(name);
        }

        if (typeof value !== "function") {
            this.values.set(name, () => value);
        } else {
            this.values.set(name, value);
        }

        this.values.get(name).tags = tags || [];
        this.values.get(name).tags.forEach(tag => {
            if (this.tagmap.has(tag) === false) {
                this.tagmap.set(tag, new Set());
            }

            this.tagmap.get(tag).add(name);
        });

        if (this.shared.has(name)) {
            this.shared.delete(name);
        }

        return this;
    }

    /**
     *
     * @param {String}                         name   The unique identifier for the factory
     * @param {Function(container: Jsimple): *} code   The executable factory function
     * @param {Array<String>}                  [tags] An array of tags to associate to the factory
     *
     * @returns {Jsimple} The current Jsimple instance
     */
    share(name, code, tags) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.share must be a string identifier")
        }

        if (typeof code !== "function") {
            throw new Error("Argument #2 passed to Jsimple.share must be a function")
        }

        return this.define(
            name,
            jsimple => {
                if (jsimple.shared.has(name) === false) {
                    jsimple.shared.set(name, code(jsimple));
                }

                let instance = jsimple.shared.get(name);

                this.frozen.add(name);

                return instance;
            },
            tags || []
        );
    }

    /**
     *
     * @param {String}                         name   The unique identifier for the factory
     * @param {Function(container: Jsimple): *} code   The executable factory function
     * @param {Array<String>}                  [tags] An array of tags to associate to the factory
     *
     * @returns {Jsimple} The current Jsimple instance
     */
    factory(name, code, tags) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.factory must be a string identifier")
        }

        if (typeof code !== "function") {
            throw new Error("Argument #2 passed to Jsimple.factory must be a function")
        }

        return this.define(
            name,
            jsimple => {
                let instance = code(jsimple);

                this.frozen.add(name);

                return instance;
            },
            tags || []
        );
    }

    /**
     *
     * @param {String}                                     name   The unique identifier for the parameter or factory to extend
     * @param {Function(service: *, container: Jsimple): *} code   The executable extended function
     * @param {Array<String>}                              [tags] An array of tags to associate to the the parameter or factory to extend
     *
     * @returns {Jsimple} The current Jsimple instance
     */
    extend(name, code, tags) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.extend must be a string identifier")
        }

        if (typeof code !== "function") {
            throw new Error("Argument #2 passed to Jsimple.extend must be a function")
        }

        let service = this.raw(name);

        return this.share(
            name,
            jsimple => code(service(jsimple), jsimple),
            tags || this.values.get(name).tags
        );
    }

    /**
     *
     * @param {String} name The unique identifier for the parameter, service or factory
     *
     * @returns {Boolean} Wether the parameter, service or factory exists
     */
    exists(name) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.exists must be a string identifier")
        }

        return this.values.has(name);
    }

    /**
     *
     * @param {String} name The unique identifier for the parameter, service or factory to fetch
     *
     * @returns {*} Result of executing the factory function
     */
    get(name) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.get must be a string identifier")
        }

        return this.raw(name)(this);
    }

    /**
     *
     * @deprecated Use {@link Jsimple#tagged} instead
     * @see Jsimple#tagged
     *
     * @param {String} tag The tag name for which to fetch parameters, services or factories
     *
     * @return {Array} Service names associated with the provided tag
     */
    getTagged(tag) {
        return this.tagged(tag);
    }

    /**
     *
     * @param {String|Array<String>} tags The tag names for which to fetch parameters, services or factories
     *
     * @returns {Array} Service names associated with the provided tags
     */
    tagged(tags) {
        if (typeof tags !== "string" && tags.constructor.name !== "Array") {
            throw new Error("Argument #1 passed to Jsimple.tagged must be a string identifier or an array of string identifiers")
        }

        if (typeof tags === "string") {
            tags = [tags];
        }

        let tagged;

        tags.forEach(tag => {
            let services = Array.from(this.tagmap.get(tag) || []);

            if (!tagged) {
                tagged = services;
            } else {
                tagged = services.filter(service => tagged.indexOf(service) > -1);
            }
        });

        return tagged;
    }

    /**
     *
     * @deprecated
     *
     * @return {Array} Declared parameter, service and factory names
     */
    keys() {
        return Array.from(this.values.keys());
    }

    /**
     *
     * @param {Function(): *} code Function to be protected from becoming a factory
     *
     * @returns {Function(): *} Function wrapping the provided function as code
     */
    protect(code) {
        if (typeof code !== "function") {
            throw new Error("Argument #1 passed to Jsimple.protect must be a function")
        }

        return () => code;
    }

    /**
     *
     * @param {String} name The unique identifier for the factory to fetch
     *
     * @returns {Function(container: Jsimple): *} The declared factory function
     */
    raw(name) {
        if (typeof name !== "string") {
            throw new Error("Argument #1 passed to Jsimple.raw must be a string identifier")
        }

        if (this.exists(name) === false) {
            throw new Error(`Identifier ${name} is not defined`);
        }

        return this.values.get(name);
    }

    /**
     *
     * @returns {Jsimple} The current Jsimple instance wrapped in a Proxy
     */
    proxify() {
        return JsimpleProxified.fromJsimple(this);
    }
}

/**
 * @access private
 */
class JsimpleProxified extends Jsimple {
    /**
     * Builds a proxified Jsimple instance from a Jsimple instance
     *
     * @param {Jsimple} jsimple The jsimple instance to proxify
     *
     * @returns {Jsimple} A proxified Jsimple instance
     */
    static fromJsimple(jsimple) {
        if (jsimple instanceof JsimpleProxified) {
            return jsimple;
        }

        let Proxy = require("./proxy.js"),
            proxified = new JsimpleProxified();

        Object.getOwnPropertyNames(jsimple).forEach(property => {
            jsimple[property].forEach((value, key) => {
                if (proxified[property] instanceof Map) {
                    proxified[property].set(key, value);
                }

                if (proxified[property] instanceof Set) {
                    proxified[property].add(value);
                }
            });
        });

        return new Proxy(proxified);
    }
}

module.exports = Jsimple;