-
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
685195c
commit 979a428
Showing
5 changed files
with
418 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,177 @@ | ||
// 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 | ||
* - model | ||
* - dataSource | ||
* - class (default) | ||
*/ | ||
type?: | ||
| 'controller' | ||
| 'repository' | ||
| 'component' | ||
| 'provider' | ||
| 'server' | ||
| 'model' | ||
| 'dataSource' | ||
| 'class' | ||
| string; // Still allow string for extensibility | ||
/** | ||
* 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,54 @@ | ||
# Feature: @injectable for classes representing various artifacts | ||
|
||
- In order to automatically bind classes for various artifacts to a context | ||
- As a developer | ||
- I want to decorate my classes to provide more metadata | ||
- So that the bootstrapper can bind them to a context according to the metadata | ||
|
||
## Scenario: Add metadata to a class to facilitate automatic binding | ||
|
||
When the bootstrapper discovers a file under `controllers` folder, it tries to | ||
bind the exported constructs to the context automatically. | ||
|
||
For example: | ||
|
||
controllers/log-controller.ts | ||
```ts | ||
export class LogController { | ||
} | ||
|
||
export const LOG_LEVEL = 'info'; | ||
export class LogProvider implements Provider<Logger> { | ||
value() { | ||
return msg => console.log(msg); | ||
} | ||
} | ||
``` | ||
|
||
There are three exported entries from `log-controller.ts` and the bootstrapper | ||
does not have enough information to bind them to the context correctly. For | ||
example, it's impossible for the bootstrapper to infer that `LogProvider` is a | ||
provider so that the class can be bound using | ||
`ctx.bind('providers.LogProvider').toProvider(LogProvider)`. | ||
|
||
Developers can help the bootstrapper by decorating these classes with | ||
`@injectable`. | ||
|
||
```ts | ||
@injectable({tags: ['log']}) | ||
export class LogController { | ||
} | ||
|
||
export const LOG_LEVEL = 'info'; | ||
|
||
@injectable({type: 'provider', tags: ['log']}) | ||
export class LogProvider implements Provider<Logger> { | ||
value() { | ||
return msg => console.log(msg); | ||
} | ||
} | ||
``` | ||
|
||
Please note that we don't intend to use `@injectable` to help the bootstrapper | ||
discover such artifacts. The purpose of `@injectable` is to allow developers to | ||
provide more metadata on how the class should be bound. |
Oops, something went wrong.