feat(injector): initial implementaion of dynamic injector

This commit is contained in:
vsavkin
2014-09-30 14:56:33 -04:00
parent 6c8da62c1b
commit b2199632c7
24 changed files with 792 additions and 44 deletions

View File

@ -0,0 +1,7 @@
//TODO: vsavkin: uncomment once const constructor are supported
//export class Inject {
// @CONST
// constructor(token){
// this.token = token;
// }
//}

64
modules/di/src/binding.js Normal file
View File

@ -0,0 +1,64 @@
import {Type} from 'facade/lang';
import {List, MapWrapper, ListWrapper} from 'facade/collection';
import {Reflector} from 'facade/di/reflector';
import {Key} from './key';
export class Binding {
constructor(key:Key, factory:Function, dependencies:List, async) {
this.key = key;
this.factory = factory;
this.dependencies = dependencies;
this.async = async;
}
}
export function bind(token):BindingBuilder {
return new BindingBuilder(token);
}
export class BindingBuilder {
constructor(token) {
this.token = token;
this.reflector = new Reflector();
}
toClass(type:Type):Binding {
return new Binding(
Key.get(this.token),
this.reflector.factoryFor(type),
this._wrapKeys(this.reflector.dependencies(type)),
false
);
}
toValue(value):Binding {
return new Binding(
Key.get(this.token),
(_) => value,
[],
false
);
}
toFactory(dependencies:List, factoryFunction:Function):Binding {
return new Binding(
Key.get(this.token),
this.reflector.convertToFactory(factoryFunction),
this._wrapKeys(dependencies),
false
);
}
toAsyncFactory(dependencies:List, factoryFunction:Function):Binding {
return new Binding(
Key.get(this.token),
this.reflector.convertToFactory(factoryFunction),
this._wrapKeys(dependencies),
true
);
}
_wrapKeys(deps:List) {
return ListWrapper.map(deps, (t) => Key.get(t));
}
}

View File

@ -1 +1,5 @@
export * from './module';
export * from './injector';
export * from './binding';
export * from './key';
export * from './module';
export {Inject} from 'facade/di/reflector';

View File

@ -0,0 +1,52 @@
import {ListWrapper, List} from 'facade/collection';
import {humanize} from 'facade/lang';
function constructResolvingPath(keys: List) {
if (keys.length > 1) {
var tokenStrs = ListWrapper.map(keys, (k) => humanize(k.token));
return " (" + tokenStrs.join(' -> ') + ")";
} else {
return "";
}
}
export class NoProviderError extends Error {
constructor(keys:List){
this.message = this._constructResolvingMessage(keys);
}
_constructResolvingMessage(keys:List) {
var last = humanize(ListWrapper.last(keys).token);
return `No provider for ${last}!${constructResolvingPath(keys)}`;
}
toString() {
return this.message;
}
}
export class AsyncProviderError extends Error {
constructor(keys:List){
this.message = this._constructResolvingMessage(keys);
}
_constructResolvingMessage(keys:List) {
var last = humanize(ListWrapper.last(keys).token);
return `Cannot instantiate ${last} synchronously. ` +
`It is provided as a future!${constructResolvingPath(keys)}`;
}
toString() {
return this.message;
}
}
export class InvalidBindingError extends Error {
constructor(binding){
this.message = `Invalid binding ${binding}`;
}
toString() {
return this.message;
}
}

133
modules/di/src/injector.js Normal file
View File

@ -0,0 +1,133 @@
import {Map, List, MapWrapper, ListWrapper} from 'facade/collection';
import {Binding, BindingBuilder, bind} from './binding';
import {NoProviderError, InvalidBindingError, AsyncProviderError} from './exceptions';
import {Type, isPresent, isBlank} from 'facade/lang';
import {Future, FutureWrapper} from 'facade/async';
import {Key} from './key';
export class Injector {
constructor(bindings:List) {
var flatten = _flattenBindings(bindings);
this._bindings = this._createListOfBindings(flatten);
this._instances = this._createInstances();
this._parent = null; //TODO: vsavkin make a parameter
}
_createListOfBindings(flattenBindings):List {
var bindings = ListWrapper.createFixedSize(Key.numberOfKeys() + 1);
MapWrapper.forEach(flattenBindings, (keyId, v) => bindings[keyId] = v);
return bindings;
}
_createInstances():List {
return ListWrapper.createFixedSize(Key.numberOfKeys() + 1);
}
get(token) {
return this.getByKey(Key.get(token));
}
asyncGet(token) {
return this.asyncGetByKey(Key.get(token));
}
getByKey(key:Key) {
return this._getByKey(key, [], false);
}
asyncGetByKey(key:Key) {
return this._getByKey(key, [], true);
}
_getByKey(key:Key, resolving:List, async) {
var keyId = key.id;
//TODO: vsavkin: use LinkedList to remove clone
resolving = ListWrapper.clone(resolving)
ListWrapper.push(resolving, key);
if (key.token === Injector) return this._injector(async);
var instance = this._get(this._instances, keyId);
if (isPresent(instance)) return instance;
var binding = this._get(this._bindings, keyId);
if (isPresent(binding)) {
return this._instantiate(key, binding, resolving, async);
}
if (isPresent(this._parent)) {
return this._parent._getByKey(key, resolving, async);
}
throw new NoProviderError(resolving);
}
createChild(bindings:List):Injector {
var inj = new Injector(bindings);
inj._parent = this; //TODO: vsavkin: change it when optional parameters are working
return inj;
}
_injector(async){
return async ? FutureWrapper.value(this) : this;
}
_get(list:List, index){
if (list.length <= index) return null;
return ListWrapper.get(list, index);
}
_instantiate(key:Key, binding:Binding, resolving:List, async) {
if (binding.async && !async) {
throw new AsyncProviderError(resolving);
}
if (async) {
return this._instantiateAsync(key, binding, resolving, async);
} else {
return this._instantiateSync(key, binding, resolving, async);
}
}
_instantiateSync(key:Key, binding:Binding, resolving:List, async) {
var deps = ListWrapper.map(binding.dependencies, d => this._getByKey(d, resolving, false));
var instance = binding.factory(deps);
ListWrapper.set(this._instances, key.id, instance);
if (!binding.async && async) {
return FutureWrapper.value(instance);
}
return instance;
}
_instantiateAsync(key:Key, binding:Binding, resolving:List, async):Future {
var instances = this._createInstances();
var futures = ListWrapper.map(binding.dependencies, d => this._getByKey(d, resolving, true));
return FutureWrapper.wait(futures).
then(binding.factory).
then(function(instance) {
ListWrapper.set(instances, key.id, instance);
return instance
});
}
}
function _flattenBindings(bindings:List) {
var res = {};
ListWrapper.forEach(bindings, function (b){
if (b instanceof Binding) {
MapWrapper.set(res, b.key.id, b);
} else if (b instanceof Type) {
var s = bind(b).toClass(b);
MapWrapper.set(res, s.key.id, s);
} else if (b instanceof BindingBuilder) {
throw new InvalidBindingError(b.token);
} else {
throw new InvalidBindingError(b);
}
});
return res;
}

View File

@ -1,3 +1,25 @@
export class Key {
import {MapWrapper} from 'facade/collection';
var _allKeys = {};
var _id = 0;
export class Key {
constructor(token, id) {
this.token = token;
this.id = id;
}
static get(token) {
if (MapWrapper.contains(_allKeys, token)) {
return MapWrapper.get(_allKeys, token)
}
var newKey = new Key(token, ++_id);
MapWrapper.set(_allKeys, token, newKey);
return newKey;
}
static numberOfKeys() {
return _id;
}
}

View File

@ -1,26 +1 @@
import {FIELD} from 'facade/lang';
import {Type} from 'facade/lang';
import {Map, MapWrapper} from 'facade/collection';
import {Key} from './key';
/// becouse we need to know when toValue was not set.
/// (it could be that toValue is set to null or undefined in js)
var _UNDEFINED = {}
export class Module {
@FIELD('final bindings:Map<Key, Binding>')
constructor(){
this.bindings = new MapWrapper();
}
bind(type:Type,
{toValue/*=_UNDEFINED*/, toFactory, toImplementation, inject, toInstanceOf, withAnnotation}/*:
{toFactory:Function, toImplementation: Type, inject: Array, toInstanceOf:Type}*/) {}
bindByKey(key:Key,
{toValue/*=_UNDEFINED*/, toFactory, toImplementation, inject, toInstanceOf}/*:
{toFactory:Function, toImplementation: Type, inject: Array, toInstanceOf:Type}*/) {}
install(module:Module) {}
}
export class Module {}