-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(context): add
@injectable
to decorate bindable classes
- Loading branch information
1 parent
2956bf9
commit 03fd769
Showing
4 changed files
with
353 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Copyright IBM Corp. 2018. All Rights Reserved. | ||
// Node module: @loopback/context | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {MetadataInspector, ClassDecoratorFactory} from '@loopback/metadata'; | ||
|
||
import {BindingScope, Binding} from './binding'; | ||
import {Context} from './context'; | ||
import {Constructor} from './value-promise'; | ||
import {Provider} from './provider'; | ||
|
||
const INJECTABLE_CLASS_KEY = 'injectable'; | ||
|
||
/** | ||
* Metadata for an injectable class | ||
*/ | ||
export interface InjectableMetadata { | ||
/** | ||
* Type of the artifact. Valid values are: | ||
* - controller | ||
* - repository | ||
* - component | ||
* - provider | ||
* - server | ||
* - class (default) | ||
*/ | ||
type?: string; | ||
/** | ||
* Name of the artifact, default to the class name | ||
*/ | ||
name?: string; | ||
/** | ||
* Binding key, default to `${type}.${name}` | ||
*/ | ||
key?: string; | ||
/** | ||
* Optional tags for the binding | ||
*/ | ||
tags?: string[]; | ||
/** | ||
* Binding scope | ||
*/ | ||
scope?: BindingScope; | ||
} | ||
|
||
class InjectableDecoratorFactory extends ClassDecoratorFactory< | ||
InjectableMetadata | ||
> { | ||
mergeWithInherited(inherited: InjectableMetadata, target: Function) { | ||
const spec = super.mergeWithInherited(inherited, target); | ||
if (!this.spec.name) { | ||
delete spec.name; | ||
} | ||
return spec; | ||
} | ||
} | ||
/** | ||
* Mark a class to be injectable or bindable for context based dependency | ||
* injection. | ||
* | ||
* @example | ||
* ```ts | ||
* @injectable({ | ||
* type: 'controller', | ||
* name: 'my-controller', | ||
* scope: BindingScope.SINGLETON}, | ||
* ) | ||
* export class MyController { | ||
* } | ||
* ``` | ||
* | ||
* @param spec The metadata for bindings | ||
*/ | ||
export function injectable(spec: InjectableMetadata = {}) { | ||
return InjectableDecoratorFactory.createDecorator(INJECTABLE_CLASS_KEY, spec); | ||
} | ||
|
||
/** | ||
* `@provider` as a shortcut of `@injectable({type: 'provider'}) | ||
* @param spec | ||
*/ | ||
export function provider( | ||
spec: InjectableMetadata = {}, | ||
): // tslint:disable-next-line:no-any | ||
((target: Constructor<Provider<any>>) => void) { | ||
return injectable(Object.assign(spec, {type: 'provider'})); | ||
} | ||
|
||
/** | ||
* Get the metadata for an injectable class | ||
* @param target The target class | ||
*/ | ||
export function getInjectableMetadata( | ||
target: Function, | ||
): InjectableMetadata | undefined { | ||
return MetadataInspector.getClassMetadata<InjectableMetadata>( | ||
INJECTABLE_CLASS_KEY, | ||
target, | ||
{ | ||
ownMetadataOnly: true, | ||
}, | ||
); | ||
} | ||
|
||
export const TYPE_NAMESPACES: {[name: string]: string} = { | ||
controller: 'controllers', | ||
repository: 'repositories', | ||
model: 'models', | ||
dataSource: 'dataSources', | ||
server: 'servers', | ||
class: 'classes', | ||
provider: 'providers', | ||
}; | ||
|
||
function getNamespace(type: string) { | ||
if (type in TYPE_NAMESPACES) { | ||
return TYPE_NAMESPACES[type]; | ||
} else { | ||
return `${type}s`; | ||
} | ||
} | ||
|
||
/** | ||
* Bind the injectable class to a given context | ||
* @param ctx The context | ||
* @param cls The target class | ||
*/ | ||
export function bindInjectable( | ||
ctx: Context, | ||
cls: Constructor<object>, | ||
): Binding { | ||
const spec = getInjectableMetadata(cls); | ||
if (spec === undefined) { | ||
throw new Error( | ||
`Target class ${cls.name} is not decorated with @injectable`, | ||
); | ||
} | ||
const type = spec.type || 'class'; | ||
const name = spec.name || cls.name; | ||
// Default binding key is ${plural form of the type}.${name} | ||
const key = spec.key || `${getNamespace(type)}.${name}`; | ||
const binding = ctx.bind(key); | ||
switch (type) { | ||
case 'provider': | ||
// tslint:disable-next-line:no-any | ||
binding.toProvider(cls as Constructor<Provider<any>>); | ||
break; | ||
default: | ||
binding.toClass(cls); | ||
} | ||
// Set tags if present | ||
if (spec.tags) { | ||
binding.tag(spec.tags); | ||
} | ||
// Set some tags for the metadata | ||
binding | ||
.tag(`name:${name}`) | ||
.tag(`type:${type}`) | ||
.tag(`${type}:${name}`); | ||
// Set scope if present | ||
if (spec.scope) { | ||
binding.inScope(spec.scope); | ||
} | ||
return binding; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// Copyright IBM Corp. 2017,2018. All Rights Reserved. | ||
// Node module: @loopback/context | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {expect} from '@loopback/testlab'; | ||
import { | ||
injectable, | ||
getInjectableMetadata, | ||
BindingScope, | ||
InjectableMetadata, | ||
Context, | ||
Provider, | ||
} from '../..'; | ||
import {bindInjectable, provider} from '../../src/injectable'; | ||
|
||
describe('@injectable', () => { | ||
it('decorates a class', () => { | ||
const spec: InjectableMetadata = { | ||
type: 'controller', | ||
name: 'my-controller', | ||
tags: ['rest'], | ||
scope: BindingScope.SINGLETON, | ||
}; | ||
|
||
@injectable(spec) | ||
class MyController {} | ||
|
||
expect(getInjectableMetadata(MyController)).to.eql(spec); | ||
}); | ||
|
||
it('has to be on the target class', () => { | ||
const spec: InjectableMetadata = { | ||
type: 'controller', | ||
name: 'my-controller', | ||
tags: ['rest'], | ||
scope: BindingScope.SINGLETON, | ||
}; | ||
|
||
@injectable(spec) | ||
class MyController {} | ||
|
||
class MySubController extends MyController {} | ||
|
||
expect(getInjectableMetadata(MySubController)).to.be.undefined(); | ||
}); | ||
|
||
it('inherits attributes except name from the base class', () => { | ||
const spec: InjectableMetadata = { | ||
type: 'controller', | ||
name: 'my-controller', | ||
tags: ['rest'], | ||
scope: BindingScope.SINGLETON, | ||
}; | ||
|
||
@injectable(spec) | ||
class MyController {} | ||
|
||
@injectable() | ||
class MySubController extends MyController {} | ||
|
||
expect(getInjectableMetadata(MySubController)).to.eql({ | ||
type: 'controller', | ||
tags: ['rest'], | ||
scope: BindingScope.SINGLETON, | ||
}); | ||
}); | ||
|
||
it('decorates a provider classes', () => { | ||
const spec = { | ||
type: 'provider', | ||
tags: ['rest'], | ||
scope: BindingScope.CONTEXT, | ||
}; | ||
|
||
@provider(spec) | ||
class MyProvider implements Provider<string> { | ||
value() { | ||
return 'my-value'; | ||
} | ||
} | ||
expect(getInjectableMetadata(MyProvider)).to.eql({ | ||
type: 'provider', | ||
tags: ['rest'], | ||
scope: BindingScope.CONTEXT, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('bindInjectable()', () => { | ||
it('binds injectable classes', () => { | ||
const spec: InjectableMetadata = { | ||
type: 'controller', | ||
name: 'my-controller', | ||
tags: ['rest'], | ||
scope: BindingScope.SINGLETON, | ||
}; | ||
|
||
@injectable(spec) | ||
class MyController {} | ||
|
||
const ctx = new Context(); | ||
const binding = bindInjectable(ctx, MyController); | ||
expect(binding.key).to.eql('controllers.my-controller'); | ||
expect(binding.scope).to.eql(spec.scope); | ||
expect(Array.from(binding.tags)).to.containDeep(spec.tags); | ||
expect(Array.from(binding.tags)).to.containDeep([ | ||
'name:my-controller', | ||
'type:controller', | ||
'controller:my-controller', | ||
]); | ||
expect(ctx.getSync(binding.key)).to.be.instanceof(MyController); | ||
}); | ||
|
||
it('binds injectable provider classes', () => { | ||
const spec: InjectableMetadata = { | ||
type: 'provider', | ||
tags: ['rest'], | ||
scope: BindingScope.CONTEXT, | ||
}; | ||
|
||
@injectable(spec) | ||
class MyProvider implements Provider<string> { | ||
value() { | ||
return 'my-value'; | ||
} | ||
} | ||
|
||
const ctx = new Context(); | ||
const binding = bindInjectable(ctx, MyProvider); | ||
expect(binding.key).to.eql('providers.MyProvider'); | ||
expect(binding.scope).to.eql(spec.scope); | ||
expect(Array.from(binding.tags)).to.containDeep(spec.tags); | ||
expect(Array.from(binding.tags)).to.containDeep([ | ||
'name:MyProvider', | ||
'type:provider', | ||
'provider:MyProvider', | ||
]); | ||
expect(ctx.getSync(binding.key)).to.eql('my-value'); | ||
}); | ||
|
||
it('honors the binding key', () => { | ||
const spec: InjectableMetadata = { | ||
type: 'controller', | ||
key: 'controllers.my', | ||
name: 'my-controller', | ||
}; | ||
|
||
@injectable(spec) | ||
class MyController {} | ||
|
||
const ctx = new Context(); | ||
const binding = bindInjectable(ctx, MyController); | ||
expect(binding.key).to.eql('controllers.my'); | ||
|
||
expect(Array.from(binding.tags)).to.containDeep([ | ||
'name:my-controller', | ||
'type:controller', | ||
'controller:my-controller', | ||
]); | ||
}); | ||
|
||
it('defaults type to class', () => { | ||
const spec: InjectableMetadata = {}; | ||
|
||
@injectable(spec) | ||
class MyClass {} | ||
|
||
const ctx = new Context(); | ||
const binding = bindInjectable(ctx, MyClass); | ||
expect(binding.key).to.eql('classes.MyClass'); | ||
|
||
expect(Array.from(binding.tags)).to.containDeep([ | ||
'name:MyClass', | ||
'type:class', | ||
'class:MyClass', | ||
]); | ||
}); | ||
}); |