Home Reference Source

src/utils/di/providers.js

/* */
import {
    ClassProvider as ClassProviderAnnotation,
    FactoryProvider as FactoryProviderAnnotation,
    SuperConstructor as SuperConstructorAnnotation,
    readAnnotations,
    hasAnnotation
} from './Decorators.js';
import {isFunction, isObject, toString, isUpperCase, ownKeys} from './Util.js';

function isClass(clsOrFunction) {

    if (hasAnnotation(clsOrFunction, ClassProviderAnnotation)) {
        return true;
    } else if (hasAnnotation(clsOrFunction, FactoryProviderAnnotation)) {
        return false;
    }
    /* When code is minified, class names are no longer upper case, so we skip this check
     * if the name is oddly short (which happens during minification). */
    else if (clsOrFunction.name && clsOrFunction.name.length && clsOrFunction.name.length > 3) {
        return isUpperCase(clsOrFunction.name.charAt(0));
    } else {
        return ownKeys(clsOrFunction.prototype).length > 0;
    }
}

// Provider is responsible for creating instances.
//
// responsibilities:
// - create instances
//
// communication:
// - exposes `create()` which creates an instance of something
// - exposes `params` (information about which arguments it requires to be passed into `create()`)
//
// Injector reads `provider.params` first, create these dependencies (however it wants),
// then calls `provider.create(args)`, passing in these arguments.


var EmptyFunction = Object.getPrototypeOf(Function);


// ClassProvider knows how to instantiate classes.
//
// If a class inherits (has parent constructors), this provider normalizes all the dependencies
// into a single flat array first, so that the injector does not need to worry about inheritance.
//
// - all the state is immutable (constructed)
//
// TODO(vojta): super constructor - should be only allowed during the constructor call?
class ClassProvider {
    constructor(clazz, params) {
        // TODO(vojta): can we hide this.provider? (only used for hasAnnotation(provider.provider))
        this.provider = clazz;

        this.params = [];
        this._constructors = [];

        this._flattenParams(clazz, params);
        this._constructors.unshift([clazz, 0, this.params.length - 1]);
    }

    // Normalize params for all the constructors (in the case of inheritance),
    // into a single flat array of DependencyDescriptors.
    // So that the injector does not have to worry about inheritance.
    //
    // This function mutates `this.params` and `this._constructors`,
    // but it is only called during the constructor.
    // TODO(vojta): remove the annotations argument?
    _flattenParams(constructor, params) {
        var SuperConstructor;
        var constructorInfo;

        for (var param of params) {
            if (param.token === SuperConstructorAnnotation) {
                SuperConstructor = Object.getPrototypeOf(constructor);

                if (SuperConstructor === EmptyFunction) {
                    throw new Error(`${toString(constructor)} does not have a parent constructor. Only classes with a parent can ask for SuperConstructor!`);
                }

                constructorInfo = [SuperConstructor, this.params.length];
                this._constructors.push(constructorInfo);
                this._flattenParams(SuperConstructor, readAnnotations(SuperConstructor).params);
                constructorInfo.push(this.params.length - 1);
            } else {
                this.params.push(param);
            }
        }
    }

    // Basically the reverse process to `this._flattenParams`:
    // We get arguments for all the constructors as a single flat array.
    // This method generates pre-bound "superConstructor" wrapper with correctly passing arguments.
    _createConstructor(currentConstructorIdx, context, allArguments) {
        var constructorInfo = this._constructors[currentConstructorIdx];
        var nextConstructorInfo = this._constructors[currentConstructorIdx + 1];
        var argsForCurrentConstructor;

        if (nextConstructorInfo) {
            argsForCurrentConstructor = allArguments
                .slice(constructorInfo[1], nextConstructorInfo[1])
                .concat([this._createConstructor(currentConstructorIdx + 1, context, allArguments)])
                .concat(allArguments.slice(nextConstructorInfo[2] + 1, constructorInfo[2] + 1));
        } else {
            argsForCurrentConstructor = allArguments
            /*.slice(constructorInfo[1], constructorInfo[2] + 1);*/
        }

        return function InjectedAndBoundSuperConstructor() {
            // TODO(vojta): throw if arguments given
            return constructorInfo[0].apply(context, argsForCurrentConstructor);
        };
    }

    // It is called by injector to create an instance.
    create(args) {
        var context = Object.create(this.provider.prototype);
        var constructor = this._createConstructor(0, context, args);
        var returnedValue = constructor();

        if (isFunction(returnedValue) || isObject(returnedValue)) {
            return returnedValue;
        }

        return context;
    }
}


// FactoryProvider knows how to create instance from a factory function.
// - all the state is immutable
class FactoryProvider {
    constructor(factoryFunction, params) {
        this.provider = factoryFunction;
        this.params = params;

        for (var param of params) {
            if (param.token === SuperConstructorAnnotation) {
                throw new Error(`${toString(factoryFunction)} is not a class. Only classes with a parent can ask for SuperConstructor!`);
            }
        }
    }

    create(args) {
        return this.provider.apply(undefined, args);
    }
}


export function createProviderFromFnOrClass(fnOrClass, annotations) {
    if (isClass(fnOrClass)) {
        return new ClassProvider(fnOrClass, annotations.params);
    }

    return new FactoryProvider(fnOrClass, annotations.params);
}