diff --git a/example/coffee/electric_heater.js b/example/coffee/electric_heater.js index a785c55..51c2091 100644 --- a/example/coffee/electric_heater.js +++ b/example/coffee/electric_heater.js @@ -1,7 +1,8 @@ -import {Provide} from 'di'; +import {Inject, Provide} from 'di'; import {Heater} from './heater'; +@Inject() @Provide(Heater) export class ElectricHeater { constructor() {} diff --git a/example/coffee/mock_heater.js b/example/coffee/mock_heater.js index 7d918b6..76f68fc 100644 --- a/example/coffee/mock_heater.js +++ b/example/coffee/mock_heater.js @@ -1,7 +1,8 @@ -import {Provide} from 'di'; +import {Inject, Provide} from 'di'; import {Heater} from './heater'; +@Inject() @Provide(Heater) export class MockHeater { on() {} diff --git a/example/kitchen-di/electricity.js b/example/kitchen-di/electricity.js index fe8fb75..6cae719 100644 --- a/example/kitchen-di/electricity.js +++ b/example/kitchen-di/electricity.js @@ -1,2 +1,4 @@ +import {Inject} from 'di'; +@Inject export class Electricity {} diff --git a/example/kitchen-di/mock_heater.js b/example/kitchen-di/mock_heater.js index ff5660d..89e2fc1 100644 --- a/example/kitchen-di/mock_heater.js +++ b/example/kitchen-di/mock_heater.js @@ -1,6 +1,8 @@ -import {Provide} from 'di'; +import {Inject, Provide} from 'di'; import {Heater} from './coffee_maker/heater'; + +@Inject @Provide(Heater) export class MockHeater { constructor() {} diff --git a/example/kitchen-di/skillet.js b/example/kitchen-di/skillet.js index 5ad60f6..102a4f2 100644 --- a/example/kitchen-di/skillet.js +++ b/example/kitchen-di/skillet.js @@ -1,4 +1,7 @@ +import {Inject} from 'di'; + +@Inject export class Skillet { constructor() {} diff --git a/src/annotations.js b/src/annotations.js index aabd957..e746c80 100644 --- a/src/annotations.js +++ b/src/annotations.js @@ -82,6 +82,23 @@ function hasAnnotation(fn, annotationClass) { } +function hasParameterAnnotation(fn, AnnotationClass) { + if (!fn.parameters || fn.parameters.length === 0) { + return false; + } + + for (var parameterAnnotations of fn.parameters) { + for (var annotation of parameterAnnotations) { + if (annotation instanceof AnnotationClass) { + return true; + } + } + } + + return false; +} + + // Read annotations on a function or class and collect "interesting" metadata: function readAnnotations(fn) { var collectedAnnotations = { @@ -143,6 +160,7 @@ function readAnnotations(fn) { export { annotate, hasAnnotation, + hasParameterAnnotation, readAnnotations, SuperConstructor, diff --git a/src/injector.js b/src/injector.js index 0dd05a9..581899d 100644 --- a/src/injector.js +++ b/src/injector.js @@ -2,12 +2,14 @@ import { annotate, readAnnotations, hasAnnotation, + hasParameterAnnotation, Provide as ProvideAnnotation, + Inject as InjectAnnotation, TransientScope as TransientScopeAnnotation } from './annotations'; -import {isFunction, toString} from './util'; +import {isFunction, isObject, toString} from './util'; import {profileInjector} from './profiler'; -import {createProviderFromFnOrClass} from './providers'; +import {createProviderFromFnOrClass, createProviderFromValue} from './providers'; function constructResolvingMessage(resolving, token) { @@ -143,6 +145,7 @@ class Injector { return args[ii + 1]; }; + annotate(fn, new InjectAnnotation()); annotate(fn, new ProvideAnnotation(args[ii])); return fn; @@ -176,9 +179,15 @@ class Injector { provider = this.providers.get(token); // No provider defined (overriden), use the default provider (token). - if (!provider && isFunction(token) && !this._hasProviderFor(token)) { - provider = createProviderFromFnOrClass(token, readAnnotations(token)); - this.providers.set(token, provider); + if (!provider && !this._hasProviderFor(token)) { + if (isFunction(token) && (hasAnnotation(token, InjectAnnotation)) || hasParameterAnnotation(token, InjectAnnotation)) { + provider = createProviderFromFnOrClass(token, readAnnotations(token)); + this.providers.set(token, provider); + } else if (isFunction(token) || isObject(token)) { + // If the token is an object or a non-annotated function, we inject it as a value. + provider = createProviderFromValue(token); + this.providers.set(token, provider); + } } if (!provider) { diff --git a/src/providers.js b/src/providers.js index f22451e..aa24763 100644 --- a/src/providers.js +++ b/src/providers.js @@ -127,6 +127,24 @@ class FactoryProvider { } +// ValueProvider knows how to return values. +// +// This is a trivial provider that just always returns the same value, +// without requiring any arguments. +class ValueProvider { + constructor(value) { + this.provider = function() {}; + this.params = []; + this.isPromise = false; + this._value = value; + } + + create() { + return this._value; + } +} + + export function createProviderFromFnOrClass(fnOrClass, annotations) { if (isClass(fnOrClass)) { return new ClassProvider(fnOrClass, annotations.params, annotations.provide.isPromise); @@ -134,3 +152,7 @@ export function createProviderFromFnOrClass(fnOrClass, annotations) { return new FactoryProvider(fnOrClass, annotations.params, annotations.provide.isPromise); } + +export function createProviderFromValue(value) { + return new ValueProvider(value); +} diff --git a/test/annotations.spec.js b/test/annotations.spec.js index 3c18d49..484bd03 100644 --- a/test/annotations.spec.js +++ b/test/annotations.spec.js @@ -1,5 +1,6 @@ import { hasAnnotation, + hasParameterAnnotation, readAnnotations, Inject, InjectLazy, @@ -43,6 +44,28 @@ describe('hasAnnotation', function() { }); +describe('hasParameterAnnotation', function() { + + it('should return false if no annotation', function() { + class FooAnnotation {} + + function nothing() {} + + expect(hasParameterAnnotation(nothing, FooAnnotation)).toBe(false); + }); + + + it('should return if at least one parameter has specific annotation', function() { + class FooAnnotation {} + class BarType {} + + function nothing(one: BarType, @FooAnnotation two: BarType) {} + + expect(hasParameterAnnotation(nothing, FooAnnotation)).toBe(true); + }); +}); + + describe('readAnnotations', function() { it('should read @Provide', function() { diff --git a/test/async.spec.js b/test/async.spec.js index 8b1e90e..b466d20 100644 --- a/test/async.spec.js +++ b/test/async.spec.js @@ -5,11 +5,13 @@ import {Injector} from '../src/injector'; class UserList {} // An async provider. +@Inject @ProvidePromise(UserList) function fetchUsers() { return Promise.resolve(new UserList); } +@Inject class SynchronousUserList {} class UserController { diff --git a/test/fixtures/car.js b/test/fixtures/car.js index 29b74ed..b790a3c 100644 --- a/test/fixtures/car.js +++ b/test/fixtures/car.js @@ -23,6 +23,8 @@ export class CyclicEngine { // @Inject(Engine) annotate(Car, new Inject(Engine)); +// @Inject +annotate(createEngine, new Inject()); // @Provide(Engine) annotate(createEngine, new Provide(Engine)); diff --git a/test/injector.spec.js b/test/injector.spec.js index 15be73e..3583781 100644 --- a/test/injector.spec.js +++ b/test/injector.spec.js @@ -9,6 +9,7 @@ import {module as shinyHouseModule} from './fixtures/shiny_house'; describe('injector', function() { it('should instantiate a class without dependencies', function() { + @Inject class Car { constructor() {} start() {} @@ -22,6 +23,7 @@ describe('injector', function() { it('should resolve dependencies based on @Inject annotation', function() { + @Inject class Engine { start() {} } @@ -57,6 +59,7 @@ describe('injector', function() { start() {} } + @Inject @Provide(Engine) class MockEngine { start() {} @@ -73,6 +76,7 @@ describe('injector', function() { it('should allow factory function', function() { class Size {} + @Inject @Provide(Size) function computeSize() { return 0; @@ -86,10 +90,12 @@ describe('injector', function() { it('should use type annotations when available', function() { + @Inject class Engine { start() {} } + @Inject class Car { constructor(e: Engine) { this.engine = e; @@ -110,6 +116,7 @@ describe('injector', function() { start() {} } + @Inject class MockEngine extends Engine { start() {} } @@ -131,14 +138,17 @@ describe('injector', function() { it('should use mixed @Inject with type annotations', function() { + @Inject class Engine { start() {} } + @Inject class Bumper { start() {} } + @Inject class Car { constructor(e: Engine, @Inject(Bumper) b) { this.engine = e; @@ -157,6 +167,7 @@ describe('injector', function() { it('should cache instances', function() { + @Inject class Car { constructor() {} start() {} @@ -194,6 +205,7 @@ describe('injector', function() { it('should show the full path when error happens in a constructor', function() { + @Inject class Engine { constructor() { throw new Error('This engine is broken!'); @@ -213,6 +225,7 @@ describe('injector', function() { it('should support "super" to call a parent constructor', function() { + @Inject class Something {} class Parent { @@ -240,7 +253,10 @@ describe('injector', function() { it('should support "super" to call multiple parent constructors', function() { + @Inject class Foo {} + + @Inject class Bar {} class Parent { @@ -345,9 +361,51 @@ describe('injector', function() { }); + describe('default providers', function() { + + it('should inject the value for object tokens', function() { + var foo = { + bar: true + }; + + @Inject(foo) + class Bar { + constructor(foo) { + this.foo = foo; + } + } + + var injector = new Injector(); + var bar = injector.get(Bar); + + expect(bar.foo).toBe(foo); + }); + + + it('should inject the value for non-annotated functions', function() { + var fs = { + readFile: function() {} + }; + + @Inject(fs.readFile) + class Foo { + constructor(readFile) { + this.readFile = readFile; + } + } + + var injector = new Injector(); + var foo = injector.get(Foo); + + expect(foo.readFile).toBe(fs.readFile); + }); + }); + + describe('transient', function() { it('should never cache', function() { @TransientScope + @Inject class Foo {} var injector = new Injector(); @@ -359,6 +417,7 @@ describe('injector', function() { describe('child', function() { it('should load instances from parent injector', function() { + @Inject class Car { start() {} } @@ -374,6 +433,7 @@ describe('injector', function() { it('should create new instance in a child injector', function() { + @Inject class Car { start() {} } @@ -397,6 +457,7 @@ describe('injector', function() { it('should force new instances by annotation', function() { class RouteScope {} + @Inject class Engine { start() {} } @@ -425,10 +486,12 @@ describe('injector', function() { it('should force new instances by annotation using overriden provider', function() { class RouteScope {} + @Inject class Engine { start() {} } + @Inject @RouteScope @Provide(Engine) class MockEngine { @@ -451,12 +514,14 @@ describe('injector', function() { it('should force new instance by annotation using the lowest overriden provider', function() { class RouteScope {} + @Inject @RouteScope class Engine { constructor() {} start() {} } + @Inject @RouteScope @Provide(Engine) class MockEngine { @@ -464,6 +529,7 @@ describe('injector', function() { start() {} } + @Inject @RouteScope @Provide(Engine) class DoubleMockEngine { @@ -508,6 +574,7 @@ describe('injector', function() { it('should instantiate lazily', function() { var constructorSpy = jasmine.createSpy('constructor'); + @Inject class ExpensiveEngine { constructor() { constructorSpy(); @@ -540,6 +607,7 @@ describe('injector', function() { it('should instantiate lazily from a parent injector', function() { var constructorSpy = jasmine.createSpy('constructor'); + @Inject class ExpensiveEngine { constructor() { constructorSpy();