feat(injector): implement async dependencies

This commit is contained in:
vsavkin 2014-10-05 16:25:42 -04:00
parent a814d48bbc
commit 14af5a0a42
9 changed files with 190 additions and 117 deletions

View File

@ -5,4 +5,11 @@ export class Inject {
constructor(token){ constructor(token){
this.token = token; this.token = token;
} }
}
export class InjectFuture {
@CONST()
constructor(token){
this.token = token;
}
} }

View File

@ -1,14 +1,14 @@
import {Type} from 'facade/lang'; import {Type} from 'facade/lang';
import {List, MapWrapper, ListWrapper} from 'facade/collection'; import {List, MapWrapper, ListWrapper} from 'facade/collection';
import {Reflector} from './reflector'; import {Reflector} from './reflector';
import {Key} from './key'; import {Key, Dependency} from './key';
export class Binding { export class Binding {
constructor(key:Key, factory:Function, dependencies:List, async) { constructor(key:Key, factory:Function, dependencies:List, providedAsFuture) {
this.key = key; this.key = key;
this.factory = factory; this.factory = factory;
this.dependencies = dependencies; this.dependencies = dependencies;
this.async = async; this.providedAsFuture = providedAsFuture;
} }
} }
@ -26,7 +26,7 @@ export class BindingBuilder {
return new Binding( return new Binding(
Key.get(this.token), Key.get(this.token),
this.reflector.factoryFor(type), this.reflector.factoryFor(type),
this._wrapKeys(this.reflector.dependencies(type)), this.reflector.dependencies(type),
false false
); );
} }
@ -44,7 +44,7 @@ export class BindingBuilder {
return new Binding( return new Binding(
Key.get(this.token), Key.get(this.token),
this.reflector.convertToFactory(factoryFunction), this.reflector.convertToFactory(factoryFunction),
this._wrapKeys(dependencies), this._constructDependencies(dependencies),
false false
); );
} }
@ -53,12 +53,12 @@ export class BindingBuilder {
return new Binding( return new Binding(
Key.get(this.token), Key.get(this.token),
this.reflector.convertToFactory(factoryFunction), this.reflector.convertToFactory(factoryFunction),
this._wrapKeys(dependencies), this._constructDependencies(dependencies),
true true
); );
} }
_wrapKeys(deps:List) { _constructDependencies(deps:List) {
return ListWrapper.map(deps, (t) => Key.get(t)); return ListWrapper.map(deps, (t) => new Dependency(Key.get(t), false));
} }
} }

View File

@ -12,7 +12,13 @@ function constructResolvingPath(keys: List) {
} }
} }
export class ProviderError extends Error { class DIError extends Error {
toString() {
return this.message;
}
}
export class ProviderError extends DIError {
constructor(key:Key, constructResolvingMessage:Function){ constructor(key:Key, constructResolvingMessage:Function){
this.keys = [key]; this.keys = [key];
this.constructResolvingMessage = constructResolvingMessage; this.constructResolvingMessage = constructResolvingMessage;
@ -23,10 +29,6 @@ export class ProviderError extends Error {
ListWrapper.push(this.keys, key); ListWrapper.push(this.keys, key);
this.message = this.constructResolvingMessage(this.keys); this.message = this.constructResolvingMessage(this.keys);
} }
toString() {
return this.message;
}
} }
export class NoProviderError extends ProviderError { export class NoProviderError extends ProviderError {
@ -38,7 +40,7 @@ export class NoProviderError extends ProviderError {
} }
} }
export class AsyncProviderError extends ProviderError { export class AsyncBindingError extends ProviderError {
constructor(key:Key){ constructor(key:Key){
super(key, function(keys:List) { super(key, function(keys:List) {
var first = stringify(ListWrapper.first(keys).token); var first = stringify(ListWrapper.first(keys).token);
@ -48,12 +50,14 @@ export class AsyncProviderError extends ProviderError {
} }
} }
export class InvalidBindingError extends Error { export class InvalidBindingError extends DIError {
constructor(binding){ constructor(binding){
this.message = `Invalid binding ${binding}`; this.message = `Invalid binding ${binding}`;
} }
}
toString() { export class NoAnnotationError extends DIError {
return this.message; constructor(type){
this.message = `Cannot resolve all parameters for ${stringify(type)}`;
} }
} }

View File

@ -1,6 +1,6 @@
import {Map, List, MapWrapper, ListWrapper} from 'facade/collection'; import {Map, List, MapWrapper, ListWrapper} from 'facade/collection';
import {Binding, BindingBuilder, bind} from './binding'; import {Binding, BindingBuilder, bind} from './binding';
import {ProviderError, NoProviderError, InvalidBindingError, AsyncProviderError} from './exceptions'; import {ProviderError, NoProviderError, InvalidBindingError, AsyncBindingError} from './exceptions';
import {Type, isPresent, isBlank} from 'facade/lang'; import {Type, isPresent, isBlank} from 'facade/lang';
import {Future, FutureWrapper} from 'facade/async'; import {Future, FutureWrapper} from 'facade/async';
import {Key} from './key'; import {Key} from './key';
@ -39,21 +39,19 @@ export class Injector {
return this._getByKey(key, true); return this._getByKey(key, true);
} }
_getByKey(key:Key, async) { _getByKey(key:Key, returnFuture) {
var keyId = key.id; var keyId = key.id;
if (key.token === Injector) return this._injector(async); if (key.token === Injector) return this._injector(returnFuture);
var instance = this._get(this._instances, keyId); var instance = this._get(this._instances, keyId);
if (isPresent(instance)) return instance; if (isPresent(instance)) return instance;
var binding = this._get(this._bindings, keyId); var binding = this._get(this._bindings, keyId);
if (isPresent(binding)) { if (isPresent(binding)) {
return this._instantiate(key, binding, async); return this._instantiate(key, binding, returnFuture);
} }
if (isPresent(this._parent)) { if (isPresent(this._parent)) {
return this._parent._getByKey(key, async); return this._parent._getByKey(key, returnFuture);
} }
throw new NoProviderError(key); throw new NoProviderError(key);
@ -65,8 +63,8 @@ export class Injector {
return inj; return inj;
} }
_injector(async){ _injector(returnFuture){
return async ? FutureWrapper.value(this) : this; return returnFuture ? FutureWrapper.value(this) : this;
} }
_get(list:List, index){ _get(list:List, index){
@ -74,39 +72,40 @@ export class Injector {
return ListWrapper.get(list, index); return ListWrapper.get(list, index);
} }
_instantiate(key:Key, binding:Binding, async) { _instantiate(key:Key, binding:Binding, returnFuture) {
if (binding.async && !async) { if (binding.providedAsFuture && !returnFuture) {
throw new AsyncProviderError(key); throw new AsyncBindingError(key);
} }
if (async) { if (returnFuture) {
return this._instantiateAsync(key, binding, async); return this._instantiateAsync(key, binding);
} else { } else {
return this._instantiateSync(key, binding, async); return this._instantiateSync(key, binding);
} }
} }
_instantiateSync(key:Key, binding:Binding, async) { _instantiateSync(key:Key, binding:Binding) {
try { try {
var deps = ListWrapper.map(binding.dependencies, d => this._getByKey(d, false)); var deps = ListWrapper.map(binding.dependencies, d => this._getByKey(d.key, d.asFuture));
var instance = binding.factory(deps); var instance = binding.factory(deps);
ListWrapper.set(this._instances, key.id, instance); ListWrapper.set(this._instances, key.id, instance);
if (!binding.async && async) {
return FutureWrapper.value(instance);
}
return instance; return instance;
} catch (e) { } catch (e) {
if (e instanceof ProviderError) e.addKey(key); if (e instanceof ProviderError) e.addKey(key);
throw e; throw e;
} }
} }
_instantiateAsync(key:Key, binding:Binding, async):Future { _instantiateAsync(key:Key, binding:Binding):Future {
var instances = this._createInstances(); var instances = this._createInstances();
var futures = ListWrapper.map(binding.dependencies, d => this._getByKey(d, true)); var futures = ListWrapper.map(binding.dependencies, d => this._getByKey(d.key, true));
return FutureWrapper.wait(futures). return FutureWrapper.wait(futures).
then(binding.factory). then(binding.factory).
catch(function(e) {
console.log('sdfsdfsd', e)
//e.addKey(key)
//return e;
}).
then(function(instance) { then(function(instance) {
ListWrapper.set(instances, key.id, instance); ListWrapper.set(instances, key.id, instance);
return instance return instance

View File

@ -3,6 +3,14 @@ import {MapWrapper} from 'facade/collection';
var _allKeys = {}; var _allKeys = {};
var _id = 0; var _id = 0;
//TODO: vsavkin: move to binding once cyclic deps are supported
export class Dependency {
constructor(key:Key, asFuture){
this.key = key;
this.asFuture = asFuture;
}
}
export class Key { export class Key {
constructor(token, id) { constructor(token, id) {
this.token = token; this.token = token;

View File

@ -1,7 +1,9 @@
library facade.di.reflector; library facade.di.reflector;
import 'dart:mirrors'; import 'dart:mirrors';
import 'annotations.dart' show Inject; import 'annotations.dart' show Inject, InjectFuture;
import 'key.dart' show Key, Dependency;
import 'exceptions.dart' show NoAnnotationError;
class Reflector { class Reflector {
factoryFor(Type type) { factoryFor(Type type) {
@ -27,26 +29,19 @@ class Reflector {
return new List.generate(ctor.parameters.length, (int pos) { return new List.generate(ctor.parameters.length, (int pos) {
ParameterMirror p = ctor.parameters[pos]; ParameterMirror p = ctor.parameters[pos];
if (p.type.qualifiedName == #dynamic) { final metadata = p.metadata.map((m) => m.reflectee);
var name = MirrorSystem.getName(p.simpleName);
throw "Error getting params for '$type': "
"The '$name' parameter must be typed";
}
if (p.type is TypedefMirror) { var inject = metadata.where((m) => m is Inject);
throw "Typedef '${p.type}' in constructor " var injectFuture = metadata.where((m) => m is InjectFuture);
"'${classMirror.simpleName}' is not supported.";
}
ClassMirror pTypeMirror = (p.type as ClassMirror);
var pType = pTypeMirror.reflectedType;
final inject = p.metadata.map((m) => m.reflectee).where((m) => m is Inject);
if (inject.isNotEmpty) { if (inject.isNotEmpty) {
return inject.first.token; return new Dependency(Key.get(inject.first.token), false);
} else if (injectFuture.isNotEmpty) {
return new Dependency(Key.get(injectFuture.first.token), true);
} else if (p.type.qualifiedName != #dynamic) {
return new Dependency(Key.get(p.type.reflectedType), false);
} else { } else {
return pType; throw new NoAnnotationError(type);
} }
}, growable:false); }, growable:false);
} }

View File

@ -1,5 +1,7 @@
import {Type} from 'facade/lang'; import {Type, isPresent} from 'facade/lang';
import {Inject} from './annotations'; import {Inject, InjectFuture} from './annotations';
import {Dependency, Key} from './key';
import {NoAnnotationError} from './exceptions';
export class Reflector { export class Reflector {
factoryFor(type:Type) { factoryFor(type:Type) {
@ -12,24 +14,34 @@ export class Reflector {
dependencies(type:Type) { dependencies(type:Type) {
var p = type.parameters; var p = type.parameters;
if (p == undefined) return []; if (p == undefined && type.length == 0) return [];
return type.parameters.map((p) => this._extractToken(p)); if (p == undefined) throw new NoAnnotationError(type);
return type.parameters.map((p) => this._extractToken(type, p));
} }
_extractToken(annotations) { _extractToken(constructedType:Type, annotations) {
var type, inject; var type;
for (var paramAnnotation of annotations) { for (var paramAnnotation of annotations) {
if (isFunction(paramAnnotation)) { if (paramAnnotation instanceof Type) {
type = paramAnnotation; type = paramAnnotation;
} else if (paramAnnotation instanceof Inject) { } else if (paramAnnotation instanceof Inject) {
inject = paramAnnotation.token; return this._createDependency(paramAnnotation.token, false);
} else if (paramAnnotation instanceof InjectFuture) {
return this._createDependency(paramAnnotation.token, true);
} }
} }
return inject != undefined ? inject : type;
}
}
function isFunction(value) { if (isPresent(type)) {
return typeof value === 'function'; return this._createDependency(type, false);
} } else {
throw new NoAnnotationError(constructedType);
}
}
_createDependency(token, asFuture):Dependency {
return new Dependency(Key.get(token), asFuture);
}
}

View File

@ -1,5 +1,5 @@
import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib'; import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib';
import {Injector, Inject, bind, Key} from 'di/di'; import {Injector, Inject, InjectFuture, bind, Key} from 'di/di';
import {Future, FutureWrapper} from 'facade/async'; import {Future, FutureWrapper} from 'facade/async';
class UserList {} class UserList {}
@ -10,69 +10,108 @@ function fetchUsers() {
class SynchronousUserList {} class SynchronousUserList {}
class UserController { class UserController {
constructor(list:UserList) { constructor(list:UserList) {
this.list = list; this.list = list;
} }
} }
class AsyncUserController {
constructor(@InjectFuture(UserList) userList) {
this.userList = userList;
}
}
export function main () { export function main () {
describe("async injection", function () { 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() { describe("asyncGet", function () {
var injector = new Injector([ it('should return a future', function() {
bind(UserList).toAsyncFactory([], fetchUsers) var injector = new Injector([
]); bind(UserList).toAsyncFactory([], fetchUsers)
]);
var p = injector.asyncGet(UserList);
expect(p).toBeFuture();
});
expect(() => injector.get(UserList)) it('should return a future when if the binding is sync', function() {
.toThrowError('Cannot instantiate UserList synchronously. It is provided as a future!'); var injector = new Injector([
}); SynchronousUserList
]);
var p = injector.asyncGet(SynchronousUserList);
expect(p).toBeFuture();
});
it('should return a future even if the provider is sync', function() { it('should return the injector', function() {
var injector = new Injector([ var injector = new Injector([]);
SynchronousUserList var p = injector.asyncGet(Injector);
]); expect(p).toBeFuture();
var p = injector.asyncGet(SynchronousUserList); });
expect(p).toBeFuture();
});
it('should provide itself', function() { it('should return a future when instantiating a sync binding ' +
var injector = new Injector([]); 'with an async dependency', function(done) {
var p = injector.asyncGet(Injector); var injector = new Injector([
expect(p).toBeFuture(); bind(UserList).toAsyncFactory([], fetchUsers),
}); UserController
]);
it('should return a future when a dependency is async', function(done) { injector.asyncGet(UserController).then(function(userController) {
var injector = new Injector([ expect(userController).toBeAnInstanceOf(UserController);
bind(UserList).toAsyncFactory([], fetchUsers), expect(userController.list).toBeAnInstanceOf(UserList);
UserController done();
]); });
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() { describe("get", function () {
var injector = new Injector([ it('should throw when instantiating an async binding', function() {
bind(UserList).toAsyncFactory([], fetchUsers), var injector = new Injector([
UserController bind(UserList).toAsyncFactory([], fetchUsers)
]); ]);
expect(() => injector.get(UserController)) expect(() => injector.get(UserList))
.toThrowError('Cannot instantiate UserList synchronously. It is provided as a future! (UserController -> UserList)'); .toThrowError('Cannot instantiate UserList synchronously. It is provided as a future!');
});
it('should throw when instantiating a sync binding with an dependency', 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)');
});
it('should resolve synchronously when an async dependency requested as a future', function() {
var injector = new Injector([
bind(UserList).toAsyncFactory([], fetchUsers),
AsyncUserController
]);
var controller = injector.get(AsyncUserController);
expect(controller).toBeAnInstanceOf(AsyncUserController);
expect(controller.userList).toBeFuture();
});
it('should wrap sync dependencies into futures if required', function() {
var injector = new Injector([
bind(UserList).toFactory([], () => new UserList()),
AsyncUserController
]);
var controller = injector.get(AsyncUserController);
expect(controller).toBeAnInstanceOf(AsyncUserController);
expect(controller.userList).toBeFuture();
});
}); });
// InjectFuture toFactory([@AsyncInject(UserList)], (userListFuutre)]
// InjectFuture toFactory((@AsyncInject(UsrList) userListFuutre) => ]
// resolve exceptions and async // resolve exceptions and async
// do not construct two instances of the same async dependency if there is one in progress
// cycles
}); });
} }

View File

@ -1,4 +1,4 @@
import {describe, it, expect, beforeEach} from 'test_lib/test_lib'; import {describe, it, iit, expect, beforeEach} from 'test_lib/test_lib';
import {Injector, Inject, bind} from 'di/di'; import {Injector, Inject, bind} from 'di/di';
class Engine {} class Engine {}
@ -33,6 +33,10 @@ class CarWithInject {
} }
} }
class NoAnnotations {
constructor(secretDependency){}
}
export function main() { export function main() {
describe('injector', function() { describe('injector', function() {
it('should instantiate a class without dependencies', function() { it('should instantiate a class without dependencies', function() {
@ -58,6 +62,11 @@ export function main() {
expect(car.engine).toBeAnInstanceOf(TurboEngine); expect(car.engine).toBeAnInstanceOf(TurboEngine);
}); });
it('should throw when no type and not @Inject', function () {
expect(() => new Injector([NoAnnotations])).toThrowError(
'Cannot resolve all parameters for NoAnnotations');
});
it('should cache instances', function() { it('should cache instances', function() {
var injector = new Injector([Engine]); var injector = new Injector([Engine]);