Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dynamic di #50

Merged
merged 1 commit into from Oct 3, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion modules/core/src/compiler/compiler.js
@@ -1,4 +1,5 @@
import {Future, Type} from 'facade/lang';
import {Type} from 'facade/lang';
import {Future} from 'facade/async';
import {Element} from 'facade/dom';
//import {ProtoView} from './view';
import {TemplateLoader} from './template_loader';
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/compiler/template_loader.js
@@ -1,4 +1,4 @@
import {Future} from 'facade/lang';
import {Future} from 'facade/async';
//import {Document} from 'facade/dom';

export class TemplateLoader {
Expand Down
7 changes: 7 additions & 0 deletions modules/di/src/annotations.js
@@ -0,0 +1,7 @@
//TODO: vsavkin: uncomment once const constructor are supported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they work now.

//export class Inject {
// @CONST
// constructor(token){
// this.token = token;
// }
//}
64 changes: 64 additions & 0 deletions modules/di/src/binding.js
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add documentation? It is not clear to me what dependencies on async is for?

this.async = async;
}
}

export function bind(token):BindingBuilder {
return new BindingBuilder(token);
}

export class BindingBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason behind BindingBuilder rather than Dart way of module.bind(Type, {toValue: 123})?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After seeing how it is used, I think I like this better. But still would love to hear your reasoning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few reasons why I prefer this approach over module.bind.

  1. Module does not have any responsibilities apart from collecting bindings. Not having this abstraction makes the API a bit simpler. You only have Injector and Binding.
  2. This approach makes patterns very explicit. When using module.bind, there are some combinations of keys that are valid and some that are not (e.g., "toFactory and inject" is good, and "toValue and inject" is not). When using BindingBuilder there are no invalid combinations.
  3. It makes declaring app services nicer:
@Component(
    appServices: [Foo, Bar]
)

instead of

@Component(
    appServices: setupAppServices
)

where `setupAppServices` is: 

setupAppServices(m) {
    m.bind(Foo)
    m.bind(Bar)
}

In Dart those have to be const expressions, so it won't matter that much. In JS though it makes the API nicer.

I think the two approaches are "isomorphic". Meaning that you can build one API on top of the other one.

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));
}
}
6 changes: 5 additions & 1 deletion modules/di/src/di.js
@@ -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 changes: 52 additions & 0 deletions modules/di/src/exceptions.js
@@ -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 changes: 133 additions & 0 deletions modules/di/src/injector.js
@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need resolving? A better way to do it is what we do in Dart. We let it run to certain depth, and than record the path in catch block. See: https://github.com/angular/di.dart/blob/master/lib/src/injector.dart#L181 and https://github.com/angular/di.dart/blob/master/lib/src/errors.dart#L20

Notice how we throw ResolvingException and then add to it in catch block. This makes sure that we don't spend time keeping track of things unless there is an error.

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;
}
22 changes: 22 additions & 0 deletions modules/di/src/key.js
@@ -1,3 +1,25 @@
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;
}
}
27 changes: 1 addition & 26 deletions modules/di/src/module.js
@@ -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 {}