Skip to content

Commit

Permalink
feat(core): add helpers for debugging invalid registration targets
Browse files Browse the repository at this point in the history
closes #19
  • Loading branch information
Daniel Schaffer committed Feb 26, 2019
1 parent bd5f506 commit 3dee0cd
Show file tree
Hide file tree
Showing 24 changed files with 240 additions and 118 deletions.
4 changes: 2 additions & 2 deletions packages/dandi-contrib/aws-lambda/src/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ export class Lambda<TEvent, TEventData, THandler extends LambdaHandler<TEventDat
})

const repo = Repository.for({})
repo.register({
repo.registerProviders({
provide: LambdaHandler,
useClass: handlerServiceType,
})
if (!existingContainer) {
modulesOrProviders.forEach((p) => repo.register(p))
modulesOrProviders.forEach((p) => repo.register(container, p))
}

const ready = existingContainer ? Promise.resolve() : container.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,22 @@ const TEST_EXPRESS_RESOLVER: any = {
},
}

Repository.global.register(TEST_EXPRESS_RESOLVER)
Repository.global.register({
const REGISTRATION_SOURCE = {
constructor: function ExpressMvcApplicationSpec() {},
}

Repository.global.register(REGISTRATION_SOURCE, TEST_EXPRESS_RESOLVER)
Repository.global.register(REGISTRATION_SOURCE, {
provide: ExpressMvcConfig,
useValue: {},
})
Repository.global.register({
Repository.global.register(REGISTRATION_SOURCE, {
provide: RouteGenerator,
useValue: {
generateRoutes: stub().returns([]),
},
})
Repository.global.register({
Repository.global.register(REGISTRATION_SOURCE, {
provide: RouteInitializer,
useValue: {},
})
2 changes: 1 addition & 1 deletion packages/dandi/core-node/src/file.system.scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class FileSystemScanner implements Scanner {
await Promise.all(
this.config.map(async (config) => {
const modules = await this.scanDir(config, process.cwd())
modules.forEach((module) => repo.register(module))
modules.forEach((module) => repo.register(this, module))
}),
)
return repo
Expand Down
2 changes: 1 addition & 1 deletion packages/dandi/core-testing/src/test.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class TestHarness implements TestResolver {

// sanity checking!
expect(repo.get(__TestSanityChecker)).not.to.exist
repo.register(__TestSanityChecker)
repo.register(this, __TestSanityChecker)
expect(repo.get(__TestSanityChecker)).to.exist

globalRepoStub = stub(Repository, 'global').get(() => repo)
Expand Down
5 changes: 4 additions & 1 deletion packages/dandi/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export * from './src/container'
export * from './src/container.error'
export * from './src/now'
export * from './src/inject.decorator'
export * from './src/injectable.decorator'
export { Injectable, injectableDecorator, InjectableOption, Singleton, Multi, NoSelf } from './src/injectable.decorator'
export * from './src/injectable.metadata'
export * from './src/injection.context'
export * from './src/injection.context.util'
Expand All @@ -22,7 +22,10 @@ export * from './src/on-config'
export * from './src/opinionated.token'
export * from './src/optional.decorator'
export * from './src/provider'
export * from './src/provider.type.error'
export * from './src/repository'
export * from './src/repository.errors'
export * from './src/repository-registration'
export * from './src/resolve.result'
export * from './src/resolver'
export * from './src/resolver.context'
Expand Down
11 changes: 10 additions & 1 deletion packages/dandi/core/logging/src/console.log-listener.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,18 @@ describe('ConsoleLogListener', function() {
this.logger.log(this.entry)
const time = new Date(this.ts)

const hours = time
.getHours()
.toString()
.padStart(2, '0')
const minutes = time
.getMinutes()
.toString()
.padStart(2, '0')

expect(console.info).to.have.been
.calledOnce
.calledWithExactly(`[${time.getHours()}:${time.getMinutes()}]`, 'test message')
.calledWithExactly(`[${hours}:${minutes}]`, 'test message')
})

it('custom tag formatter', function() {
Expand Down
27 changes: 22 additions & 5 deletions packages/dandi/core/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Disposable } from '@dandi/common'
import { RepositoryRegistrationSource } from '@dandi/core/src/repository-registration'

import { Bootstrapper } from './bootstrapper'
import { ContainerError, ContainerNotInitializedError, MissingTokenError } from './container.error'
Expand Down Expand Up @@ -196,15 +197,19 @@ export class Container<TConfig extends ContainerConfig = ContainerConfig> implem
}

// register self as the Resolver
this.repository.register({
const source = {
constructor: this.constructor,
tag: '.preInit',
}
this.repository.register(source, {
provide: Resolver,
useValue: this,
})

// register explicitly set providers
// this must happen before scanning so that scanners can be specified in the providers config
if (this.config.providers) {
this.registerProviders(this.config.providers)
this.registerProviders(source, this.config.providers)
}

this.initialized = true
Expand Down Expand Up @@ -277,12 +282,24 @@ export class Container<TConfig extends ContainerConfig = ContainerConfig> implem
logger.debug(`Application started after ${now() - this.startTs}ms`)
}

private registerProviders(module: any): void {
private registerProviders(parentSource: RepositoryRegistrationSource, module: any): void {
if (Array.isArray(module)) {
module.forEach((provider) => this.registerProviders(provider))
const source = module.constructor === Array ?
// use parentSource if the "module" is just a plain array to avoid an extra useless entry in the source chain
parentSource :
{
constructor: module.constructor,
parent: parentSource,
}
module.forEach((provider) => this.registerProviders(source, provider))
return
}
this.repository.register(module)
const source = {
constructor: this.constructor,
parent: parentSource,
tag: '.config.providers',
}
this.repository.register(source, module)
}

private async resolveParam<T>(
Expand Down
8 changes: 5 additions & 3 deletions packages/dandi/core/src/injectable.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { INJECTABLE_REGISTRATION_SOURCE } from '@dandi/core/testing'

import { expect } from 'chai'
import { SinonSpy, spy } from 'sinon'

Expand All @@ -19,7 +21,7 @@ describe('@Injectable', () => {
Injectable()(TestClass)

expect(register).to.have.been.calledOnce
expect(register).to.have.been.calledWith(TestClass, {})
expect(register).to.have.been.calledWith(INJECTABLE_REGISTRATION_SOURCE, TestClass, {})
})

it('registers the decorated class for the specified token', () => {
Expand All @@ -31,7 +33,7 @@ describe('@Injectable', () => {
Injectable(FooClass)(TestClass)

expect(register).to.have.been.calledOnce
expect(register).to.have.been.calledWith(TestClass, { provide: FooClass })
expect(register).to.have.been.calledWith(INJECTABLE_REGISTRATION_SOURCE, TestClass, { provide: FooClass })
})

it('throws if the specified token is not a valid InjectionToken', () => {
Expand All @@ -46,7 +48,7 @@ describe('@Injectable', () => {
Injectable(Singleton, Multi)(TestClass)

expect(register).to.have.been.calledOnce
expect(register).to.have.been.calledWith(TestClass, {
expect(register).to.have.been.calledWith(INJECTABLE_REGISTRATION_SOURCE, TestClass, {
multi: true,
singleton: true,
})
Expand Down
9 changes: 7 additions & 2 deletions packages/dandi/core/src/injectable.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { ProviderOptions } from './provider'
import { Repository } from './repository'
import { InjectionToken, InjectionTokenTypeError, isInjectionToken } from './injection.token'

export interface InjectableDecorator<T> {
export const INJECTABLE_REGISTRATION_SOURCE = {
constructor: Injectable,
tag: '.global',
}

export interface InjectableDecorator<T> extends ClassDecorator {
(options?: InjectionToken<T>): ClassDecorator;
new (options?: InjectionToken<T>): InjectionToken<T>;
}
Expand All @@ -22,7 +27,7 @@ export function injectableDecorator<T>(
providerOptions.provide = injectable
}
injectableOptions.forEach((option) => option.setOptions(providerOptions))
Repository.global.register(target, providerOptions)
Repository.global.register(INJECTABLE_REGISTRATION_SOURCE, target, providerOptions)
Reflect.set(target, ProviderOptions.valueOf() as symbol, providerOptions)
}

Expand Down
6 changes: 5 additions & 1 deletion packages/dandi/core/src/manual.scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ export class ManualScanner implements Scanner {

public async scan(): Promise<Repository> {
const repo = Repository.for(this)
const source = {
constructor: this.constructor,
tag: '.scan',
}
this.config.forEach((config) => {
const modules = config()
modules.forEach((module) => {
repo.register(module)
repo.register(source, module)
})
})
return repo
Expand Down
2 changes: 1 addition & 1 deletion packages/dandi/core/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class Module extends Array<Registerable> {

public static moduleInfo(target: any): ModuleInfo {
if (!target) {
return null
return undefined
}
if (isInjectionToken(target) && !isConstructor(target)) {
return MODULE_INFO_REG.get(target)
Expand Down
5 changes: 5 additions & 0 deletions packages/dandi/core/src/repository-registration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface RepositoryRegistrationSource {
constructor: Function
tag?: string
parent?: RepositoryRegistrationSource
}
17 changes: 13 additions & 4 deletions packages/dandi/core/src/repository.errors.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { AppError } from '@dandi/common'

import { RepositoryRegistrationSource } from './repository-registration'

import { Module, ModuleInfo } from './module'

function getRegistrationTargetSourceString(source: RepositoryRegistrationSource): string {
const str = source.parent ? `${getRegistrationTargetSourceString(source.parent)} -> ` : ''
const moduleInfo = Module.moduleInfo(source.constructor)
const sourceName = moduleInfo ? `(${moduleInfo.package}#${moduleInfo.name}):${source.constructor.name}` : source.constructor.name
return `${str}${sourceName}${source.tag || ''}`
}

export class RegistrationError extends AppError {
public readonly moduleInfo: ModuleInfo;

constructor(public readonly target: any) {
super()
constructor(message: string, public readonly target: any) {
super(message)
this.moduleInfo = Module.moduleInfo(target)
}
}

export class InvalidRegistrationTargetError extends RegistrationError {
constructor(target: any, public readonly options: any) {
super(target)
constructor(public readonly source: RepositoryRegistrationSource, target: any, public readonly options: any) {
super(`Invalid Registration Target '${target}' specified by ${getRegistrationTargetSourceString(source)}`, target)
}
}

Expand Down

0 comments on commit 3dee0cd

Please sign in to comment.