Skip to content

Commit

Permalink
feat(core/di): make resolving component tree code DRY and add support…
Browse files Browse the repository at this point in the history
… for registering config phase functions via provide

- clean up provider and move related logic to provider_utils and reflective_provider
  • Loading branch information
Hotell committed Jun 12, 2016
1 parent ac5e5e4 commit 272e22a
Show file tree
Hide file tree
Showing 7 changed files with 521 additions and 69 deletions.
81 changes: 17 additions & 64 deletions src/core/di/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) : [];
Expand Down Expand Up @@ -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
` );

}

/**
Expand All @@ -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 ) {
Expand Down Expand Up @@ -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 ) ) {
Expand Down Expand Up @@ -457,6 +452,9 @@ export function getInjectableName(injectable: ProviderType): string{
* @returns {boolean}
* @private
* @internal
* @deprecated
*
* @TODO: delete this
*/
export function _areAllDirectiveInjectionsAtTail( metadata: ParamMetaInst[][] ): boolean {

Expand Down Expand Up @@ -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';
}

}
26 changes: 26 additions & 0 deletions src/core/di/provider_util.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
}
158 changes: 158 additions & 0 deletions src/core/di/reflective_provider.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string|Type|ProviderLiteral|any[]>
): 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;
} )
}
6 changes: 2 additions & 4 deletions test/core/di/provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Loading

0 comments on commit 272e22a

Please sign in to comment.