From 14af5a0a425fc3e790e9f6dd8cddf1a58083294c Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sun, 5 Oct 2014 16:25:42 -0400 Subject: [PATCH] feat(injector): implement async dependencies --- modules/di/src/annotations.js | 7 ++ modules/di/src/binding.js | 16 ++-- modules/di/src/exceptions.js | 22 +++-- modules/di/src/injector.js | 45 +++++----- modules/di/src/key.js | 8 ++ modules/di/src/reflector.dart | 29 +++---- modules/di/src/reflector.es6 | 40 ++++++--- modules/di/test/di/async_spec.js | 129 ++++++++++++++++++---------- modules/di/test/di/injector_spec.js | 11 ++- 9 files changed, 190 insertions(+), 117 deletions(-) diff --git a/modules/di/src/annotations.js b/modules/di/src/annotations.js index 0fd9483df7..63e903c91c 100644 --- a/modules/di/src/annotations.js +++ b/modules/di/src/annotations.js @@ -5,4 +5,11 @@ export class Inject { constructor(token){ this.token = token; } +} + +export class InjectFuture { + @CONST() + constructor(token){ + this.token = token; + } } \ No newline at end of file diff --git a/modules/di/src/binding.js b/modules/di/src/binding.js index de4ec70d1d..02c8ad8c4d 100644 --- a/modules/di/src/binding.js +++ b/modules/di/src/binding.js @@ -1,14 +1,14 @@ import {Type} from 'facade/lang'; import {List, MapWrapper, ListWrapper} from 'facade/collection'; import {Reflector} from './reflector'; -import {Key} from './key'; +import {Key, Dependency} from './key'; export class Binding { - constructor(key:Key, factory:Function, dependencies:List, async) { + constructor(key:Key, factory:Function, dependencies:List, providedAsFuture) { this.key = key; this.factory = factory; this.dependencies = dependencies; - this.async = async; + this.providedAsFuture = providedAsFuture; } } @@ -26,7 +26,7 @@ export class BindingBuilder { return new Binding( Key.get(this.token), this.reflector.factoryFor(type), - this._wrapKeys(this.reflector.dependencies(type)), + this.reflector.dependencies(type), false ); } @@ -44,7 +44,7 @@ export class BindingBuilder { return new Binding( Key.get(this.token), this.reflector.convertToFactory(factoryFunction), - this._wrapKeys(dependencies), + this._constructDependencies(dependencies), false ); } @@ -53,12 +53,12 @@ export class BindingBuilder { return new Binding( Key.get(this.token), this.reflector.convertToFactory(factoryFunction), - this._wrapKeys(dependencies), + this._constructDependencies(dependencies), true ); } - _wrapKeys(deps:List) { - return ListWrapper.map(deps, (t) => Key.get(t)); + _constructDependencies(deps:List) { + return ListWrapper.map(deps, (t) => new Dependency(Key.get(t), false)); } } \ No newline at end of file diff --git a/modules/di/src/exceptions.js b/modules/di/src/exceptions.js index c50b31c9fa..023bce6590 100644 --- a/modules/di/src/exceptions.js +++ b/modules/di/src/exceptions.js @@ -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){ this.keys = [key]; this.constructResolvingMessage = constructResolvingMessage; @@ -23,10 +29,6 @@ export class ProviderError extends Error { ListWrapper.push(this.keys, key); this.message = this.constructResolvingMessage(this.keys); } - - toString() { - return this.message; - } } 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){ super(key, function(keys:List) { 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){ this.message = `Invalid binding ${binding}`; } +} - toString() { - return this.message; +export class NoAnnotationError extends DIError { + constructor(type){ + this.message = `Cannot resolve all parameters for ${stringify(type)}`; } } \ No newline at end of file diff --git a/modules/di/src/injector.js b/modules/di/src/injector.js index a810503f17..45b7802435 100644 --- a/modules/di/src/injector.js +++ b/modules/di/src/injector.js @@ -1,6 +1,6 @@ import {Map, List, MapWrapper, ListWrapper} from 'facade/collection'; 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 {Future, FutureWrapper} from 'facade/async'; import {Key} from './key'; @@ -39,21 +39,19 @@ export class Injector { return this._getByKey(key, true); } - _getByKey(key:Key, async) { + _getByKey(key:Key, returnFuture) { 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); if (isPresent(instance)) return instance; var binding = this._get(this._bindings, keyId); - if (isPresent(binding)) { - return this._instantiate(key, binding, async); + return this._instantiate(key, binding, returnFuture); } - if (isPresent(this._parent)) { - return this._parent._getByKey(key, async); + return this._parent._getByKey(key, returnFuture); } throw new NoProviderError(key); @@ -65,8 +63,8 @@ export class Injector { return inj; } - _injector(async){ - return async ? FutureWrapper.value(this) : this; + _injector(returnFuture){ + return returnFuture ? FutureWrapper.value(this) : this; } _get(list:List, index){ @@ -74,39 +72,40 @@ export class Injector { return ListWrapper.get(list, index); } - _instantiate(key:Key, binding:Binding, async) { - if (binding.async && !async) { - throw new AsyncProviderError(key); + _instantiate(key:Key, binding:Binding, returnFuture) { + if (binding.providedAsFuture && !returnFuture) { + throw new AsyncBindingError(key); } - if (async) { - return this._instantiateAsync(key, binding, async); + if (returnFuture) { + return this._instantiateAsync(key, binding); } else { - return this._instantiateSync(key, binding, async); + return this._instantiateSync(key, binding); } } - _instantiateSync(key:Key, binding:Binding, async) { + _instantiateSync(key:Key, binding:Binding) { 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); ListWrapper.set(this._instances, key.id, instance); - if (!binding.async && async) { - return FutureWrapper.value(instance); - } return instance; - } catch (e) { if (e instanceof ProviderError) e.addKey(key); throw e; } } - _instantiateAsync(key:Key, binding:Binding, async):Future { + _instantiateAsync(key:Key, binding:Binding):Future { 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). then(binding.factory). + catch(function(e) { + console.log('sdfsdfsd', e) + //e.addKey(key) + //return e; + }). then(function(instance) { ListWrapper.set(instances, key.id, instance); return instance diff --git a/modules/di/src/key.js b/modules/di/src/key.js index 3d3dd610c7..f2c5714035 100644 --- a/modules/di/src/key.js +++ b/modules/di/src/key.js @@ -3,6 +3,14 @@ import {MapWrapper} from 'facade/collection'; var _allKeys = {}; 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 { constructor(token, id) { this.token = token; diff --git a/modules/di/src/reflector.dart b/modules/di/src/reflector.dart index 99bf783caf..46677c3956 100644 --- a/modules/di/src/reflector.dart +++ b/modules/di/src/reflector.dart @@ -1,7 +1,9 @@ library facade.di.reflector; 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 { factoryFor(Type type) { @@ -27,26 +29,19 @@ class Reflector { return new List.generate(ctor.parameters.length, (int pos) { ParameterMirror p = ctor.parameters[pos]; - if (p.type.qualifiedName == #dynamic) { - var name = MirrorSystem.getName(p.simpleName); - throw "Error getting params for '$type': " - "The '$name' parameter must be typed"; - } + final metadata = p.metadata.map((m) => m.reflectee); - if (p.type is TypedefMirror) { - throw "Typedef '${p.type}' in constructor " - "'${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); + var inject = metadata.where((m) => m is Inject); + var injectFuture = metadata.where((m) => m is InjectFuture); 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 { - return pType; + throw new NoAnnotationError(type); } }, growable:false); } diff --git a/modules/di/src/reflector.es6 b/modules/di/src/reflector.es6 index 5bbb69dbbf..2675346cf7 100644 --- a/modules/di/src/reflector.es6 +++ b/modules/di/src/reflector.es6 @@ -1,5 +1,7 @@ -import {Type} from 'facade/lang'; -import {Inject} from './annotations'; +import {Type, isPresent} from 'facade/lang'; +import {Inject, InjectFuture} from './annotations'; +import {Dependency, Key} from './key'; +import {NoAnnotationError} from './exceptions'; export class Reflector { factoryFor(type:Type) { @@ -12,24 +14,34 @@ export class Reflector { dependencies(type:Type) { var p = type.parameters; - if (p == undefined) return []; - return type.parameters.map((p) => this._extractToken(p)); + if (p == undefined && type.length == 0) return []; + if (p == undefined) throw new NoAnnotationError(type); + return type.parameters.map((p) => this._extractToken(type, p)); } - _extractToken(annotations) { - var type, inject; + _extractToken(constructedType:Type, annotations) { + var type; + for (var paramAnnotation of annotations) { - if (isFunction(paramAnnotation)) { + if (paramAnnotation instanceof Type) { type = paramAnnotation; } 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) { - return typeof value === 'function'; -} + if (isPresent(type)) { + return this._createDependency(type, false); + } else { + throw new NoAnnotationError(constructedType); + } + } + + _createDependency(token, asFuture):Dependency { + return new Dependency(Key.get(token), asFuture); + } +} \ No newline at end of file diff --git a/modules/di/test/di/async_spec.js b/modules/di/test/di/async_spec.js index 211eceae03..3a9d89e26d 100644 --- a/modules/di/test/di/async_spec.js +++ b/modules/di/test/di/async_spec.js @@ -1,5 +1,5 @@ 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'; class UserList {} @@ -10,69 +10,108 @@ function fetchUsers() { class SynchronousUserList {} - class UserController { constructor(list:UserList) { this.list = list; } } +class AsyncUserController { + constructor(@InjectFuture(UserList) userList) { + this.userList = userList; + } +} + 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) - ]); + describe("asyncGet", function () { + it('should return a future', function() { + var injector = new Injector([ + bind(UserList).toAsyncFactory([], fetchUsers) + ]); + var p = injector.asyncGet(UserList); + expect(p).toBeFuture(); + }); - expect(() => injector.get(UserList)) - .toThrowError('Cannot instantiate UserList synchronously. It is provided as a future!'); - }); + it('should return a future when if the binding is sync', function() { + 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() { - var injector = new Injector([ - SynchronousUserList - ]); - var p = injector.asyncGet(SynchronousUserList); - expect(p).toBeFuture(); - }); + it('should return the injector', function() { + var injector = new Injector([]); + var p = injector.asyncGet(Injector); + 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 instantiating a sync binding ' + + 'with an async dependency', function(done) { + var injector = new Injector([ + bind(UserList).toAsyncFactory([], fetchUsers), + UserController + ]); - 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(); + 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 - ]); + describe("get", function () { + it('should throw when instantiating an async binding', function() { + var injector = new Injector([ + bind(UserList).toAsyncFactory([], fetchUsers) + ]); - expect(() => injector.get(UserController)) - .toThrowError('Cannot instantiate UserList synchronously. It is provided as a future! (UserController -> UserList)'); + expect(() => injector.get(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 + // do not construct two instances of the same async dependency if there is one in progress + // cycles }); } \ No newline at end of file diff --git a/modules/di/test/di/injector_spec.js b/modules/di/test/di/injector_spec.js index 66c39802a2..4f3290f419 100644 --- a/modules/di/test/di/injector_spec.js +++ b/modules/di/test/di/injector_spec.js @@ -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'; class Engine {} @@ -33,6 +33,10 @@ class CarWithInject { } } +class NoAnnotations { + constructor(secretDependency){} +} + export function main() { describe('injector', function() { it('should instantiate a class without dependencies', function() { @@ -58,6 +62,11 @@ export function main() { 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() { var injector = new Injector([Engine]);