diff --git a/ng-metadata.test.ts b/ng-metadata.test.ts index a0c2716..92ff916 100644 --- a/ng-metadata.test.ts +++ b/ng-metadata.test.ts @@ -10,6 +10,7 @@ import './test/life_cycle.spec'; import './test/providers.spec'; import './test/di/decorators.spec'; +import './test/di/povider.spec'; import './test/util/decorators.spec'; import './test/reflection/reflection.spec'; diff --git a/src/di/provider.ts b/src/di/provider.ts index e69de29..079da91 100644 --- a/src/di/provider.ts +++ b/src/di/provider.ts @@ -0,0 +1,80 @@ +import {Type,stringify} from '../facade/lang'; +import {isFunction} from "../facade/lang"; +import {reflector} from "../reflection/reflection"; +import {isString} from "../facade/lang"; +import {isArray} from "../facade/lang"; +import {isType} from "../facade/lang"; +import {getTypeName} from "../facade/lang"; +import {PipeMetadata} from "../directives/metadata_directives"; +import {DirectiveMetadata} from "../directives/metadata_directives"; +import {InjectableMetadata} from "./metadata"; +import {resolveDirectiveNameFromSelector} from "../facade/lang"; +import {InjectMetadata} from "./metadata"; + +/** + * should extract the string token from provided Type and add $inject angular 1 annotation to constructor if @Inject + * was used + * @param type + * @returns {string} + */ +export function provide( type: Type | string, {useClass}:{useClass?:Type} = {} ): string { + + // create $inject annotation if needed + const parameters = isString( type ) + ? reflector.parameters( useClass ) + : reflector.parameters( type ); + const injectTo = isString( type ) + ? useClass + : type; + + const $inject = _getInjectStringTokens( parameters ); + + if ( isArray( $inject ) ) { + + (injectTo).$inject = $inject; + + } + + return provideResolver( type ); + +} + +export function _getInjectStringTokens( parameters: any[][] = [] ): string[] { + + return parameters + .filter( ( paramMeta )=>paramMeta.length === 1 && paramMeta[ 0 ] instanceof InjectMetadata ) + .map( ( [injectMeta] )=>provideResolver( injectMeta.token ) ); + +} + +export function provideResolver( type: Type | string ): string { + + if ( isString( type ) ) { + return type; + } + if ( isType( type ) ) { + + // only the first class annotations is injectable + const [annotation=null] = reflector.annotations( type as Type ) || []; + + if ( !annotation ) { + + return getTypeName( type ); + + } + + if ( annotation instanceof PipeMetadata ) { + return annotation.name; + } + + if ( annotation instanceof DirectiveMetadata ) { + return resolveDirectiveNameFromSelector( annotation.selector ); + } + + if ( annotation instanceof InjectableMetadata ) { + return getTypeName( type ); + } + + } + +} diff --git a/src/facade/lang.ts b/src/facade/lang.ts index 3bb0127..13e8ac3 100644 --- a/src/facade/lang.ts +++ b/src/facade/lang.ts @@ -41,7 +41,7 @@ export function isBlank( obj: any ): boolean { return obj === undefined || obj === null; } -export function isString( obj: any ): boolean { +export function isString( obj: any ): obj is String { return typeof obj === "string"; } @@ -106,3 +106,63 @@ export function assign( destination: any, ...sources: any[] ): any { return envAssign( destination, ...sources ); } + +const ATTRS_BOUNDARIES = /\[|\]/g; +const COMPONENT_SELECTOR = /^\[?[\w|-]*\]?$/; +const SKEWER_CASE = /-(\w)/g; + +export function resolveDirectiveNameFromSelector( selector: string ): string { + + if ( !selector.match( COMPONENT_SELECTOR ) ) { + throw new Error( + `Only selectors matching element names or base attributes are supported, got: ${selector}` + ); + } + + return selector + .trim() + .replace( + ATTRS_BOUNDARIES, + '' + ) + .replace( + SKEWER_CASE, + ( all, letter ) => letter.toUpperCase() + ) +} + +export function getTypeName(type): string{ + + const typeName = _getFuncName(type); + return firstToLowerCase( typeName ); + +} + +/** + * + * @param {Function} func + * @returns {string} + * @private + */ +function _getFuncName( func: Function ): string { + + const parsedFnStatement = /function\s*([^\s(]+)/.exec(stringify(func)); + const [,name=''] = parsedFnStatement || []; + + return name || stringify(func); + +} + +export function controllerKey( name: string ): string { + return '$' + name + 'Controller'; +} +export function hasCtorInjectables( Type ): boolean { + return (Array.isArray( Type.$inject ) && Type.$inject.length !== 0); +} +export function firstToLowerCase( value: string ): string { + return value.charAt( 0 ).toLocaleLowerCase() + value.substring( 1 ); +} +export function firstToUpperCase( value: string ): string { + return value.charAt( 0 ).toUpperCase() + value.substring( 1 ); +} + diff --git a/src/reflection/reflection.ts b/src/reflection/reflection.ts index de8241e..928098f 100644 --- a/src/reflection/reflection.ts +++ b/src/reflection/reflection.ts @@ -4,4 +4,4 @@ import {Reflector} from './reflector'; * The {@link Reflector} used internally in Angular to access metadata * about symbols. */ -export var reflector = new Reflector(); +export const reflector = new Reflector(); diff --git a/test/di/povider.spec.ts b/test/di/povider.spec.ts new file mode 100644 index 0000000..cb55b5d --- /dev/null +++ b/test/di/povider.spec.ts @@ -0,0 +1,229 @@ +import {expect} from 'chai'; +import {provide,provideResolver, _getInjectStringTokens} from "../../src/di/provider"; +import {Inject,Injectable} from "../../src/di/decorators"; +import {InjectMetadata,OptionalMetadata,HostMetadata} from "../../src/di/metadata"; +import {Component,Directive,Pipe} from "../../src/directives/decorators"; + +describe( `di/provider`, ()=> { + + describe( `public #provide`, ()=> { + + it( `should return string name for Angular registry and add $inject prop if needed (string)`, ()=> { + + class Foo{ + constructor(@Inject('$http') private $http){ + + } + } + const actual = provide('fooToken',{useClass:Foo}); + const expected = 'fooToken'; + + expect(actual).to.equal(expected); + expect(Foo.$inject).to.deep.equal(['$http']); + + } ); + it( `should return string name for Angular registry and add $inject prop if needed (Class)`, ()=> { + + class MyService{} + + class Foo{ + constructor(@Inject(MyService) private mySvc){} + } + const actual = provide(Foo); + const expected = 'foo'; + + expect(actual).to.equal(expected); + expect(Foo.$inject).to.deep.equal(['myService']); + + } ); + it( `should return string name for Angular registry and add $inject prop if needed (Pipe)`, ()=> { + + class MyService{} + + @Pipe({ + name: 'fooYo' + }) + class FooPipe{ + constructor(@Inject(MyService) private mySvc){} + } + const actual = provide(FooPipe); + const expected = 'fooYo'; + + expect(actual).to.equal(expected); + expect(FooPipe.$inject).to.deep.equal(['myService']); + + } ); + it( `should return string name for Angular registry and add $inject prop if needed (Directive)`, ()=> { + + class MyService{} + + @Directive({ + selector: '[my-foo]' + }) + class FooDirective{ + constructor(@Inject(MyService) private mySvc){} + } + const actual = provide(FooDirective); + const expected = 'myFoo'; + + expect(actual).to.equal(expected); + expect(FooDirective.$inject).to.deep.equal(['myService']); + + } ); + it( `should return string name for Angular registry and add $inject prop if needed (Component)`, ()=> { + + class MyService{} + + @Component({ + selector: 'my-foo', + template:`hello` + }) + class FooComponent{ + constructor(@Inject(MyService) private mySvc,@Inject('$element') private $element){} + } + const actual = provide(FooComponent); + const expected = 'myFoo'; + + expect(actual).to.equal(expected); + expect(FooComponent.$inject).to.deep.equal(['myService','$element']); + + } ); + + // @TODO + it.skip( `should work as factory with angular.module.*.apply and output array [injectName,typeFunction]`, ()=> { + + /*class MyService{} + + class Foo{ + constructor(@Inject(MyService) private mySvc){} + } + let actual = provide(Foo); + let expected = [ 'foo', Foo ]; + + expect(actual).to.deep.equal(expected); + expect(Foo.$inject).to.deep.equal(['myService']); + + + @Directive( { + selector: '[my-attr]' + } ) + class FooDirective{} + + let directiveProvider = provide(FooDirective) as [string,Function]; + actual = [ directiveProvider[ 0 ], directiveProvider[ 1 ]() ]; + expected = [ 'myAttr', { + controller: FooDirective, + link: function _postLink(){} + } ]; + + expect(actual).to.deep.equal(expected); + expect(FooDirective.$inject).to.deep.equal(['myService']);*/ + + } ); + + } ); + + describe( `#_getInjectStringTokens`, ()=> { + + it( `should create proper $inject string array`, ()=> { + + class MyService{} + + @Injectable() + class AnotherService { + } + + const parameters = [ + [new InjectMetadata('foo')], + [new InjectMetadata(MyService)], + [new InjectMetadata('boo')], + [new InjectMetadata(AnotherService)], + [new InjectMetadata('nope'),new OptionalMetadata(), new HostMetadata()] + ]; + + const actual = _getInjectStringTokens(parameters); + const expected = ['foo','myService','boo','anotherService']; + + expect( actual ).to.deep.equal( expected ); + + } ); + + } ); + + describe( `#provideResolver`, ()=> { + + it( `should get DI container string name if string`, ()=> { + + const actual = provideResolver( '$http' ); + const expected = '$http'; + + expect( actual ).to.equal( expected ); + + } ); + + it( `should get DI container string name if service Class`, ()=> { + + class MyService{} + + const actual = provideResolver( MyService ); + const expected = 'myService'; + + expect( actual ).to.equal( expected ); + + } ); + it( `should get DI container string name if Injectable Class`, ()=> { + + @Injectable() + class MyService{} + + const actual = provideResolver( MyService ); + const expected = 'myService'; + + expect( actual ).to.equal( expected ); + + } ); + it( `should get DI container string name if Directive Class`, ()=> { + + @Directive({ + selector:'[my-attr]' + }) + class MyDirective{} + + const actual = provideResolver( MyDirective ); + const expected = 'myAttr'; + + expect( actual ).to.equal( expected ); + + + } ); + it( `should get DI container string name if Component Class`, ()=> { + + @Component({ + selector:'my-cmp' + }) + class MyComponent{} + + const actual = provideResolver( MyComponent ); + const expected = 'myCmp'; + + expect( actual ).to.equal( expected ); + + } ); + it( `should get DI container string name if Pipe Class`, ()=> { + + @Pipe({ + name:'myPipeYo' + }) + class MyPipe{} + + const actual = provideResolver( MyPipe ); + const expected = 'myPipeYo'; + + expect( actual ).to.equal( expected ); + + } ); + + + } ); + +} );