feat(injector): initial implementaion of dynamic injector
This commit is contained in:
7
modules/di/src/annotations.js
Normal file
7
modules/di/src/annotations.js
Normal 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
64
modules/di/src/binding.js
Normal 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));
|
||||
}
|
||||
}
|
@ -1 +1,5 @@
|
||||
export * from './module';
|
||||
export * from './injector';
|
||||
export * from './binding';
|
||||
export * from './key';
|
||||
export * from './module';
|
||||
export {Inject} from 'facade/di/reflector';
|
||||
|
52
modules/di/src/exceptions.js
Normal file
52
modules/di/src/exceptions.js
Normal 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
133
modules/di/src/injector.js
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 {}
|
76
modules/di/test/di/async_spec.js
Normal file
76
modules/di/test/di/async_spec.js
Normal file
@ -0,0 +1,76 @@
|
||||
import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib';
|
||||
import {Injector, Inject, bind, Key} from 'di/di';
|
||||
import {Future, FutureWrapper} from 'facade/async';
|
||||
|
||||
class UserList {}
|
||||
|
||||
function fetchUsers() {
|
||||
return FutureWrapper.value(new UserList());
|
||||
}
|
||||
|
||||
class SynchronousUserList {}
|
||||
|
||||
|
||||
class UserController {
|
||||
constructor(list:UserList) {
|
||||
this.list = list;
|
||||
}
|
||||
}
|
||||
|
||||
export function main () {
|
||||
describe("async injection", function () {
|
||||
it('should return a future', function() {
|
||||
var injector = new Injector([
|
||||
bind(UserList).toAsyncFactory([], fetchUsers)
|
||||
]);
|
||||
var p = injector.asyncGet(UserList);
|
||||
expect(p).toBeFuture();
|
||||
});
|
||||
|
||||
it('should throw when instantiating async provider synchronously', function() {
|
||||
var injector = new Injector([
|
||||
bind(UserList).toAsyncFactory([], fetchUsers)
|
||||
]);
|
||||
|
||||
expect(() => injector.get(UserList))
|
||||
.toThrowError('Cannot instantiate UserList synchronously. It is provided as a future!');
|
||||
});
|
||||
|
||||
it('should return a future even if the provider is sync', function() {
|
||||
var injector = new Injector([
|
||||
SynchronousUserList
|
||||
]);
|
||||
var p = injector.asyncGet(SynchronousUserList);
|
||||
expect(p).toBeFuture();
|
||||
});
|
||||
|
||||
it('should provide itself', function() {
|
||||
var injector = new Injector([]);
|
||||
var p = injector.asyncGet(Injector);
|
||||
expect(p).toBeFuture();
|
||||
});
|
||||
|
||||
it('should return a future when a dependency is async', function(done) {
|
||||
var injector = new Injector([
|
||||
bind(UserList).toAsyncFactory([], fetchUsers),
|
||||
UserController
|
||||
]);
|
||||
|
||||
injector.asyncGet(UserController).then(function(userController) {
|
||||
expect(userController).toBeAnInstanceOf(UserController);
|
||||
expect(userController.list).toBeAnInstanceOf(UserList);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when a dependency is async', function() {
|
||||
var injector = new Injector([
|
||||
bind(UserList).toAsyncFactory([], fetchUsers),
|
||||
UserController
|
||||
]);
|
||||
|
||||
expect(() => injector.get(UserController))
|
||||
.toThrowError('Cannot instantiate UserList synchronously. It is provided as a future! (UserController -> UserList)');
|
||||
});
|
||||
});
|
||||
}
|
144
modules/di/test/di/injector_spec.js
Normal file
144
modules/di/test/di/injector_spec.js
Normal file
@ -0,0 +1,144 @@
|
||||
import {describe, it, expect, beforeEach} from 'test_lib/test_lib';
|
||||
import {Injector, Inject, bind} from 'di/di';
|
||||
|
||||
class Engine {}
|
||||
class Dashboard {}
|
||||
class TurboEngine extends Engine{}
|
||||
|
||||
class Car {
|
||||
constructor(engine:Engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
}
|
||||
|
||||
class CarWithDashboard {
|
||||
constructor(engine:Engine, dashboard:Dashboard) {
|
||||
this.engine = engine;
|
||||
this.dashboard = dashboard;
|
||||
}
|
||||
}
|
||||
|
||||
class SportsCar extends Car {
|
||||
constructor(engine:Engine) {
|
||||
super(engine);
|
||||
}
|
||||
}
|
||||
|
||||
class CarWithInject {
|
||||
constructor(@Inject(TurboEngine) engine:Engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
}
|
||||
|
||||
export function main() {
|
||||
describe('injector', function() {
|
||||
it('should instantiate a class without dependencies', function() {
|
||||
var injector = new Injector([Engine]);
|
||||
var engine = injector.get(Engine);
|
||||
|
||||
expect(engine).toBeAnInstanceOf(Engine);
|
||||
});
|
||||
|
||||
it('should resolve dependencies based on type information', function() {
|
||||
var injector = new Injector([Engine, Car]);
|
||||
var car = injector.get(Car);
|
||||
|
||||
expect(car).toBeAnInstanceOf(Car);
|
||||
expect(car.engine).toBeAnInstanceOf(Engine);
|
||||
});
|
||||
|
||||
it('should resolve dependencies based on @Inject annotation', function() {
|
||||
var injector = new Injector([TurboEngine, Engine, CarWithInject]);
|
||||
var car = injector.get(CarWithInject);
|
||||
|
||||
expect(car).toBeAnInstanceOf(CarWithInject);
|
||||
expect(car.engine).toBeAnInstanceOf(TurboEngine);
|
||||
});
|
||||
|
||||
it('should cache instances', function() {
|
||||
var injector = new Injector([Engine]);
|
||||
|
||||
var e1 = injector.get(Engine);
|
||||
var e2 = injector.get(Engine);
|
||||
|
||||
expect(e1).toBe(e2);
|
||||
});
|
||||
|
||||
it('should bind to a value', function() {
|
||||
var injector = new Injector([
|
||||
bind(Engine).toValue("fake engine")
|
||||
]);
|
||||
|
||||
var engine = injector.get(Engine);
|
||||
expect(engine).toEqual("fake engine");
|
||||
});
|
||||
|
||||
it('should bind to a factory', function() {
|
||||
var injector = new Injector([
|
||||
Engine,
|
||||
bind(Car).toFactory([Engine], (e) => new SportsCar(e))
|
||||
]);
|
||||
|
||||
var car = injector.get(Car);
|
||||
expect(car).toBeAnInstanceOf(SportsCar);
|
||||
expect(car.engine).toBeAnInstanceOf(Engine);
|
||||
});
|
||||
|
||||
it('should use non-type tokens', function() {
|
||||
var injector = new Injector([
|
||||
bind('token').toValue('value')
|
||||
]);
|
||||
|
||||
expect(injector.get('token')).toEqual('value');
|
||||
});
|
||||
|
||||
it('should throw when given invalid bindings', function() {
|
||||
expect(() => new Injector(["blah"])).toThrowError('Invalid binding blah');
|
||||
expect(() => new Injector([bind("blah")])).toThrowError('Invalid binding blah');
|
||||
});
|
||||
|
||||
describe("child", function () {
|
||||
it('should load instances from parent injector', function() {
|
||||
var parent = new Injector([Engine]);
|
||||
var child = parent.createChild([]);
|
||||
|
||||
var engineFromParent = parent.get(Engine);
|
||||
var engineFromChild = child.get(Engine);
|
||||
|
||||
expect(engineFromChild).toBe(engineFromParent);
|
||||
});
|
||||
|
||||
it('should create new instance in a child injector', function() {
|
||||
var parent = new Injector([Engine]);
|
||||
var child = parent.createChild([
|
||||
bind(Engine).toClass(TurboEngine)
|
||||
]);
|
||||
|
||||
var engineFromParent = parent.get(Engine);
|
||||
var engineFromChild = child.get(Engine);
|
||||
|
||||
expect(engineFromParent).not.toBe(engineFromChild);
|
||||
expect(engineFromChild).toBeAnInstanceOf(TurboEngine);
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide itself', function() {
|
||||
var parent = new Injector([]);
|
||||
var child = parent.createChild([]);
|
||||
|
||||
expect(child.get(Injector)).toBe(child);
|
||||
});
|
||||
|
||||
it('should throw when no provider defined', function() {
|
||||
var injector = new Injector([]);
|
||||
expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!');
|
||||
});
|
||||
|
||||
it('should show the full path when no provider', function() {
|
||||
var injector = new Injector([CarWithDashboard, Engine]);
|
||||
|
||||
expect(() => injector.get(CarWithDashboard)).
|
||||
toThrowError('No provider for Dashboard! (CarWithDashboard -> Dashboard)');
|
||||
});
|
||||
});
|
||||
}
|
14
modules/di/test/di/key_spec.js
Normal file
14
modules/di/test/di/key_spec.js
Normal file
@ -0,0 +1,14 @@
|
||||
import {describe, it, expect} from 'test_lib/test_lib';
|
||||
import {Key} from 'di/di';
|
||||
|
||||
export function main () {
|
||||
describe("key", function () {
|
||||
it('should be equal to another key if type is the same', function () {
|
||||
expect(Key.get('car')).toBe(Key.get('car'));
|
||||
});
|
||||
|
||||
it('should not be equal to another key if types are different', function () {
|
||||
expect(Key.get('car')).not.toBe(Key.get('porsche'));
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user