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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
//TODO: vsavkin: uncomment once const constructor are supported | ||
//export class Inject { | ||
// @CONST | ||
// constructor(token){ | ||
// this.token = token; | ||
// } | ||
//} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we add documentation? It is not clear to me what |
||
this.async = async; | ||
} | ||
} | ||
|
||
export function bind(token):BindingBuilder { | ||
return new BindingBuilder(token); | ||
} | ||
|
||
export class BindingBuilder { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the reason behind BindingBuilder rather than Dart way of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few reasons why I prefer this approach over
instead of
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)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
export * from './module'; | ||
export * from './injector'; | ||
export * from './binding'; | ||
export * from './key'; | ||
export * from './module'; | ||
export {Inject} from 'facade/di/reflector'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they work now.