From 6c8398df9bb6780d11a0e9b768fd35374bbead19 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Thu, 9 Apr 2015 23:17:05 -0700 Subject: [PATCH] fix(di): refactor bindings to support Dart annotations --- gulpfile.js | 2 +- karma-dart.conf.js | 1 + modules/angular2/di.js | 2 +- .../src/core/compiler/element_injector.js | 37 +++-- modules/angular2/src/di/binding.js | 154 +++++++++++++----- modules/angular2/src/di/injector.js | 27 +-- .../angular2/test/di/binding_dart_spec.dart | 62 +++++++ 7 files changed, 207 insertions(+), 78 deletions(-) create mode 100644 modules/angular2/test/di/binding_dart_spec.dart diff --git a/gulpfile.js b/gulpfile.js index 49c0bcab91..4c82dc9a5c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -756,7 +756,6 @@ gulp.task('build/packages.dart', function(done) { 'build/transpile.dart', // Creates the folder structure needed by subsequent tasks. ['build/html.dart', 'build/copy.dart', 'build/multicopy.dart'], 'build/format.dart', - 'build/pubspec.dart', done ); }); @@ -765,6 +764,7 @@ gulp.task('build/packages.dart', function(done) { gulp.task('build.dart', function(done) { runSequence( 'build/packages.dart', + 'build/pubspec.dart', 'build/analyze.dart', 'build/pubbuild.dart', done diff --git a/karma-dart.conf.js b/karma-dart.conf.js index cd42af73d0..238d0bc6ac 100644 --- a/karma-dart.conf.js +++ b/karma-dart.conf.js @@ -13,6 +13,7 @@ module.exports = function(config) { // Unit test files needs to be included. // Karma-dart generates `__adapter_unittest.dart` that imports these files. {pattern: 'modules/*/test/**/*_spec.js', included: true}, + {pattern: 'modules/*/test/**/*_spec.dart', included: true}, {pattern: 'tools/transpiler/spec/**/*_spec.js', included: true}, // These files are not included, they are imported by the unit tests above. diff --git a/modules/angular2/di.js b/modules/angular2/di.js index 89ebdc5aea..ac0f07f165 100644 --- a/modules/angular2/di.js +++ b/modules/angular2/di.js @@ -6,7 +6,7 @@ export {Inject, InjectPromise, InjectLazy, Injectable, Optional, DependencyAnnotation} from './src/di/annotations'; export {Injector} from './src/di/injector'; -export {Binding, Dependency, bind} from './src/di/binding'; +export {Binding, ResolvedBinding, Dependency, bind} from './src/di/binding'; export {Key, KeyRegistry} from './src/di/key'; export {KeyMetadataError, NoProviderError, ProviderError, AsyncBindingError, CyclicDependencyError, InstantiationError, InvalidBindingError, NoAnnotationError} from './src/di/exceptions'; diff --git a/modules/angular2/src/core/compiler/element_injector.js b/modules/angular2/src/core/compiler/element_injector.js index d42e49d4a2..f6edc9e03e 100644 --- a/modules/angular2/src/core/compiler/element_injector.js +++ b/modules/angular2/src/core/compiler/element_injector.js @@ -1,7 +1,7 @@ import {isPresent, isBlank, Type, int, BaseException} from 'angular2/src/facade/lang'; import {Math} from 'angular2/src/facade/math'; import {List, ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; -import {Injector, Key, Dependency, bind, Binding, NoProviderError, ProviderError, CyclicDependencyError} from 'angular2/di'; +import {Injector, Key, Dependency, bind, Binding, ResolvedBinding, NoProviderError, ProviderError, CyclicDependencyError} from 'angular2/di'; import {Parent, Ancestor} from 'angular2/src/core/annotations/visibility'; import {EventEmitter, PropertySetter, Attribute, Query} from 'angular2/src/core/annotations/di'; import * as viewModule from 'angular2/src/core/compiler/view'; @@ -278,7 +278,7 @@ export class DirectiveDependency extends Dependency { } } -export class DirectiveBinding extends Binding { +export class DirectiveBinding extends ResolvedBinding { callOnDestroy:boolean; callOnChange:boolean; callOnAllChangesDone:boolean; @@ -292,13 +292,14 @@ export class DirectiveBinding extends Binding { this.annotation = annotation; } - static createFromBinding(b:Binding, annotation:Directive):Binding { - var deps = ListWrapper.map(b.dependencies, DirectiveDependency.createFrom); - return new DirectiveBinding(b.key, b.factory, deps, b.providedAsPromise, annotation); + static createFromBinding(b:Binding, annotation:Directive):DirectiveBinding { + var rb = b.resolve(); + var deps = ListWrapper.map(rb.dependencies, DirectiveDependency.createFrom); + return new DirectiveBinding(rb.key, rb.factory, deps, rb.providedAsPromise, annotation); } - static createFromType(type:Type, annotation:Directive):Binding { - var binding = bind(type).toClass(type); + static createFromType(type:Type, annotation:Directive):DirectiveBinding { + var binding = new Binding(type, {toClass: type}); return DirectiveBinding.createFromBinding(binding, annotation); } @@ -343,16 +344,16 @@ ElementInjector: */ export class ProtoElementInjector { - _binding0:Binding; - _binding1:Binding; - _binding2:Binding; - _binding3:Binding; - _binding4:Binding; - _binding5:Binding; - _binding6:Binding; - _binding7:Binding; - _binding8:Binding; - _binding9:Binding; + _binding0:ResolvedBinding; + _binding1:ResolvedBinding; + _binding2:ResolvedBinding; + _binding3:ResolvedBinding; + _binding4:ResolvedBinding; + _binding5:ResolvedBinding; + _binding6:ResolvedBinding; + _binding7:ResolvedBinding; + _binding8:ResolvedBinding; + _binding9:ResolvedBinding; _binding0IsComponent:boolean; _keyId0:int; _keyId1:int; @@ -642,7 +643,7 @@ export class ElementInjector extends TreeNode { this._dynamicallyCreatedComponentBinding.key.id; } - _new(binding:Binding) { + _new(binding:ResolvedBinding) { if (this._constructionCounter++ > _MAX_DIRECTIVE_CONSTRUCTION_COUNTER) { throw new CyclicDependencyError(binding.key); } diff --git a/modules/angular2/src/di/binding.js b/modules/angular2/src/di/binding.js index 684b97b6cd..2efbb49257 100644 --- a/modules/angular2/src/di/binding.js +++ b/modules/angular2/src/di/binding.js @@ -1,9 +1,9 @@ -import {Type, isBlank, isPresent} from 'angular2/src/facade/lang'; +import {Type, isBlank, isPresent, CONST} from 'angular2/src/facade/lang'; import {List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {reflector} from 'angular2/src/reflection/reflection'; import {Key} from './key'; import {Inject, InjectLazy, InjectPromise, Optional, DependencyAnnotation} from './annotations'; -import {NoAnnotationError} from './exceptions'; +import {NoAnnotationError, InvalidBindingError} from './exceptions'; export class Dependency { key:Key; @@ -11,6 +11,7 @@ export class Dependency { lazy:boolean; optional:boolean; properties:List; + constructor(key:Key, asPromise:boolean, lazy:boolean, optional:boolean, properties:List) { this.key = key; this.asPromise = asPromise; @@ -24,13 +25,100 @@ export class Dependency { } } +var _EMPTY_LIST = []; // TODO: make const when supported + +/** + * Declaration of a dependency binding. + */ export class Binding { + token; + toClass:Type; + toValue; + toAlias; + toFactory:Function; + toAsyncFactory:Function; + dependencies:List; + + @CONST() + constructor( + token, + { + toClass, + toValue, + toAlias, + toFactory, + toAsyncFactory, + deps + }) { + this.token = token; + this.toClass = toClass; + this.toValue = toValue; + this.toAlias = toAlias; + this.toFactory = toFactory; + this.toAsyncFactory = toAsyncFactory; + this.dependencies = deps; + } + + resolve(): ResolvedBinding { + var factoryFn:Function; + var resolvedDeps; + var isAsync = false; + if (isPresent(this.toClass)) { + factoryFn = reflector.factory(this.toClass); + resolvedDeps = _dependenciesFor(this.toClass); + } else if (isPresent(this.toAlias)) { + factoryFn = (aliasInstance) => aliasInstance; + resolvedDeps = [Dependency.fromKey(Key.get(this.toAlias))]; + } else if (isPresent(this.toFactory)) { + factoryFn = this.toFactory; + resolvedDeps = _constructDependencies(this.toFactory, this.dependencies); + } else if (isPresent(this.toAsyncFactory)) { + factoryFn = this.toAsyncFactory; + resolvedDeps = _constructDependencies(this.toAsyncFactory, this.dependencies); + isAsync = true; + } else { + factoryFn = () => this.toValue; + resolvedDeps = _EMPTY_LIST; + } + + return new ResolvedBinding( + Key.get(this.token), + factoryFn, + resolvedDeps, + isAsync + ); + } + + static resolveAll(bindings:List): List { + var resolvedList = ListWrapper.createFixedSize(bindings.length); + for (var i = 0; i < bindings.length; i++) { + var unresolved = bindings[i]; + var resolved; + if (unresolved instanceof Type) { + resolved = bind(unresolved).toClass(unresolved).resolve(); + } else if (unresolved instanceof Binding) { + resolved = unresolved.resolve(); + } else if (unresolved instanceof List) { + resolved = Binding.resolveAll(unresolved); + } else if (unresolved instanceof BindingBuilder) { + throw new InvalidBindingError(unresolved.token); + } else { + throw new InvalidBindingError(unresolved); + } + resolvedList[i] = resolved; + } + return resolvedList; + } +} + +/// Dependency binding with resolved keys and dependencies. +export class ResolvedBinding { key:Key; factory:Function; - dependencies:List; + dependencies:List; providedAsPromise:boolean; - constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean) { + constructor(key:Key, factory:Function, dependencies:List, providedAsPromise:boolean) { this.key = key; this.factory = factory; this.dependencies = dependencies; @@ -38,66 +126,54 @@ export class Binding { } } +/** + * Provides fluent API for imperative construction of [Binding] objects. + */ export function bind(token):BindingBuilder { return new BindingBuilder(token); } +/** + * Helper class for [bind] function. + */ export class BindingBuilder { token; + constructor(token) { this.token = token; } toClass(type:Type):Binding { - return new Binding( - Key.get(this.token), - reflector.factory(type), - _dependenciesFor(type), - false - ); + return new Binding(this.token, {toClass: type}); } toValue(value):Binding { - return new Binding( - Key.get(this.token), - () => value, - [], - false - ); + return new Binding(this.token, {toValue: value}); } toAlias(aliasToken):Binding { - return new Binding( - Key.get(this.token), - (aliasInstance) => aliasInstance, - [Dependency.fromKey(Key.get(aliasToken))], - false - ); + return new Binding(this.token, {toAlias: aliasToken}); } toFactory(factoryFunction:Function, dependencies:List = null):Binding { - return new Binding( - Key.get(this.token), - factoryFunction, - this._constructDependencies(factoryFunction, dependencies), - false - ); + return new Binding(this.token, { + toFactory: factoryFunction, + deps: dependencies + }); } toAsyncFactory(factoryFunction:Function, dependencies:List = null):Binding { - return new Binding( - Key.get(this.token), - factoryFunction, - this._constructDependencies(factoryFunction, dependencies), - true - ); + return new Binding(this.token, { + toAsyncFactory: factoryFunction, + deps: dependencies + }); } +} - _constructDependencies(factoryFunction:Function, dependencies:List) { - return isBlank(dependencies) ? - _dependenciesFor(factoryFunction) : - ListWrapper.map(dependencies, (t) => Dependency.fromKey(Key.get(t))); - } +function _constructDependencies(factoryFunction:Function, dependencies:List) { + return isBlank(dependencies) ? + _dependenciesFor(factoryFunction) : + ListWrapper.map(dependencies, (t) => Dependency.fromKey(Key.get(t))); } function _dependenciesFor(typeOrFunc):List { diff --git a/modules/angular2/src/di/injector.js b/modules/angular2/src/di/injector.js index 3807abfabe..5910769d39 100644 --- a/modules/angular2/src/di/injector.js +++ b/modules/angular2/src/di/injector.js @@ -1,6 +1,6 @@ import {Map, List, MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; -import {Binding, BindingBuilder, bind} from './binding'; -import {ProviderError, NoProviderError, InvalidBindingError, +import {ResolvedBinding, Binding, BindingBuilder, bind} from './binding'; +import {ProviderError, NoProviderError, AsyncBindingError, CyclicDependencyError, InstantiationError} from './exceptions'; import {FunctionWrapper, Type, isPresent, isBlank} from 'angular2/src/facade/lang'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; @@ -28,7 +28,7 @@ export class Injector { _asyncStrategy: _AsyncInjectorStrategy; _syncStrategy:_SyncInjectorStrategy; constructor(bindings:List, {parent=null, defaultBindings=false}={}) { - var flatten = _flattenBindings(bindings, MapWrapper.create()); + var flatten = _flattenBindings(Binding.resolveAll(bindings), MapWrapper.create()); this._bindings = this._createListOfBindings(flatten); this._instances = this._createInstances(); this._parent = parent; @@ -89,7 +89,7 @@ export class Injector { } } - _resolveDependencies(key:Key, binding:Binding, forceAsync:boolean):List { + _resolveDependencies(key:Key, binding:ResolvedBinding, forceAsync:boolean):List { try { var getDependency = d => this._getByKey(d.key, forceAsync || d.asPromise, d.lazy, d.optional); return ListWrapper.map(binding.dependencies, getDependency); @@ -115,7 +115,7 @@ export class Injector { ListWrapper.get(this._bindings, key.id); if (isBlank(binding) && this._defaultBindings) { - return bind(key.token).toClass(key.token); + return bind(key.token).toClass(key.token).resolve(); } else { return binding; } @@ -166,7 +166,7 @@ class _SyncInjectorStrategy { return this._createInstance(key, binding, deps); } - _createInstance(key:Key, binding:Binding, deps:List) { + _createInstance(key:Key, binding:ResolvedBinding, deps:List) { try { var instance = FunctionWrapper.apply(binding.factory, deps); this.injector._setInstance(key, instance); @@ -227,7 +227,7 @@ class _AsyncInjectorStrategy { return PromiseWrapper.reject(e); } - _findOrCreate(key:Key, binding:Binding, deps:List) { + _findOrCreate(key:Key, binding:ResolvedBinding, deps:List) { try { var instance = this.injector._getInstance(key); if (!_isWaiting(instance)) return instance; @@ -246,21 +246,10 @@ class _AsyncInjectorStrategy { function _flattenBindings(bindings:List, res:Map) { ListWrapper.forEach(bindings, function (b) { - if (b instanceof Binding) { + if (b instanceof ResolvedBinding) { 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 List) { _flattenBindings(b, res); - - } else if (b instanceof BindingBuilder) { - throw new InvalidBindingError(b.token); - - } else { - throw new InvalidBindingError(b); } }); return res; diff --git a/modules/angular2/test/di/binding_dart_spec.dart b/modules/angular2/test/di/binding_dart_spec.dart new file mode 100644 index 0000000000..d958703b59 --- /dev/null +++ b/modules/angular2/test/di/binding_dart_spec.dart @@ -0,0 +1,62 @@ +/// This file contains tests that make sense only in Dart world, such as +/// verifying that things are valid constants. + +import 'dart:mirrors'; +import 'package:angular2/test_lib.dart'; +import 'package:angular2/di.dart'; + +main() { + describe('Binding', () { + it('can create constant from token', () { + expect(const Binding(Foo).token).toBe(Foo); + }); + + it('can create constant from class', () { + expect(const Binding(Foo, toClass: Bar).toClass).toBe(Bar); + }); + + it('can create constant from value', () { + expect(const Binding(Foo, toValue: 5).toValue).toBe(5); + }); + + it('can create constant from alias', () { + expect(const Binding(Foo, toAlias: Bar).toAlias).toBe(Bar); + }); + + it('can create constant from factory', () { + expect(const Binding(Foo, toFactory: fn).toFactory).toBe(fn); + }); + + it('can create constant from async factory', () { + expect(const Binding(Foo, toAsyncFactory: fn).toAsyncFactory).toBe(fn); + }); + + it('can be used in annotation', () { + ClassMirror mirror = reflectType(Annotated); + var bindings = mirror.metadata[0].reflectee.bindings; + expect(bindings.length).toBe(6); + bindings.forEach((b) { + expect(b).toBeA(Binding); + }); + }); + }); +} + +class Foo {} +class Bar extends Foo {} +fn() => null; + +class Annotation { + final List bindings; + const Annotation(this.bindings); +} + +@Annotation(const [ + const Binding(Foo), + const Binding(Foo, toClass: Bar), + const Binding(Foo, toValue: 5), + const Binding(Foo, toAlias: Bar), + const Binding(Foo, toFactory: fn), + const Binding(Foo, toAsyncFactory: fn), +]) +class Annotated {}