zest/base.resolver

View on GitHub
lib/resolver/utils.js

Summary

Maintainability
A
3 hrs
Test Coverage
'use strict';
/**
 * @fileOverview The base-resolver/utils module has utility functions for parsing expressions, normalizing dependencies
 * and validations
 * @module base-resolver/utils
 * @requires {@link external:q}
 */
var q = require('q');
/**
 * Extracts arguments from a function and makes a list of dependencies. The function parameter is transformed from
 * camel-case to <code>-</code> and <code>.</code> separated, and an expression is created which evaluates them in
 * sequence, cascading, till a match is found.<br/>
 * eg. <code>databaseMongoLocal</code> as a parameter dependency will try to resolve to
 * <code>databaseMongoLocal</code>. If there are no components with that name, <code>database-mongo-local</code> will
 * be tried. If <code>database-mongo-local</code> is also not resolved, <code>database.mongo.local</code> will be
 * tried. The first resolution will be taken as the value and if none of them resolves, the component will fail to
 * resolve.
 * @param {callback} callback - the function to be inspected
 * @returns {Array.<string>} an array of dependency expressions in the same sequence as the parameters
 * @example
 * utils.getDependencies(function (x, y, z) {
 *      ...
 * });
 * // this will return ['x', 'y', 'z']
 * @example
 * utils.getDependencies(function (dataStore, userProvider) {
 *      ...
 * });
 * // This should return
 * [
 *      'dataStore|data-store|data.store',
 *      'userProvider|user-provider|user.provider'
 * ]
 */
exports.getDependencies = function (callback) {
    // only accept functions as argument
    if (typeof callback !== 'function') {
        throw new Error('only functions can have params!');
    }
    /*jslint regexp: true */
    // first trim off any comments form the string representation of the function
    var args = callback.toString().replace(
        /((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg, ''
    ).// then split out the parameters in a ocmma separated string
        match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1];
    /*jslint regexp: false */
    // if there are no parameters, return an empty array
    if (args.length === 0) {
        return [];
    }
    // if there are parameters, make an expression for each of them and return the array of expressions
    return args.split(/,/).map(
        function (dependency) {
            var splits = dependency.match(/[A-Z]?[a-z0-9]*/g).map(
                function (match) {
                    return match.toLowerCase();
                }
            ).slice(0, -1);
            if (splits.length === 1) {
                return dependency;
            }
            return dependency + '|' + splits.join('-') + '|' + splits.join('.');
        }
    );
};
/**
 * Resolves any expression with modifiers. Modifiers are used to add logic to dependency injection. There are 4 kinds
 * of modifiers. They are listed in their order of execution priority below:<
 *
 *  - **`#` (parameter modifier)** will pass parameters to the component that can be used to construct the options
 *    object. See [Configuring Resolver](#configuring-resolver) for more details on parameter usage.
 *  - **`!` (immediate modifier)** will mark a dependency as immediate. When a dependency is immediate, resolver will
 *    not wait for it to resolve, but instead, pass a promise ( which will get resolved when the immediate dependency
 *    resolves ) to the component factory function. Immediate is discussed in detail in the 
 *    [Circular Dependencies](#circular-dependencies) section.
 *  - **`|` (OR modifier)** will inject the first resolvable component
 *  - **`?` (optional modifier)** will silently pass undefined if resolution fails
 *
 * If the expression is a primitive non string type, it will be returned as-is. All strings will be considered as
 * expressions and resolved. For objects or arrays, resolution will happen recursively.
 * @param {string} expression - the expression to evaluate
 * @param {module:base-resolver/utils~ResolverCallback} resolverFunction - the resolver function to be called.
 * @returns {external:q} a promise that resolves to the evaluated value of the expression
 * @example
 * utils.resolveExpression('a|b#c|d', function (expression, params, expressionWithParams) {
 *          ...
 * }
 * // the ResolverCallback will be called with these arguments
 * // first call
 *  a, [], a
 * // second call
 *  b, [c], b#c
 * // third call
 *  d, [], d
 * // if any of the calls return a non undefined value or do not throw an Error, the next call will not happen
 * // if none of the callbacks return a defined value, the promise returned will reject with error
 * @example
 * utils.resolveExpression('a|b#c/#c1#c2|d?', function (expression, params, expressionWithParams) {
 *          ...
 * }
 * // the ResolverCallback will be called with these arguments
 * // first call
 *  a, [], a
 * // second call
 *  b, [c#c1, c2], b#c/#c1#c2
 * // third call
 *  d, [], d
 * // #, |, ? and / can be escaped with a / as shown in the second call
 * // if any of the calls return a non undefined value or do not throw an Error, the next call will not happen
 * // if none of the callbacks return a defined value, the promise returned will still resolve with undefined
 * // because theoptional flag (?) is specified
 */
exports.resolveExpression = function (expression, resolverFunction) {
    var optional, expressions, deferred, promise, retVal, getNextPromise;
    if (typeof expression !== 'string') {
        if (typeof expression !== 'object') {
            // if the expression is not a string or an object, we resolve it with its value
            return q(expression);
        }
        // if the expression is not a string and an object, every key has to be resolved recursively
        // make sure you do not modify the original object
        retVal = new expression.constructor();
        return q.all(
            Object.keys(expression).map(
                function (key) {
                    return exports.resolveExpression(expression[key], resolverFunction).then(
                        function (value) {
                            retVal[key] = value;
                        }
                    );
                }
            )
        ).then(
            function () {
                // once all keys are resolved, construct the object and send it accross
                return retVal;
            }
        );
    }
    // for strings, do the following
    deferred = q.defer();
    promise = deferred.promise;
    // check if the parameter is opitonal
    optional = /\?$/.test(expression);
    if (optional) {
        // trim off the trailing ?
        expression = expression.slice(0, -1);
    }
    // split the expression with the OR (|) modifier making sure \ is escaped
    /*jslint regexp: true */
    expressions = expression.match(/(\/.|[^\|])+/g);
    /*jslint regexp: false */
    // try resolving the expressions one by one, breaking at first successful resolution
    getNextPromise = function () {
        // split the expression to get the parameters
        var thisExpressionWithParams = expressions.shift();
        // check if the parameter is immediate
        var immediate = /!$/.test(thisExpressionWithParams);
        if (immediate) {
            // trim off the trailing !
            thisExpressionWithParams = thisExpressionWithParams.slice(0, -1);
        }
        /*jslint regexp: true */
        var thisParams = thisExpressionWithParams.match(/(\/.|[^#])+/g).map(
            function (value) {
                return value.replace(/\/([\/\|\?#]{1})/g, '$1');
            }
        );
        /*jslint regexp: false */
        var thisExpression = thisParams.shift();
        return q.Promise(
            function (resolve, reject) {
                // process.nextTick
                process.nextTick(function () {
                    try {
                        resolve(resolverFunction(thisExpression, thisParams, thisExpressionWithParams, immediate));
                    } catch(error) {
                        reject(error);
                    }
                });
            }
        ).then(
            function (value) {
                // if resolution returned a value, skip further resolutions and return
                if (value) {
                    return value;
                }
                // otherwise, use the next expression till all expressions are used
                if (expressions.length) {
                    return getNextPromise();
                }
                return undefined;
            }, function () {
                // the error case is handled as an undefined resolution
                // as before, use the next expression till all expressions are used
                if (expressions.length) {
                    return getNextPromise();
                }
                return undefined;
            }
        );
    };
    getNextPromise().then(
        function (value) {
            // once the resolution is recieved, if the resolution is undefined and the resolution is not optional,
            // reject with an error
            if (!value && !optional) {
                return deferred.reject(
                    new Error(
                            'expression could not be resolved to a defined value. Use ? if this is acceptable (in ' +
                            expression + ')'
                    )
                );
            }
            // otherwise, return the resolution
            return deferred.resolve(value);
        }
    );
    return promise;
};
/**
 * This function checks if an array is an injector array or not. An injector array is a valid form of zest component
 * representation which is an array with the last element of type function and other elements of type string.
 * The strings in the array is mapped to components and are injected as parameters to the function in the same
 * sequence.
 * @param {*} obj - any object
 * @returns {boolean} true if the object is an injector array. false otherwise.
 */
exports.isInjectorArray = function (obj) {
    return (Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj[obj.length - 1] === 'function') &&
        (function () {
            var i, j;
            for (i = 0, j = obj.length - 1; i < j; i = i + 1) {
                if (typeof obj[i] !== 'string') {
                    return false;
                }
            }
            return true;
        }());
};
/**
 * This function clones and creates a new copy of the object.
 * @param {*} obj - any object
 * @returns {*} cloned object
 */
exports.clone = function (obj) {
    return JSON.parse(JSON.stringify(obj));
};
/**
 * The ResolverCallback is used in {@link module:base-resolver/utils.resolveExpression| resolveExpression} function as
 * a parameter. This callback is called for every part of the expression to get its value. The values are then
 * consolidated and a result is obtained.
 * @callback module:base-resolver/utils~ResolverCallback
 * @param {string} expression - the actual expression on which the resolver is to be called
 * @param {Array.<string>} params - the list of parameters to be sent to the configurations object
 * @param {string} expressionWithParams - the original expression. This can be used for caching results.
 * @param {boolean} [immediate] - if true, the callback should not return a promise. This is useful when resolving a
 * circular dependency
 * @see module:base-resolver/utils.resolveExpression
 */