diff --git a/src/core/di/provider.ts b/src/core/di/provider.ts index 739d57f..fcb2066 100644 --- a/src/core/di/provider.ts +++ b/src/core/di/provider.ts @@ -6,26 +6,27 @@ import { resolveDirectiveNameFromSelector, isPresent, stringify, - getFuncName, normalizeBool + getFuncName, + normalizeBool, + isArray } from '../../facade/lang'; import { reflector } from '../reflection/reflection'; import { OpaqueToken } from './opaque_token'; -import { PipeMetadata } from '../pipes/metadata'; import { - DirectiveMetadata, OutputMetadata, HostBindingMetadata, HostListenerMetadata, - InputMetadata, - ComponentMetadata + InputMetadata } from '../directives/metadata_directives'; -import { InjectMetadata, InjectableMetadata, SkipSelfMetadata, SelfMetadata, HostMetadata } from './metadata'; +import { InjectMetadata, SkipSelfMetadata, SelfMetadata, HostMetadata } from './metadata'; import { pipeProvider } from '../pipes/pipe_provider'; import { directiveProvider } from '../directives/directive_provider'; import { ListWrapper } from '../../facade/collections'; import { resolveForwardRef } from './forward_ref'; import { getErrorMsg } from '../../facade/exceptions'; -import { isArray } from '../../facade/lang'; +import { isPipe, isOpaqueToken, isDirective, isService } from './provider_util'; +import { isComponent } from './provider_util'; +import { isInjectMetadata } from './provider_util'; export type PropMetaInst = InputMetadata | OutputMetadata | HostBindingMetadata | HostListenerMetadata; @@ -220,7 +221,7 @@ class ProviderBuilder{ { useClass, useValue, useFactory, deps }: ProviderAliasOptions ): [string,Type] { - // ...provide('myFactory',{useFactory: () => { return new Foo(); } }) + // ...provide('myFactory',{useFactory: () => () => { return new Foo(); } }) if ( isPresent( useFactory ) ) { const factoryToken = getInjectableName(type); const injectableDeps = isArray( deps ) ? deps.map(getInjectableName) : []; @@ -250,13 +251,11 @@ class ProviderBuilder{ : ''; if ( !isType( injectableType ) ) { - throw new Error( ` Provider registration: "${stringify( injectableType )}": ======================================================= token ${ stringify( injectableType ) } must be type of Type, You cannot provide none class ` ); - } /** @@ -267,15 +266,11 @@ class ProviderBuilder{ const [rootAnnotation] = annotations; + // No Annotation === it's config function !!! + // NOTE: we are not checking anymore if user annotated the class or not, + // we cannot do that anymore at the costs for nic config functions registration if ( ListWrapper.isEmpty( annotations ) ) { - - throw new Error( ` - Provider registration: "${ stringify(injectableType) }": - ======================================================= - cannot create appropriate construct from provided Type. - -> Type "${ stringify(injectableType) }" must have one of class decorators: [ @Pipe(), @Component(), @Directive(), @Injectable() ] - ` ); - + return [ injectableType ] as any; } if ( ListWrapper.size( annotations ) > 1 ) { @@ -405,7 +400,7 @@ export function _extractToken( metadata: ParamMetaInst[] ): string { * @param {ProviderType} injectable * @returns {string} */ -export function getInjectableName(injectable: ProviderType): string{ +export function getInjectableName( injectable: ProviderType ): string { // @Inject('foo') foo if ( isString( injectable ) ) { @@ -457,6 +452,9 @@ export function getInjectableName(injectable: ProviderType): string{ * @returns {boolean} * @private * @internal + * @deprecated + * + * @TODO: delete this */ export function _areAllDirectiveInjectionsAtTail( metadata: ParamMetaInst[][] ): boolean { @@ -487,48 +485,3 @@ export function _areAllDirectiveInjectionsAtTail( metadata: ParamMetaInst[][] ): } ); } - -export function isOpaqueToken( obj: any ): obj is OpaqueToken { - return obj instanceof OpaqueToken; -} -export function isDirective( annotation: any ): annotation is DirectiveMetadata { - return isString( annotation.selector ) && annotation instanceof DirectiveMetadata; -} -export function isComponent( annotation: any ): annotation is ComponentMetadata { - const hasTemplate = !isBlank( annotation.template || annotation.templateUrl ); - return isString( annotation.selector ) && hasTemplate && annotation instanceof ComponentMetadata -} -export function isService(annotation: any): annotation is InjectableMetadata{ - return annotation instanceof InjectableMetadata; -} -export function isPipe(annotation: any): annotation is PipeMetadata { - return isString(annotation.name) && annotation instanceof PipeMetadata; -} -function isInjectMetadata( injectMeta: any ): injectMeta is InjectMetadata { - return injectMeta instanceof InjectMetadata; -} - -export function getNgModuleMethodByType( injectable: Type ): string { - // only the first class annotations is injectable - const [annotation] = reflector.annotations( injectable ); - - if ( isBlank( annotation ) ) { - throw new Error( ` - cannot get injectable name token from none decorated class ${ getFuncName( injectable ) } - Only decorated classes by one of [ @Injectable,@Directive,@Component,@Pipe ], can be injected by reference - ` ); - } - - if ( isPipe( annotation ) ) { - return 'filter'; - } - - if ( isDirective( annotation ) ) { - return 'directive'; - } - - if ( isService( annotation ) ) { - return 'service'; - } - -} diff --git a/src/core/di/provider_util.ts b/src/core/di/provider_util.ts index 7fe69c5..a6e643d 100644 --- a/src/core/di/provider_util.ts +++ b/src/core/di/provider_util.ts @@ -1,4 +1,10 @@ +import { isString, isBlank } from '../../facade/lang'; +import { DirectiveMetadata, ComponentMetadata } from '../directives/metadata_directives'; +import { PipeMetadata } from '../pipes/metadata'; + import { Provider } from './provider'; +import { OpaqueToken } from './opaque_token'; +import { InjectableMetadata, InjectMetadata } from './metadata'; export type ProviderLiteral = { provide: any, @@ -17,3 +23,23 @@ export function isProviderLiteral( obj: any ): obj is ProviderLiteral { export function createProvider( obj: ProviderLiteral ): Provider { return new Provider( obj.provide, obj ); } + +export function isOpaqueToken( obj: any ): obj is OpaqueToken { + return obj instanceof OpaqueToken; +} +export function isDirective( annotation: any ): annotation is DirectiveMetadata { + return isString( annotation.selector ) && annotation instanceof DirectiveMetadata; +} +export function isComponent( annotation: any ): annotation is ComponentMetadata { + const hasTemplate = !isBlank( annotation.template || annotation.templateUrl ); + return isString( annotation.selector ) && hasTemplate && annotation instanceof ComponentMetadata +} +export function isService(annotation: any): annotation is InjectableMetadata { + return annotation instanceof InjectableMetadata; +} +export function isPipe(annotation: any): annotation is PipeMetadata { + return isString(annotation.name) && annotation instanceof PipeMetadata; +} +export function isInjectMetadata( injectMeta: any ): injectMeta is InjectMetadata { + return injectMeta instanceof InjectMetadata; +} diff --git a/src/core/di/reflective_provider.ts b/src/core/di/reflective_provider.ts index 22caf94..c8e34d5 100644 --- a/src/core/di/reflective_provider.ts +++ b/src/core/di/reflective_provider.ts @@ -1,5 +1,15 @@ +import { isType, isArray, isString, getFuncName, isBlank } from '../../facade/lang'; + +import { reflector } from '../reflection/reflection'; + import { Provider, provide } from './provider'; +import { isPipe, isDirective, isService, isProviderLiteral, createProvider, ProviderLiteral } from './provider_util'; +/** + * process provider literals and return map for ngModule consumption + * @param provider + * @returns {{method: string, name: string, value: any}} + */ export function resolveReflectiveProvider( provider: Provider ): {method: string, name: string, value: any} { const {token} = provider; @@ -22,9 +32,157 @@ export function resolveReflectiveProvider( provider: Provider ): {method: string if (provider.useExisting) { const [name,value] = provide( provider.useExisting ); const method = 'factory'; + + throw new Error('useExisting is unimplemented'); // target = (v) => v; // annotate(target, 'factory', {name}); // annotate(target, 'inject', [toInjectorName(provider.useExisting)]); } } + +/** + * returns StringMap of values needed for ngModule registration and duplicity checks + * @param injectable + * @returns {any} + * @private + */ +export function _getNgModuleMetadataByType( injectable: Type ): { providerName: string, providerMethod: string, moduleMethod: string} { + // only the first class annotations is injectable + const [annotation] = reflector.annotations( injectable ); + + if ( isBlank( annotation ) ) { + + // support configPhase ( function or pure class ) + if ( isType( injectable ) ) { + return { + providerName: '$injector', + providerMethod: 'invoke', + moduleMethod: 'config' + } + } + + throw new Error( ` + cannot get injectable name token from none decorated class ${ getFuncName( injectable ) } + Only decorated classes by one of [ @Injectable,@Directive,@Component,@Pipe ], can be injected by reference + ` ); + } + + if ( isPipe( annotation ) ) { + return { + providerName: '$filterProvider', + providerMethod: 'register', + moduleMethod: 'filter' + } + } + + if ( isDirective( annotation ) ) { + return { + providerName: '$compileProvider', + providerMethod: 'directive', + moduleMethod: 'directive' + } + } + + if ( isService( annotation ) ) { + return { + providerName: '$provide', + providerMethod: 'service', + moduleMethod: 'service' + } + } + +} + + +/** + * run through Component tree and register everything that is registered via Metadata + * - works for nested arrays like angular 2 does ;) + * @param ngModule + * @param providers + * @returns {ng.IModule} + * @private + */ +export function _normalizeProviders( + ngModule: ng.IModule, + providers: Array +): ng.IModule { + + providers.forEach( ( providerType ) => { + + // this is for legacy Angular 1 module + if ( isString( providerType ) ) { + ngModule.requires.push( providerType ); + return; + } + + // this works only value,factory,aliased services + // you cannot register directive/pipe within provider literal + if ( isProviderLiteral( providerType ) ) { + const provider = createProvider( providerType ); + const { method, name, value } = resolveReflectiveProvider( provider ); + if ( !_isTypeRegistered( name, ngModule, '$provide', method ) ) { + ngModule[ method ]( name, value ); + } + return; + } + + // this is for pipes/directives/services + if (isType(providerType)) { + // const provider = createProvider( {provide:b, useClass:b} ); + // const { method, name, value } = resolveReflectiveProvider( provider ); + const [name,value] = provide( providerType ); + const { providerName, providerMethod, moduleMethod } = _getNgModuleMetadataByType( providerType ); + + // config phase support + if ( isType( name ) ) { + ngModule.config( name ); + return; + } + + if ( !_isTypeRegistered( name, ngModule, providerName, providerMethod ) ) { + ngModule[ moduleMethod ]( name, value ); + } + return; + } + + // un flattened array, unwrap and parse next array level of providers + if (isArray(providerType)) { + _normalizeProviders( ngModule, providerType ); + } else { + throw new Error(`InvalidProviderError(${providerType})`); + } + }); + + // return res; + return ngModule; +} + +/** + * check if `findRegisteredType` is registered within ngModule, so we don't have duplicates + * @param findRegisteredType + * @param ngModule + * @param instanceType + * @param methodName + * @returns {boolean} + * @private + */ +export function _isTypeRegistered( + findRegisteredType: string, + ngModule: ng.IModule, + instanceType: string, + methodName: string +): boolean { + const invokeQueue: any[] = (ngModule as any)._invokeQueue; + const types = invokeQueue + .filter( ( [type,fnName]:[string,string] ) => { + return type === instanceType && fnName === methodName; + } ) + .map( ( [type,fnName, registeredProvider]:[string,string,[string,any]] ) => { + return registeredProvider + } ); + + return types.some( ( [typeName,typeFn] )=> { + return findRegisteredType === typeName; + } ) +} diff --git a/test/core/di/provider.spec.ts b/test/core/di/provider.spec.ts index 949e554..ba9500a 100644 --- a/test/core/di/provider.spec.ts +++ b/test/core/di/provider.spec.ts @@ -191,15 +191,13 @@ describe( `di/provider`, ()=> { expect( ()=>(provide as any)( { hello: 'wat' } ) ).to.throw(); } ); - it( `should throw if registering class which is missing annotation`, ()=> { - + it( `should not throw if registering class or function, cause they are supposed to be config functions`, ()=> { class MyService { constructor( @Inject( '$log' ) private $log ) {} } - expect( ()=>provide( MyService ) ).to.throw(); - + expect( ()=>provide( MyService ) ).to.not.throw(); } ); it( `should throw if class has more than 1 class decorator and those are two aren't @Component and @StateConfig`, diff --git a/test/core/di/reflective_provider.spec.ts b/test/core/di/reflective_provider.spec.ts new file mode 100644 index 0000000..9bc9484 --- /dev/null +++ b/test/core/di/reflective_provider.spec.ts @@ -0,0 +1,316 @@ +import { expect } from 'chai'; + +import { global } from '../../../src/facade/lang'; +import { Component, Directive } from '../../../src/core/directives/decorators'; +import { Injectable, Inject } from '../../../src/core/di/decorators'; +import { Pipe } from '../../../src/core/pipes/decorators'; +import { _getNgModuleMetadataByType, resolveReflectiveProvider } from '../../../src/core/di/reflective_provider'; +import { createNgModule } from '../../utils'; +import { getInjectableName } from '../../../src/core/di/provider'; +import { createProvider } from '../../../src/core/di/provider_util'; +import { OpaqueToken } from '../../../src/core/di/opaque_token'; +import { _isTypeRegistered } from '../../../src/core/di/reflective_provider'; +import { provide } from '../../../src/core/di/provider'; +import { _normalizeProviders } from '../../../src/core/di/reflective_provider'; + +describe( `di/reflective_provider`, () => { + + beforeEach( () => { + global.angular = createNgModule() as any; + } ); + + describe( `#resolveReflectiveProvider`, () => { + + it( `should resolve value by useValue and provide metadata for ngModule`, () => { + + const ValueToken = new OpaqueToken('helloToken'); + const providers = [ + createProvider( { provide: ValueToken, useValue: 'hello' } ), + createProvider( { provide: 'viaString', useValue: 'stringzzzz' } ), + ]; + + const actual = providers.map(resolveReflectiveProvider); + const expected = [ + { method: 'value', name: 'helloToken', value: 'hello' }, + { method: 'value', name: 'viaString', value: 'stringzzzz' } + ]; + + expect( actual ).to.deep.equal( expected ); + + } ); + it( `should resolve service by useClass and provide metadata for ngModule`, () => { + + const classToken = new OpaqueToken('classToken'); + + @Injectable() + class SomeClass{} + + @Injectable() + class SomeInjectable{} + + const providers = [ + createProvider( { provide: classToken, useClass: SomeClass } ), + createProvider( { provide: 'byString', useClass: SomeClass } ), + createProvider( { provide: SomeInjectable, useClass: SomeInjectable } ) + ]; + + const actual = providers.map(resolveReflectiveProvider); + const expected = [ + { method: 'service', name: 'classToken', value: SomeClass }, + { method: 'service', name: 'byString', value: SomeClass }, + { method: 'service', name: getInjectableName(SomeInjectable), value: SomeInjectable }, + ]; + + expect( actual ).to.deep.equal( expected ); + + } ); + it( `should resolve factory by useFactory and provide metadata for ngModule`, () => { + + const classToken = new OpaqueToken('classToken'); + + @Injectable('$http') + class Http{} + + @Injectable() + class SomeClass{} + + @Injectable() + class SomeInjectable{} + + class SomeLogger{ + constructor(private $log, private http){} + } + + const factoryOne = ()=>new SomeClass(); + const factoryTwo = ( $log, http )=>new SomeLogger( $log, http ); + const factoryThree = ()=>()=>new SomeInjectable(); + + const providers = [ + createProvider( { provide: classToken, useFactory: factoryOne } ), + createProvider( { provide: 'byString', deps: [ '$log',Http ], useFactory: factoryTwo } ), + createProvider( { provide: SomeInjectable, useFactory: factoryThree} ) + ]; + + const actual = providers.map( resolveReflectiveProvider ); + const expected = [ + { method: 'factory', name: 'classToken', value: factoryOne }, + { method: 'factory', name: 'byString', value: factoryTwo }, + { method: 'factory', name: getInjectableName( SomeInjectable ), value: factoryThree } + ]; + + expect( actual ).to.deep.equal( expected ); + expect( actual[ 1 ].value.$inject ).to.deep.equal( [ '$log','$http' ] ); + + } ); + it( `should resolve factory by useExisting and provide metadata for ngModule`, () => { + + @Injectable() + class SomeInjectable{} + + const provider = createProvider( { provide: 'foo', useExisting: SomeInjectable } ); + + expect( ()=>resolveReflectiveProvider(provider) ).to.throw(); + + } ); + + } ); + describe( `#_normalizeProviders`, () => { + + let ngModule: ng.IModule; + beforeEach( () => { + ngModule = global.angular.module( 'myApp', [] ); + } ); + + it( `should add dependant module to existing one if provider is string`, () => { + const provider = 'ui.bootstrap.datepicker'; + const updatedNgModule = _normalizeProviders( ngModule, [provider] ); + + expect( updatedNgModule.requires ).to.deep.equal( [ 'ui.bootstrap.datepicker' ] ); + } ); + it( `should register $provide via value/service/factory if provider is ProviderLiteral`, () => { + + @Injectable() + class HelloSvc{} + + const providers = [ + { provide: 'myValToken', useValue: 'lorem' }, + { provide: 'myHelloSvc', useClass: HelloSvc }, + { provide: 'myHelloFactory', useFactory: ()=>new HelloSvc() } + ]; + const updatedNgModule = _normalizeProviders( ngModule, providers ); + + expect( _isTypeRegistered( 'myValToken', updatedNgModule, '$provide', 'value' ) ).to.equal( true ); + expect( _isTypeRegistered( 'myHelloSvc', updatedNgModule, '$provide', 'service' ) ).to.equal( true ); + expect( _isTypeRegistered( 'myHelloFactory', updatedNgModule, '$provide', 'factory' ) ).to.equal( true ); + + } ); + it( `should register $provide via service if provider is Decorated Type`, () => { + @Injectable() + class HelloSvc{} + + const provider = HelloSvc; + const updatedNgModule = _normalizeProviders( ngModule, [provider] ); + + expect( _isTypeRegistered( getInjectableName(HelloSvc), updatedNgModule, '$provide', 'service' ) ).to.equal( true ); + } ); + it( `should register $compileProvider via directive if provider is Decorated Type`, () => { + + @Directive( { selector: '[myFooAttr]' } ) + class HelloAttrDirective { } + + @Component( { selector: 'my-cmp', template: 'foo hello' } ) + class MyComponent { } + + const providers = [ HelloAttrDirective, MyComponent ]; + const updatedNgModule = _normalizeProviders( ngModule, [ providers ] ); + + expect( _isTypeRegistered( + getInjectableName( HelloAttrDirective ), + updatedNgModule, + '$compileProvider', + 'directive' + ) ).to.equal( true ); + + expect( _isTypeRegistered( + getInjectableName( MyComponent ), + updatedNgModule, + '$compileProvider', + 'directive' + ) ).to.equal( true ); + + } ); + it( `should register $filterProvider via register if provider is Decorated Type`, () => { + @Pipe( { name: 'upsHello' } ) + class UpsHelloPipe { + transform( input ) { return input; } + } + + const provider = UpsHelloPipe; + const updatedNgModule = _normalizeProviders( ngModule, [provider] ); + + expect( _isTypeRegistered( + getInjectableName( UpsHelloPipe ), + updatedNgModule, + '$filterProvider', + 'register' + ) ).to.equal( true ); + } ); + it( `should register config initializer if provider is class/function without Decorator`, () => { + stateConfig.$inject = [ 'stateConfig' ]; + function stateConfig( $stateProvider ) {} + + let internalRef; + function createProvider( initialValue ) { + internalRef = ()=>({}); + return internalRef; + } + + const updatedNgModule = _normalizeProviders( ngModule, [ stateConfig, createProvider( {} ) ] ) as any; + + expect( updatedNgModule._configBlocks ).to.deep.equal( + [ + [ '$injector', 'invoke', [ stateConfig ] ], + [ '$injector', 'invoke', [ internalRef ] ] + ] + ); + + } ); + it( `should throw if non supported provider type is used`, () => { + expect( ()=>_normalizeProviders( ngModule, [ 23213 ] as any ) ).to.throw(); + expect( ()=>_normalizeProviders( ngModule, [ {} ] as any ) ).to.throw(); + expect( ()=>_normalizeProviders( ngModule, [ true ] as any ) ).to.throw(); + } ); + + } ); + describe( `#_isTypeRegistered`, () => { + + let ngModule: ng.IModule; + + @Component({selector:'my-cmp',template:'fooo'}) + class MyCmp{} + + beforeEach( () => { + ngModule = global.angular.module( 'myApp', [] ); + } ); + + it( `should check ngModule for duplicates`, () => { + + let actual = _isTypeRegistered( 'myValue', ngModule, '$provide', 'value' ); + + expect( actual ).to.equal( false ); + + ngModule.value( ...provide( 'myValue', { useValue: 'hello' } ) ); + actual = _isTypeRegistered( 'myValue', ngModule, '$provide', 'value' ); + + expect( actual ).to.equal( true ); + + actual = _isTypeRegistered( 'myCmp', ngModule, '$compileProvider', 'directive' ); + + expect( actual ).to.equal( false ); + + ngModule.directive( ...provide( MyCmp ) ); + actual = _isTypeRegistered( 'myCmp', ngModule, '$compileProvider', 'directive' ); + + expect( actual ).to.equal( true ); + + } ); + + } ); + describe(`#_getNgModuleMetadataByType`, () => { + + @Component({selector:'foo',template:'hello'}) + class FooComponent{} + + @Directive({selector:'[fooAttr]'}) + class FooDirective{} + + @Injectable() + class MyService{} + + @Pipe({name:'ups'}) + class UpsPipe{} + + it(`should not throw if type has no metadata, cause support of config function`, () => { + class Configure { + constructor( @Inject( '$provide' ) $provide: ng.auto.IProvideService ) {} + } + + configPhase.$inject = ['$provide']; + function configPhase($provide: ng.auto.IProvideService){} + + function configFactory( initState ) { + return configPhase; + } + + expect( () => _getNgModuleMetadataByType( Configure ) ).to.not.throw(); + expect( () => _getNgModuleMetadataByType( configPhase ) ).to.not.throw(); + expect( () => _getNgModuleMetadataByType( configFactory( {} ) ) ).to.not.throw(); + + expect( _getNgModuleMetadataByType( configPhase ) ).to.deep.equal( { + providerName: '$injector', + providerMethod: 'invoke', + moduleMethod: 'config' + } ); + + }); + + it(`should return ngModule registration method by Type Metadata`, () => { + const actual = [ + _getNgModuleMetadataByType( FooComponent ), + _getNgModuleMetadataByType( FooDirective ), + _getNgModuleMetadataByType( MyService ), + _getNgModuleMetadataByType( UpsPipe ), + ]; + const expected = [ + { providerName: '$compileProvider', providerMethod: 'directive', moduleMethod: 'directive' }, + { providerName: '$compileProvider', providerMethod: 'directive', moduleMethod: 'directive' }, + { providerName: '$provide', providerMethod: 'service', moduleMethod: 'service' }, + { providerName: '$filterProvider', providerMethod: 'register', moduleMethod: 'filter' }, + ]; + + expect( actual ).to.deep.equal( expected ); + }); + + }); + +} ); diff --git a/test/index.ts b/test/index.ts index 425fee4..4ee848f 100644 --- a/test/index.ts +++ b/test/index.ts @@ -3,6 +3,7 @@ import './core/change_detection/changes_queue.spec'; import './core/change_detection/change_detection_util.spec'; import './core/di/decorators.spec'; import './core/di/provider.spec'; +import './core/di/reflective_provider.spec'; import './core/di/key.spec'; import './core/di/forward_ref.spec'; import './core/util/decorators.spec'; diff --git a/test/utils.ts b/test/utils.ts index 9f6f190..fa8f80a 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -26,7 +26,7 @@ export function createNgModule() { _runBlocks: [], constant(n,v){ this._invokeQueue.push(['$provide','constant',[n,v]]); return this; }, - value(n,v){ this._invokeQueue.push(['$provide','value',n,v]); return this; }, + value(n,v){ this._invokeQueue.push(['$provide','value',[n,v]]); return this; }, provider(n,v){ this._invokeQueue.push(['$provide','provider',[n,v]]); return this; }, factory(n,v){ this._invokeQueue.push(['$provide','factory',[n,v]]); return this; }, service(n,v){ this._invokeQueue.push(['$provide','service',[n,v]]); return this; },