etcinit/enclosure

View on GitHub
src/Chromabits/Container/Container.js

Summary

Maintainability
A
4 hrs
Test Coverage
'use strict';

import ensure from 'ensure.js';
import { Nullable } from 'ensure.js';
import introspect from 'retrieve-arguments';

import Wrap from './Wrap';

/**
 * Class Container
 *
 * An Inversion of Control container
 */
class Container
{
    /**
     * Construct an instance of a Container
     */
    constructor () {
        /**
         * Container bindings
         *
         * @type {Array}
         */
        this.bindings = [];

        /**
         * Singleton instances
         *
         * @type {Array}
         */
        this.instances = [];

        /**
         * Factory functions
         *
         * @type {Array}
         */
        this.factories = [];

        /**
         * Abstract types that should be shared (singletons after built)
         *
         * @type {Array}
         */
        this.shared = [];

        this.buildStack = [];

        /**
         * Class loader
         *
         * @type {null|Loader}
         */
        this.loader = null;

        /**
         * The max depth dependencies will be explored
         *
         * Circular dependencies or overly complex applications
         * might cause the container to reach this limit and bail.
         *
         * If you are encountering this issue, you can increase the value below
         *
         * @type {number}
         */
        this.maxDepth = 255;

        /**
         * Make the container available as a service
         */
        this.instance('EnclosureContainer', this);
    }

    /**
     * Bind a singleton instance to an abstract
     *
     * @param abstract
     * @param instance
     */
    instance (abstract, instance) {
        // If the abstract is an array, we will assume it is an array of strings
        // of each each abstract we want to bind the instance to
        if (ensure.isArray(abstract)) {
            abstract.forEach((single) => {
                ensure(single, String);

                this.instances[single] = instance;
            });
        } else {
            ensure(abstract, String);

            this.instances[abstract] = instance;
        }
    }

    /**
     * Bind an abstract with a concrete with an abstract
     *
     * Acceptable concrete types are Wrap objects or a String
     * reference to another abstract type which eventually
     * should resolve into a concrete
     *
     * @param abstract
     * @param concrete
     * @param shared
     */
    bind (abstract, concrete, shared) {
        ensure(abstract, String);
        // TODO: #1: Type-check concrete param once ensure.js supports Maybe

        // If the concrete implementation is either a string or a Wrap,
        // we just create a binding
        if (concrete instanceof Wrap
            || concrete instanceof Function
            || ensure.isString(concrete)) {
            this.bindings[abstract] = concrete;

            if (shared) {
                this.shared[abstract] = true;
            }

            return;
        }

        throw new Error('Unsupported binding type');
    }

    /**
     * Bind a factory function to an abstract
     *
     * @param abstract
     * @param concrete
     * @param shared
     */
    factory (abstract, concrete, shared) {
        ensure(abstract, String);
        ensure(concrete, Function);

        this.factories[abstract] = concrete;

        if (shared) {
            this.shared[abstract] = true;
        }
    }

    /**
     * Attempt to find the concrete implementation associated with
     * an abstract
     *
     * @param abstract
     *
     * @returns {*}
     */
    getConcrete (abstract) {
        // If the abstract is not defined in the bindings array
        // then we can't find a concrete, so we have to bail
        if (!(abstract in this.bindings) && !(abstract in this.factories)) {
            throw new Error(
                'Unable to resolve concrete type: ' + abstract
            );
        }

        if (abstract in this.bindings) {
            var concrete = this.bindings[abstract];

            // If the concrete is a string, then it still is abstract
            // so we need to recurse and try to find it's concrete
            if (ensure.isString(concrete)) {
                return this.getConcrete(concrete);
            }

            // If there is a Wrap or constructor bound to the abstract, then we
            // know it is actually already concrete already
            if (concrete instanceof Wrap || concrete instanceof Function) {
                return abstract;
            }
        } else if (abstract in this.factories) {
            // If there is a factory function for the abstract, then we know
            // it is actually a concrete already
            return abstract;
        }

        throw new Error(
            'Unable to resolve concrete type. Unknown binding type'
        );
    }

    /**
     * Resolve the abstract in the container
     *
     * @param abstract
     *
     * @returns {*}
     */
    make (abstract) {
        // If there is a singleton instance being managed, return it
        if (abstract in this.instances) {
            return this.instances[abstract];
        }

        // Get concrete
        var concrete = abstract;
        let instance;

        try {
            concrete = this.getConcrete(abstract);

            // If buildable, build one
            if (this.isBuildable(concrete)) {
                instance = this.build(concrete);

                // If the abstract type is marked as shared, we will cache the
                // instance as a singleton so subsequent calls to make will
                // return the same instance
                if (abstract in this.shared) {
                    this.instance(abstract, instance);
                }

                return instance;
            }
        } catch (err) {
            // Fall back to building a class using the loader (if it is present)
            if (this.loader && this.loader.has(abstract)) {
                var constructor = this.loader.get(abstract);
                var dependencies = this.resolveDependencies(constructor);

                instance = Object.create(constructor.prototype);

                constructor.apply(instance, dependencies);

                return instance;
            }

            throw err;
        }

        throw new Error('Unable to build abstract: ' + abstract);
    }

    /**
     * Construct a new instance of the specified concrete
     *
     * @param concrete
     * @param parameters
     *
     * @returns {*}
     */
    build (concrete, parameters) {
        let constructor,
            dependencies;

        parameters = parameters || [];

        // Check if there is a factory function defined for this function
        // If there is one, we will use it to build the instance. This
        // allows functions to be defined as more refined resolvers
        if (concrete in this.factories) {
            var factoryArguments = [this].concat(parameters);

            var factoryFunction = this.factories[concrete];

            return factoryFunction.apply(
                Object.create(factoryFunction.prototype),
                factoryArguments
            );
        }

        this.buildStack.push(concrete);

        // Check if we have descended too much into the dependency tree.
        // This could mean we have a circular dependency or a very complex
        // application
        if (this.buildStack.length > this.maxDepth) {
            throw new Error(
                'Max dependency depth reached.'
                + 'Possible circular dependency'
            );
        }

        // If the concrete is a Wrap object, we can resolve its dependencies
        // and then construct an instance
        if (this.isWrapped(concrete)) {
            var wrap = this.bindings[concrete];

            dependencies = this.resolveDependencies(wrap);

            // When calling the constructor in the Wrap object, we pass a
            // reference to this container, and all the dependencies as
            // arguments
            let constructorArguments = [this].concat(dependencies, parameters);

            constructor = wrap.getConstructor();

            this.buildStack.pop();

            return constructor.apply(
                Object.create(constructor.prototype),
                constructorArguments
            );
        }

        // If the concrete is just a function, then we will assume it is a
        // service constructor. We will use introspection to find out which are
        // its dependencies
        if (
            concrete in this.bindings
            && this.bindings[concrete] instanceof Function
        ) {
            constructor = this.bindings[concrete];

            dependencies = this.resolveDependencies(constructor);

            this.buildStack.pop();

            // Do some magic to call new with a variable number of arguments
            // See http://goo.gl/R7Dpt
            var instance = Object.create(constructor.prototype);

            constructor.apply(instance, dependencies);

            return instance;
        }

        throw new Error(
            'Unable to resolve. The concrete type is not instantiable'
        );
    }

    /**
     * Use the container to get the dependencies of a Wrap or a constructor
     * function
     *
     * @param {Function|Wrap} construct
     *
     * @returns {Array}
     */
    resolveDependencies (construct) {
        var dependenciesNames = [],
            dependencies = [];

        if (construct instanceof Wrap) {
            dependenciesNames = construct.getDependencies();
        } else if (construct instanceof Function) {
            dependenciesNames = introspect(construct);
        }

        dependenciesNames.forEach((dependencyName) => {
            dependencyName = dependencyName.replace(/_/g, '/');

            dependencies.push(this.make(dependencyName));
        });

        return dependencies;
    }

    /**
     * Check that a concrete object is wrapped in an Enclosure wrap
     *
     * @param concrete
     *
     * @returns {boolean}
     */
    isWrapped (concrete) {
        return (concrete in this.bindings &&
        this.bindings[concrete] instanceof Wrap);
    }

    /**
     * Check if an abstract is resolvable by this container
     *
     * @param abstract
     *
     * @returns {boolean}
     */
    isResolvable (abstract) {
        if (this.instances[abstract]) {
            return true;
        }

        try {
            var concrete = this.getConcrete(abstract);

            return true;
        } catch (err) {
            return false;
        }
    }

    /**
     * Get whether a concrete type can be built by this container
     *
     * @param concrete
     * @returns {boolean}
     */
    isBuildable (concrete) {
        // If we have a factory function, we can build the type
        if (concrete in this.factories) {
            return true;
        }

        // If we have a wrap or constructor, we can also build it
        if (concrete in this.bindings
            && (this.bindings[concrete] instanceof Wrap)
            || (this.bindings[concrete] instanceof Function)) {
            return true;
        }

        return false;
    }

    /**
     * Set the loader to use for automatic class loading and instantiation
     *
     * @param loader
     */
    setLoader (loader) {
        this.loader = loader;

        this.instance('Chromabits/Loader/Loader', loader);
    }

    /**
     * Load a class constructor
     *
     * A simple alias for the Loader's get method
     *
     * @param fullClassName
     *
     * @returns {Function}
     */
    use (fullClassName) {
        if (this.loader) {
            return this.loader.get(fullClassName);
        }

        throw new Error('This container does not have a loader attached');
    }

    /**
     * Installs container shortcut properties into a specific object
     *
     * This is useful for making the container available on the global or window
     * contexts.
     *
     * @param target
     */
    installTo (target) {
        ensure(target, Object);

        target.container = this;

        if (this.loader) {
            target.use = this.use.bind(this);
        }
    }
}

module.exports = Container;