Home Reference Source Repository

src/services/injector.js

/**
 * @module injector.js
 * @author Joe Groseclose <@benderTheCrime>
 * @date 8/23/2015
 */

// Project Modules
import * as $Exceptions from    './exceptions';

/**
 * @desc Handles dependency injection, will return one or many arguments passed
 * as a string or an array.
 * @since 0.0.1
 * @access public
 * @example $Injector.get('test');
 */
class $Injector {

    /**
     * @desc Responsible for routing of dependencies
     * @param {object|string} The name or array of names of providers to fetch
     * @returns {object|string|number|Array<>|boolean} The provider value
     * @todo Replace `Array.prototype.slice.call` with `Array.from` when it is
     * supported without the polyfill
     * @since 0.0.1
     * @access public
     * @example $Injector.get('$scope'); // = { $id: 1 }
     */
    static get() {
        const FIRST_ARGUMENT_IS_ARRAY = arguments[ 0 ] instanceof Array;
        const ARGS = Array.prototype.slice.call(
            FIRST_ARGUMENT_IS_ARRAY ? arguments[ 0 ] : arguments
        );
        const LAST_ARGUMENT = ARGS.slice(-1)[ 0 ];
        const REGISTRAR = global.app.$$registry;
        let providers = [],
            scoping = {};

        if (FIRST_ARGUMENT_IS_ARRAY && typeof arguments[ 1 ] === 'object') {
            scoping = arguments[ 1 ];
        } else if (typeof LAST_ARGUMENT === 'object') {
            scoping = LAST_ARGUMENT;
        }

        if (Object.keys(scoping).length && !FIRST_ARGUMENT_IS_ARRAY) {
            ARGS.pop();
        }

        for (let arg of ARGS) {
            let provider;

            if (typeof arg !== 'string') {
                continue;
            }

            // Doing this for safety reasons...if the arg didn't come from IB,
            // it potentially has unsafe spaces and underscores
            arg = arg.toString().replace(/^(_){0,2}|(_){0,2}$|\s/g, '').trim();
            if (
                REGISTRAR && REGISTRAR[ arg ] === 'Models' &&
                scoping.type && scoping.type === 'directive'
            ) {
                throw new $Exceptions.$$ProviderTypeError();
            }

            // Try to find the provider in the registrar or the declared object
            try {
                provider = app[ REGISTRAR[ arg ] ][ arg ];
            } catch (e) {

                // These are session controlled and we need to make sure
                // we pull out the right one
                if (typeof scoping.type === 'string') {
                    const TYPE = scoping.type.toLowerCase();
                    if (arg === '$scope' && [
                        'controller', 'directive', 'view', 'component'
                    ].indexOf(TYPE) > -1) {
                        provider = scoping[ arg ].val;
                    } else if (
                        [ '$request', '$response' ].indexOf(arg) > -1 &&
                        TYPE === 'controller'
                    ) {
                        provider = scoping[ arg ].val;
                    }
                }
            }

            if (provider) {
                providers.push(provider);
            } else {
                throw new $Exceptions.$$ProviderNotFoundError(arg);
            }
        }

        if (providers.length) {
            if (providers.length > 1) {
                return providers;
            }

            return providers[ 0 ];
        }

        return providers;
    }
}

/* eslint-disable no-nested-ternary */
/**
 * @desc Responsible for binding of dependencies to functions
 * @param {function} fn The function to which values are being provided
 * @param {string} type An optional Angie module type
 * @returns {function} Bound function
 * @since 0.0.1
 * @access public
 */
function $injectionBinder(fn, scoping) {
    const ARGS = $$arguments(fn);
    const PROVIDERS = $Injector.get(ARGS, scoping);

    if (PROVIDERS instanceof Array) {
        return fn.bind(null, ...PROVIDERS);
    }

    return fn.bind(null, PROVIDERS);
}

/**
 * @desc Parses dependencies out of a function using
 * `Function.prototype.toString`
 * @param {function} fn The function to which values are being provided
 * @returns {function} Bound function
 * @since 0.9.15
 * @access private
 */
function $$arguments(fn = () => false) {
    if (typeof fn === 'function') {
        let str = fn.toString(),

            // We need to pose this string in this fashion because arrow
            // function params may not be wrapped in parens
            args = str.match(
                /function([^\)\(]+)?\(([^\)\(]+)?\)|\(?([^\)\(]+)?\)?\s+?\=\>/g
            );

        if (args && args.length) {

            // Replace all of the "function" characters
            let argStr = args.map(
                v => v.replace(/(function.*)\(|[\s\=\>\)\(]/g, '')
            )[ 0 ];

            // Split our argument string and pass it back to the injector
            if (argStr.length) {
                return argStr.split(',').map(v => v.trim());
            }
        }
    }
    return [];
}

export default $Injector;
export { $injectionBinder, $$arguments };