diff --git a/modules/core/src/compiler/compiler.js b/modules/core/src/compiler/compiler.js index d1ecf77711..80051b60a7 100644 --- a/modules/core/src/compiler/compiler.js +++ b/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'; diff --git a/modules/core/src/compiler/template_loader.js b/modules/core/src/compiler/template_loader.js index 346a9343c4..b71b688447 100644 --- a/modules/core/src/compiler/template_loader.js +++ b/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 { diff --git a/modules/di/src/annotations.js b/modules/di/src/annotations.js new file mode 100644 index 0000000000..b478cd4da4 --- /dev/null +++ b/modules/di/src/annotations.js @@ -0,0 +1,7 @@ +//TODO: vsavkin: uncomment once const constructor are supported +//export class Inject { +// @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 new file mode 100644 index 0000000000..12ec1ba3d7 --- /dev/null +++ b/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; + this.async = async; + } +} + +export function bind(token):BindingBuilder { + return new BindingBuilder(token); +} + +export class BindingBuilder { + 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)); + } +} \ No newline at end of file diff --git a/modules/di/src/di.js b/modules/di/src/di.js index 57b512dfc9..69fde04c59 100644 --- a/modules/di/src/di.js +++ b/modules/di/src/di.js @@ -1 +1,5 @@ -export * from './module'; \ No newline at end of file +export * from './injector'; +export * from './binding'; +export * from './key'; +export * from './module'; +export {Inject} from 'facade/di/reflector'; diff --git a/modules/di/src/exceptions.js b/modules/di/src/exceptions.js new file mode 100644 index 0000000000..d2bc10bd77 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/modules/di/src/injector.js b/modules/di/src/injector.js new file mode 100644 index 0000000000..63892d7fb9 --- /dev/null +++ b/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) { + 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; +} diff --git a/modules/di/src/key.js b/modules/di/src/key.js index 3495ba68ce..3d3dd610c7 100644 --- a/modules/di/src/key.js +++ b/modules/di/src/key.js @@ -1,3 +1,25 @@ -export class Key { +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; + } } \ No newline at end of file diff --git a/modules/di/src/module.js b/modules/di/src/module.js index 80d6018593..90361de181 100644 --- a/modules/di/src/module.js +++ b/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') - 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 {} \ No newline at end of file diff --git a/modules/di/test/di/async_spec.js b/modules/di/test/di/async_spec.js new file mode 100644 index 0000000000..e3e6094742 --- /dev/null +++ b/modules/di/test/di/async_spec.js @@ -0,0 +1,76 @@ +import {ddescribe, describe, it, iit, xit, expect, beforeEach} from 'test_lib/test_lib'; +import {Injector, Inject, bind, Key} from 'di/di'; +import {Future, FutureWrapper} from 'facade/async'; + +class UserList {} + +function fetchUsers() { + return FutureWrapper.value(new UserList()); +} + +class SynchronousUserList {} + + +class UserController { + constructor(list:UserList) { + this.list = list; + } +} + +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) + ]); + + expect(() => injector.get(UserList)) + .toThrowError('Cannot instantiate UserList synchronously. It is provided as a future!'); + }); + + 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 provide itself', function() { + var injector = new Injector([]); + var p = injector.asyncGet(Injector); + expect(p).toBeFuture(); + }); + + 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(); + }); + }); + + it('should throw when a dependency is async', 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)'); + }); + }); +} \ No newline at end of file diff --git a/modules/di/test/di/injector_spec.js b/modules/di/test/di/injector_spec.js new file mode 100644 index 0000000000..b1ec0d6b8c --- /dev/null +++ b/modules/di/test/di/injector_spec.js @@ -0,0 +1,144 @@ +import {describe, it, expect, beforeEach} from 'test_lib/test_lib'; +import {Injector, Inject, bind} from 'di/di'; + +class Engine {} +class Dashboard {} +class TurboEngine extends Engine{} + +class Car { + constructor(engine:Engine) { + this.engine = engine; + } +} + +class CarWithDashboard { + constructor(engine:Engine, dashboard:Dashboard) { + this.engine = engine; + this.dashboard = dashboard; + } +} + +class SportsCar extends Car { + constructor(engine:Engine) { + super(engine); + } +} + +class CarWithInject { + constructor(@Inject(TurboEngine) engine:Engine) { + this.engine = engine; + } +} + +export function main() { + describe('injector', function() { + it('should instantiate a class without dependencies', function() { + var injector = new Injector([Engine]); + var engine = injector.get(Engine); + + expect(engine).toBeAnInstanceOf(Engine); + }); + + it('should resolve dependencies based on type information', function() { + var injector = new Injector([Engine, Car]); + var car = injector.get(Car); + + expect(car).toBeAnInstanceOf(Car); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should resolve dependencies based on @Inject annotation', function() { + var injector = new Injector([TurboEngine, Engine, CarWithInject]); + var car = injector.get(CarWithInject); + + expect(car).toBeAnInstanceOf(CarWithInject); + expect(car.engine).toBeAnInstanceOf(TurboEngine); + }); + + it('should cache instances', function() { + var injector = new Injector([Engine]); + + var e1 = injector.get(Engine); + var e2 = injector.get(Engine); + + expect(e1).toBe(e2); + }); + + it('should bind to a value', function() { + var injector = new Injector([ + bind(Engine).toValue("fake engine") + ]); + + var engine = injector.get(Engine); + expect(engine).toEqual("fake engine"); + }); + + it('should bind to a factory', function() { + var injector = new Injector([ + Engine, + bind(Car).toFactory([Engine], (e) => new SportsCar(e)) + ]); + + var car = injector.get(Car); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should use non-type tokens', function() { + var injector = new Injector([ + bind('token').toValue('value') + ]); + + expect(injector.get('token')).toEqual('value'); + }); + + it('should throw when given invalid bindings', function() { + expect(() => new Injector(["blah"])).toThrowError('Invalid binding blah'); + expect(() => new Injector([bind("blah")])).toThrowError('Invalid binding blah'); + }); + + describe("child", function () { + it('should load instances from parent injector', function() { + var parent = new Injector([Engine]); + var child = parent.createChild([]); + + var engineFromParent = parent.get(Engine); + var engineFromChild = child.get(Engine); + + expect(engineFromChild).toBe(engineFromParent); + }); + + it('should create new instance in a child injector', function() { + var parent = new Injector([Engine]); + var child = parent.createChild([ + bind(Engine).toClass(TurboEngine) + ]); + + var engineFromParent = parent.get(Engine); + var engineFromChild = child.get(Engine); + + expect(engineFromParent).not.toBe(engineFromChild); + expect(engineFromChild).toBeAnInstanceOf(TurboEngine); + }); + }); + + it('should provide itself', function() { + var parent = new Injector([]); + var child = parent.createChild([]); + + expect(child.get(Injector)).toBe(child); + }); + + it('should throw when no provider defined', function() { + var injector = new Injector([]); + expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!'); + }); + + it('should show the full path when no provider', function() { + var injector = new Injector([CarWithDashboard, Engine]); + + expect(() => injector.get(CarWithDashboard)). + toThrowError('No provider for Dashboard! (CarWithDashboard -> Dashboard)'); + }); + }); +} diff --git a/modules/di/test/di/key_spec.js b/modules/di/test/di/key_spec.js new file mode 100644 index 0000000000..132be4bb58 --- /dev/null +++ b/modules/di/test/di/key_spec.js @@ -0,0 +1,14 @@ +import {describe, it, expect} from 'test_lib/test_lib'; +import {Key} from 'di/di'; + +export function main () { + describe("key", function () { + it('should be equal to another key if type is the same', function () { + expect(Key.get('car')).toBe(Key.get('car')); + }); + + it('should not be equal to another key if types are different', function () { + expect(Key.get('car')).not.toBe(Key.get('porsche')); + }); + }); +} \ No newline at end of file diff --git a/modules/facade/src/async.dart b/modules/facade/src/async.dart new file mode 100644 index 0000000000..6ea2f23049 --- /dev/null +++ b/modules/facade/src/async.dart @@ -0,0 +1,14 @@ +library angular.core.facade.async; + +import 'dart:async'; +export 'dart:async' show Future; + +class FutureWrapper { + static Future value(obj) { + return new Future.value(obj); + } + + static Future wait(List futures){ + return Future.wait(futures); + } +} diff --git a/modules/facade/src/async.es6 b/modules/facade/src/async.es6 new file mode 100644 index 0000000000..7bf7568bca --- /dev/null +++ b/modules/facade/src/async.es6 @@ -0,0 +1,12 @@ +export var Future = Promise; + +export class FutureWrapper { + static value(obj):Future { + return Future.resolve(obj); + } + + static wait(futures):Future { + if (futures.length == 0) return Future.resolve([]); + return Future.all(futures); + } +} \ No newline at end of file diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index f9d7fccf5b..57f0510cec 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -8,14 +8,25 @@ class MapWrapper { static get(m, k) => m[k]; static void set(m, k, v){ m[k] = v; } static contains(m, k) => m.containsKey(k); + static forEach(m, fn) { + m.forEach(fn); + } } class ListWrapper { static List clone(List l) => new List.from(l); static List create() => new List(); + static List createFixedSize(int size) => new List(size); static get(m, k) => m[k]; static void set(m, k, v) { m[k] = v; } static contains(m, k) => m.containsKey(k); + static map(list, fn) => list.map(fn).toList(); + static forEach(list, fn) { + list.forEach(fn); + } + static last(list) { + return list.last; + } static void push(List l, e) { l.add(e); } } diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index f2fe82b0fc..a72002197e 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -6,21 +6,41 @@ export class MapWrapper { static create():HashMap { return new HashMap(); } static get(m, k) { return m[k]; } static set(m, k, v) { m[k] = v; } - static contains(m, k) { return m.containsKey(k); } + static contains(m, k) { return m[k] != undefined; } + static forEach(m, fn) { + for(var k in m) { + fn(k, m[k]); + } + } } export class ListWrapper { static create():List { return new List(); } + static createFixedSize(size):List { return new List(); } static get(m, k) { return m[k]; } static set(m, k, v) { m[k] = v; } static clone(array) { return Array.prototype.slice.call(array, 0); } - static push(l, e) { l.push(e); } + static map(array, fn) { + return array.map(fn); + } + static forEach(array, fn) { + for(var p of array) { + fn(p); + } + } + static push(array, el) { + array.push(el); + } + static last(array) { + if (!array || array.length == 0) return null; + return array[array.length - 1]; + } } export class SetWrapper { static createFromList(lst:List) { return new Set(lst); } static has(s:Set, key):boolean { return s.has(key); } -} +} \ No newline at end of file diff --git a/modules/facade/src/di/reflector.dart b/modules/facade/src/di/reflector.dart new file mode 100644 index 0000000000..8b2e42bdb0 --- /dev/null +++ b/modules/facade/src/di/reflector.dart @@ -0,0 +1,57 @@ +library facade.di.reflector; + +import 'dart:mirrors'; + +class Inject { + final Object token; + const Inject(this.token); +} + +class Reflector { + factoryFor(Type type) { + return _generateFactory(type); + } + + convertToFactory(Function factory) { + return (args) => Function.apply(factory, args); + } + + Function _generateFactory(Type type) { + ClassMirror classMirror = reflectType(type); + MethodMirror ctor = classMirror.declarations[classMirror.simpleName]; + Function create = classMirror.newInstance; + Symbol name = ctor.constructorName; + return (args) => create(name, args).reflectee; + } + + dependencies(Type type) { + ClassMirror classMirror = reflectType(type); + MethodMirror ctor = classMirror.declarations[classMirror.simpleName]; + + 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"; + } + + 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); + + if (inject.isNotEmpty) { + return inject.first.token; + } else { + return pType; + } + }, growable:false); + } +} \ No newline at end of file diff --git a/modules/facade/src/di/reflector.es6 b/modules/facade/src/di/reflector.es6 new file mode 100644 index 0000000000..78a2099d5c --- /dev/null +++ b/modules/facade/src/di/reflector.es6 @@ -0,0 +1,41 @@ +import {Type} from 'facade/lang'; + +//TODO: vsvakin: remove when const constructors are implemented +export class Inject { + constructor(token){ + this.token = token; + } +} + +export class Reflector { + factoryFor(type:Type) { + return (args) => new type(...args); + } + + convertToFactory(factoryFunction:Function) { + return (args) => factoryFunction(...args); + } + + dependencies(type:Type) { + var p = type.parameters; + if (p == undefined) return []; + return type.parameters.map((p) => this._extractToken(p)); + } + + _extractToken(annotations) { + var type, inject; + for (var paramAnnotation of annotations) { + if (isFunction(paramAnnotation)) { + type = paramAnnotation; + + } else if (paramAnnotation instanceof Inject) { + inject = paramAnnotation.token; + } + } + return inject != undefined ? inject : type; + } +} + +function isFunction(value) { + return typeof value === 'function'; +} diff --git a/modules/facade/src/lang.dart b/modules/facade/src/lang.dart index ca5004b9c2..56126b5796 100644 --- a/modules/facade/src/lang.dart +++ b/modules/facade/src/lang.dart @@ -1,7 +1,6 @@ -library angular.core.facade.async; +library angular.core.facade.lang; -export 'dart:async' show Future; -export 'dart:core' show Type, int; +export 'dart:core' show Type; class FIELD { final String definition; @@ -19,6 +18,10 @@ class IMPLEMENTS { const IMPLEMENTS(this.interfaceClass); } +bool isPresent(obj) => obj != null; +bool isBlank(obj) => obj == null; + +String humanize(obj) => obj.toString(); class StringWrapper { static String fromCharCode(int code) { diff --git a/modules/facade/src/lang.es6 b/modules/facade/src/lang.es6 index 3adf91399d..8f8feb1bc7 100644 --- a/modules/facade/src/lang.es6 +++ b/modules/facade/src/lang.es6 @@ -1,4 +1,3 @@ -export var Future = Promise; export var Type = Function; export class FIELD { @@ -12,6 +11,30 @@ export class ABSTRACT {} export class IMPLEMENTS {} +export function isPresent(obj){ + return obj != undefined && obj != null; +} + +export function isBlank(obj){ + return obj == undefined || obj == null; +} + +export function humanize(token) { + if (typeof token === 'string') { + return token; + } + + if (token === undefined || token === null) { + return '' + token; + } + + if (token.name) { + return token.name; + } + + return token.toString(); +} + export class StringWrapper { static fromCharCode(code:int) { return String.fromCharCode(code); diff --git a/modules/test_lib/src/test_lib.dart b/modules/test_lib/src/test_lib.dart index f2dcb370d3..a4b1d942d8 100644 --- a/modules/test_lib/src/test_lib.dart +++ b/modules/test_lib/src/test_lib.dart @@ -1,5 +1,45 @@ library test_lib.test_lib; -export 'package:guinness/guinness.dart' show - describe, ddescribe, xdescribe, - it, xit, iit, - beforeEach, afterEach, expect; + +import 'package:guinness/guinness_html.dart' as gns; +export 'package:guinness/guinness_html.dart'; +import 'package:unittest/unittest.dart' hide expect; +import 'dart:mirrors'; +import 'dart:async'; + +Expect expect(actual, [matcher]) { + final expect = new Expect(actual); + if (matcher != null) expect.to(matcher); + return expect; +} + +class Expect extends gns.Expect { + Expect(actual) : super(actual); + + void toThrowError(message) => this.toThrowWith(message: message); + void toBeFuture() => _expect(actual is Future, equals(true)); + Function get _expect => gns.guinness.matchers.expect; +} + +it(name, fn) { + gns.it(name, _handleAsync(fn)); +} + +iit(name, fn) { + gns.iit(name, _handleAsync(fn)); +} + +_handleAsync(fn) { + ClosureMirror cm = reflect(fn); + MethodMirror mm = cm.function; + + var completer = new Completer(); + + if (mm.parameters.length == 1) { + return () { + cm.apply([completer.complete]); + return completer.future; + }; + } + + return fn; +} \ No newline at end of file diff --git a/modules/test_lib/src/test_lib.es6 b/modules/test_lib/src/test_lib.es6 index 66a84826d4..e10a436ec5 100644 --- a/modules/test_lib/src/test_lib.es6 +++ b/modules/test_lib/src/test_lib.es6 @@ -16,3 +16,35 @@ window.print = function(msg) { window.console.log(msg); } }; + +window.beforeEach(function() { + jasmine.addMatchers({ + toBeFuture: function() { + return { + compare: function (actual, expectedClass) { + var pass = typeof actual === 'object' && typeof actual.then === 'function'; + return { + pass: pass, + get message() { + return 'Expected ' + actual + ' to be a future'; + } + }; + } + }; + }, + + toBeAnInstanceOf: function() { + return { + compare: function(actual, expectedClass) { + var pass = typeof actual === 'object' && actual instanceof expectedClass; + return { + pass: pass, + get message() { + return 'Expected ' + actual + ' to be an instance of ' + expectedClass; + } + }; + } + }; + } + }); +}); diff --git a/tools/transpiler/src/codegeneration/ClassTransformer.js b/tools/transpiler/src/codegeneration/ClassTransformer.js index 056ca57c50..a61d73c4e6 100644 --- a/tools/transpiler/src/codegeneration/ClassTransformer.js +++ b/tools/transpiler/src/codegeneration/ClassTransformer.js @@ -73,7 +73,8 @@ export class ClassTransformer extends ParseTreeTransformer { // Collect all fields, defined in the constructor. elementTree.body.statements.forEach(function(statement) { var exp = statement.expression; - if (exp.type === BINARY_EXPRESSION && + if (exp && + exp.type === BINARY_EXPRESSION && exp.operator.type === EQUAL && exp.left.type === MEMBER_EXPRESSION && exp.left.operand.type === THIS_EXPRESSION) { @@ -170,7 +171,8 @@ export class ClassTransformer extends ParseTreeTransformer { var superCall = null; body.statements.forEach(function (statement) { - if (statement.expression.type === CALL_EXPRESSION && + if (statement.expression && + statement.expression.type === CALL_EXPRESSION && statement.expression.operand.type === SUPER_EXPRESSION) { superCall = statement.expression; } else { diff --git a/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js b/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js index e967fdbaee..b0e9690960 100644 --- a/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js +++ b/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js @@ -11,7 +11,8 @@ import { OPEN_CURLY, OPEN_PAREN, SEMI_COLON, - STAR + STAR, + STATIC } from 'traceur/src/syntax/TokenType'; import {ParseTreeWriter as JavaScriptParseTreeWriter, ObjectLiteralExpression} from 'traceur/src/outputgeneration/ParseTreeWriter';