diff --git a/ng-metadata.test.ts b/ng-metadata.test.ts index 98d3fe8..7343df1 100644 --- a/ng-metadata.test.ts +++ b/ng-metadata.test.ts @@ -9,6 +9,9 @@ import './test/pipe.spec'; import './test/life_cycle.spec'; import './test/providers.spec'; +import './test/di/decorators.spec'; +import './test/util/decorators.spec'; + describe( 'ng-metadata', ()=> { } ); diff --git a/src/di/decorators.ts b/src/di/decorators.ts new file mode 100644 index 0000000..bcce2d4 --- /dev/null +++ b/src/di/decorators.ts @@ -0,0 +1,87 @@ +import { + InjectMetadata, + OptionalMetadata, + InjectableMetadata, + SelfMetadata, + HostMetadata, + SkipSelfMetadata +} from './metadata'; +import {makeDecorator, makeParamDecorator} from '../util/decorators'; + +/** + * Factory for creating {@link InjectMetadata}. + */ +export interface InjectFactory { + (token: any): any; + new (token: any): InjectMetadata; +} + +/** + * Factory for creating {@link OptionalMetadata}. + */ +export interface OptionalFactory { + (): any; + new (): OptionalMetadata; +} + +/** + * Factory for creating {@link InjectableMetadata}. + */ +export interface InjectableFactory { + (): any; + new (): InjectableMetadata; +} + +/** + * Factory for creating {@link SelfMetadata}. + */ +export interface SelfFactory { + (): any; + new (): SelfMetadata; +} + +/** + * Factory for creating {@link HostMetadata}. + */ +export interface HostFactory { + (): any; + new (): HostMetadata; +} + +/** + * Factory for creating {@link SkipSelfMetadata}. + */ +export interface SkipSelfFactory { + (): any; + new (): SkipSelfMetadata; +} + +/** + * Factory for creating {@link InjectMetadata}. + */ +export const Inject: InjectFactory = makeParamDecorator(InjectMetadata,InjectMetadata.paramDecoratorForNonConstructor); + +/** + * Factory for creating {@link OptionalMetadata}. + */ +export const Optional: OptionalFactory = makeParamDecorator(OptionalMetadata); + +/** + * Factory for creating {@link InjectableMetadata}. + */ +export const Injectable: InjectableFactory = makeDecorator(InjectableMetadata); + +/** + * Factory for creating {@link SelfMetadata}. + */ +export const Self: SelfFactory = makeParamDecorator(SelfMetadata); + +/** + * Factory for creating {@link HostMetadata}. + */ +export const Host: HostFactory = makeParamDecorator(HostMetadata); + +/** + * Factory for creating {@link SkipSelfMetadata}. + */ +export const SkipSelf: SkipSelfFactory = makeParamDecorator(SkipSelfMetadata); diff --git a/src/di/metadata.ts b/src/di/metadata.ts new file mode 100644 index 0000000..a84f9c8 --- /dev/null +++ b/src/di/metadata.ts @@ -0,0 +1,246 @@ +import {CONST, stringify, isBlank, isPresent, isString} from "../facade/lang"; +import {InjectFactory} from "./decorators"; + +/** + * A parameter metadata that specifies a dependency. + * + * ### Example ([live demo](http://plnkr.co/edit/6uHYJK?p=preview)) + * + * ```typescript + * class Engine {} + * + * @Injectable() + * class Car { + * engine; + * constructor(@Inject("MyEngine") engine:Engine) { + * this.engine = engine; + * } + * } + * + * var injector = Injector.resolveAndCreate([ + * provide("MyEngine", {useClass: Engine}), + * Car + * ]); + * + * expect(injector.get(Car).engine instanceof Engine).toBe(true); + * ``` + * + * When `@Inject()` is not present, {@link Injector} will use the type annotation of the parameter. + * + * ### Example + * + * ```typescript + * class Engine {} + * + * @Injectable() + * class Car { + * constructor(public engine: Engine) {} //same as constructor(@Inject(Engine) engine:Engine) + * } + * + * var injector = Injector.resolveAndCreate([Engine, Car]); + * expect(injector.get(Car).engine instanceof Engine).toBe(true); + * ``` + */ +@CONST() +export class InjectMetadata { + + static paramDecoratorForNonConstructor( + annotationInstance: InjectMetadata, + target: any, + propertyKey: string, + paramIndex: number + ) { + + const annotateMethod = target[ propertyKey ]; + + annotateMethod.$inject = annotateMethod.$inject || []; + annotateMethod.$inject[ paramIndex ] = annotationInstance.token; + + } + + constructor( public token: any ) {} + + toString(): string { return `@Inject(${stringify( this.token )})`; } +} + +/** + * A parameter metadata that marks a dependency as optional. {@link Injector} provides `null` if + * the dependency is not found. + * + * ### Example ([live demo](http://plnkr.co/edit/AsryOm?p=preview)) + * + * ```typescript + * class Engine {} + * + * @Injectable() + * class Car { + * engine; + * constructor(@Optional() engine:Engine) { + * this.engine = engine; + * } + * } + * + * var injector = Injector.resolveAndCreate([Car]); + * expect(injector.get(Car).engine).toBeNull(); + * ``` + */ +@CONST() +export class OptionalMetadata { + toString(): string { return `@Optional()`; } +} + +/** + * A marker metadata that marks a class as available to {@link Injector} for creation. + * + * ### Example ([live demo](http://plnkr.co/edit/Wk4DMQ?p=preview)) + * + * ```typescript + * @Injectable() + * class UsefulService {} + * + * @Injectable() + * class NeedsService { + * constructor(public service:UsefulService) {} + * } + * + * var injector = Injector.resolveAndCreate([NeedsService, UsefulService]); + * expect(injector.get(NeedsService).service instanceof UsefulService).toBe(true); + * ``` + * {@link Injector} will throw {@link NoAnnotationError} when trying to instantiate a class that + * does not have `@Injectable` marker, as shown in the example below. + * + * ```typescript + * class UsefulService {} + * + * class NeedsService { + * constructor(public service:UsefulService) {} + * } + * + * var injector = Injector.resolveAndCreate([NeedsService, UsefulService]); + * expect(() => injector.get(NeedsService)).toThrowError(); + * ``` + */ +@CONST() +export class InjectableMetadata { +} + +/** + * Specifies that an {@link Injector} should retrieve a dependency only from itself. + * + * ### Example ([live demo](http://plnkr.co/edit/NeagAg?p=preview)) + * + * ```typescript + * class Dependency { + * } + * + * @Injectable() + * class NeedsDependency { + * dependency; + * constructor(@Self() dependency:Dependency) { + * this.dependency = dependency; + * } + * } + * + * var inj = Injector.resolveAndCreate([Dependency, NeedsDependency]); + * var nd = inj.get(NeedsDependency); + * + * expect(nd.dependency instanceof Dependency).toBe(true); + * + * var inj = Injector.resolveAndCreate([Dependency]); + * var child = inj.resolveAndCreateChild([NeedsDependency]); + * expect(() => child.get(NeedsDependency)).toThrowError(); + * ``` + */ +@CONST() +export class SelfMetadata { + toString(): string { return `@Self()`; } +} + +/** + * Specifies that the dependency resolution should start from the parent injector. + * + * ### Example ([live demo](http://plnkr.co/edit/Wchdzb?p=preview)) + * + * ```typescript + * class Dependency { + * } + * + * @Injectable() + * class NeedsDependency { + * dependency; + * constructor(@SkipSelf() dependency:Dependency) { + * this.dependency = dependency; + * } + * } + * + * var parent = Injector.resolveAndCreate([Dependency]); + * var child = parent.resolveAndCreateChild([NeedsDependency]); + * expect(child.get(NeedsDependency).dependency instanceof Depedency).toBe(true); + * + * var inj = Injector.resolveAndCreate([Dependency, NeedsDependency]); + * expect(() => inj.get(NeedsDependency)).toThrowError(); + * ``` + */ +@CONST() +export class SkipSelfMetadata { + toString(): string { return `@SkipSelf()`; } +} + +/** + * Specifies that an injector should retrieve a dependency from any injector until reaching the + * closest host. + * + * In Angular, a component element is automatically declared as a host for all the injectors in + * its view. + * + * ### Example ([live demo](http://plnkr.co/edit/GX79pV?p=preview)) + * + * In the following example `App` contains `ParentCmp`, which contains `ChildDirective`. + * So `ParentCmp` is the host of `ChildDirective`. + * + * `ChildDirective` depends on two services: `HostService` and `OtherService`. + * `HostService` is defined at `ParentCmp`, and `OtherService` is defined at `App`. + * + *```typescript + * class OtherService {} + * class HostService {} + * + * @Directive({ + * selector: 'child-directive' + * }) + * class ChildDirective { + * constructor(@Optional() @Host() os:OtherService, @Optional() @Host() hs:HostService){ + * console.log("os is null", os); + * console.log("hs is NOT null", hs); + * } + * } + * + * @Component({ + * selector: 'parent-cmp', + * providers: [HostService], + * template: ` + * Dir: + * `, + * directives: [ChildDirective] + * }) + * class ParentCmp { + * } + * + * @Component({ + * selector: 'app', + * providers: [OtherService], + * template: ` + * Parent: + * `, + * directives: [ParentCmp] + * }) + * class App { + * } + * + * bootstrap(App); + *``` + */ +@CONST() +export class HostMetadata { + toString(): string { return `@Host()`; } +} diff --git a/src/di/opaque_token.ts b/src/di/opaque_token.ts new file mode 100644 index 0000000..8ceeca9 --- /dev/null +++ b/src/di/opaque_token.ts @@ -0,0 +1,29 @@ +import {CONST} from '../facade/lang'; + +/** + * Creates a token that can be used in a DI Provider. + * + * ### Example ([live demo](http://plnkr.co/edit/Ys9ezXpj2Mnoy3Uc8KBp?p=preview)) + * + * ```typescript + * var t = new OpaqueToken("value"); + * + * var injector = Injector.resolveAndCreate([ + * provide(t, {useValue: "providedValue"}) + * ]); + * + * expect(injector.get(t)).toEqual("providedValue"); + * ``` + * + * Using an `OpaqueToken` is preferable to using strings as tokens because of possible collisions + * caused by multiple providers using the same string as two different tokens. + * + * Using an `OpaqueToken` is preferable to using an `Object` as tokens because it provides better + * error messages. + */ +@CONST() +export class OpaqueToken { + constructor(private _desc: string) {} + + toString(): string { return `Token ${this._desc}`; } +} diff --git a/src/di/provider.ts b/src/di/provider.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/di/decorators.spec.ts b/test/di/decorators.spec.ts new file mode 100644 index 0000000..2422001 --- /dev/null +++ b/test/di/decorators.spec.ts @@ -0,0 +1,162 @@ +import {expect} from 'chai'; +import { + Inject, + Host, + Injectable, + Optional, + Self, + SkipSelf +} from '../../src/di/decorators'; +import { + InjectMetadata, + HostMetadata, + InjectableMetadata, + OptionalMetadata, + SelfMetadata, + SkipSelfMetadata +} from '../../src/di/metadata'; +import { + CLASS_META_KEY, + PARAM_META_KEY +} from '../../src/util/decorators'; +import {isArray,isBlank,isPresent} from '../../src/facade/lang' + +describe( 'di/decorators', () => { + + describe( 'Class decorators', () => { + + it( 'should create annotation metadata on class', () => { + + @Injectable() + class Test { + } + + expect( Array.isArray( Test[ CLASS_META_KEY ] ) ).to.equal( true ); + + } ); + + } ); + + describe( 'Parameter decorators', () => { + + let cls: any; + + beforeEach( () => { + + class Test { + constructor( + @Inject( '$log' ) private $log, + @Host() @Inject( 'ngModel' ) private ngModel + ) {} + } + + cls = Test; + + } ); + it( 'should create param metadata on class', () => { + + expect( Array.isArray( cls[ PARAM_META_KEY ] ) ).to.equal( true ); + + } ); + it( 'should create 2 dimensional param metadata', () => { + + const [paramOne,paramTwo] = cls[ PARAM_META_KEY ]; + + expect( cls[ PARAM_META_KEY ].length ).to.equal( 2 ); + expect( paramOne.length ).to.equal( 1 ); + expect( paramTwo.length ).to.equal( 2 ); + + } ); + it( 'should put to proper indexes proper paramDecorator instance', () => { + + const [paramOne,paramTwo] = cls[ PARAM_META_KEY ]; + + expect( paramOne[ 0 ] instanceof InjectMetadata ).to.equal( true ); + expect( paramTwo[ 0 ] instanceof InjectMetadata ).to.equal( true ); + expect( paramTwo[ 1 ] instanceof HostMetadata ).to.equal( true ); + + } ); + + } ); + + describe( 'param decorators used on non constructor(@Inject)', () => { + + let cls: any; + + beforeEach( () => { + + class TestProvider { + + static $compile( + @Inject( '$template' ) $template + ){} + + $get( + @Inject( '$log' ) $log, + @Inject( 'foo' ) foo + ) {} + + } + + cls = TestProvider; + + } ); + + it( 'should not add instance to PARAM_META_KEY if used on non constructor', () => { + + expect( isBlank( cls[ PARAM_META_KEY ] ) ).to.equal( true ); + + } ); + + it( 'should immediately add $inject with proper annotations on method', () => { + + expect( isArray( cls.prototype.$get.$inject ) ).to.equal( true ); + expect( cls.prototype.$get.$inject ).to.deep.equal( [ '$log', 'foo' ] ); + + } ); + + it( 'should immediately add $inject with proper annotations on static method', () => { + + expect( isArray( cls.$compile.$inject ) ).to.equal( true ); + expect( cls.$compile.$inject ).to.deep.equal( [ '$template' ] ); + + } ); + + it( `should work for both constructor and method within a class`, ()=> { + + class TestBothInjectProvider { + + static $compile( + @Inject( '$template' ) $template + ){} + + constructor( + @Inject('$http') private $http + ){} + + $get( + @Inject( '$log' ) $log, + @Inject( 'foo' ) foo + ) {} + + } + + const cls = TestBothInjectProvider; + + expect( isBlank( cls[ PARAM_META_KEY ] ) ).to.equal( false ); + expect( cls[ PARAM_META_KEY ][0][0] instanceof InjectMetadata ).to.equal( true ); + expect( isBlank( cls.$inject ) ).to.equal( true ); + + expect( isArray( cls.prototype.$get.$inject ) ).to.equal( true ); + expect( cls.prototype.$get.$inject ).to.deep.equal( [ '$log', 'foo' ] ); + + expect( isArray( cls.$compile.$inject ) ).to.equal( true ); + expect( cls.$compile.$inject ).to.deep.equal( [ '$template' ] ); + + + } ); + + } ); + + +} );