Skip to content

Commit eae0da3

Browse files
committed
feat(core): allow components to expose an array of bindings
1 parent 8f77cef commit eae0da3

File tree

6 files changed

+201
-34
lines changed

6 files changed

+201
-34
lines changed

docs/site/Creating-components.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ export class MyComponent implements Component {
2020
this.providers = {
2121
'my-value': MyValueProvider,
2222
};
23+
this.classes = {
24+
'my-validator': MyValidator,
25+
};
26+
27+
const bindingX = new Binding('x').to('Value X');
28+
const bindingY = new Binding('y').toClass(ClassY);
29+
this.bindings = [bindingX, bindingY];
2330
}
2431
}
2532
```
@@ -28,10 +35,22 @@ When a component is mounted to an application, a new instance of the component
2835
class is created and then:
2936

3037
- Each Controller class is registered via `app.controller()`,
31-
- Each Provider is bound to its key in `providers` object.
38+
- Each Provider is bound to its key in `providers` object via
39+
`app.bind(key).toProvider(providerClass)`
40+
- Each Class is bound to its key in `classes` object via
41+
`app.bind(key).toClass(cls)`
42+
- Each Binding is added via `app.add(binding)`
43+
44+
Please note that `providers` and `classes` are shortcuts for provider and class
45+
`bindings`.
3246

3347
The example `MyComponent` above will add `MyController` to application's API and
34-
create a new binding `my-value` that will be resolved using `MyValueProvider`.
48+
create the following bindings to the application context:
49+
50+
- `my-value` -> `MyValueProvider` (provider)
51+
- `my-validator` -> `MyValidator` (class)
52+
- `x` -> `'Value X'` (value)
53+
- `y` -> `ClassY` (class)
3554

3655
## Providers
3756

packages/context/src/binding.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export class Binding<T = BoundValue> {
383383
*
384384
* @param provider The value provider to use.
385385
*/
386-
public toProvider(providerClass: Constructor<Provider<T>>): this {
386+
toProvider(providerClass: Constructor<Provider<T>>): this {
387387
/* istanbul ignore if */
388388
if (debug.enabled) {
389389
debug('Bind %s to provider %s', this.key, providerClass.name);
@@ -436,4 +436,14 @@ export class Binding<T = BoundValue> {
436436
}
437437
return json;
438438
}
439+
440+
/**
441+
* A static method to create a binding so that we can do
442+
* `Binding.bind('foo').to('bar');` as `new Binding('foo').to('bar')` is not
443+
* easy to read.
444+
* @param key Binding key
445+
*/
446+
static bind(key: string): Binding {
447+
return new Binding(key);
448+
}
439449
}

packages/context/src/context.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,23 @@ export class Context {
4242
* Create a binding with the given key in the context. If a locked binding
4343
* already exists with the same key, an error will be thrown.
4444
*
45-
* @param keyOrBinding Binding key or a binding
45+
* @param key Binding key
4646
*/
4747
bind<ValueType = BoundValue>(
48-
keyOrBinding: BindingAddress<ValueType> | Binding,
48+
key: BindingAddress<ValueType>,
4949
): Binding<ValueType> {
50-
let key: string;
51-
let binding: Binding<ValueType>;
52-
if (keyOrBinding instanceof Binding) {
53-
key = keyOrBinding.key;
54-
binding = keyOrBinding;
55-
} else {
56-
key = keyOrBinding.toString();
57-
binding = new Binding<ValueType>(key);
58-
}
50+
const binding = new Binding<ValueType>(key.toString());
51+
this.add(binding);
52+
return binding;
53+
}
5954

55+
/**
56+
* Add a binding to the context. If a locked binding already exists with the
57+
* same key, an error will be thrown.
58+
* @param binding The configured binding to be added
59+
*/
60+
add<ValueType = BoundValue>(binding: Binding<ValueType>): this {
61+
const key = binding.key;
6062
/* istanbul ignore if */
6163
if (debug.enabled) {
6264
debug('Adding binding: %s', key);
@@ -70,7 +72,7 @@ export class Context {
7072
throw new Error(`Cannot rebind key "${key}" to a locked binding`);
7173
}
7274
this.registry.set(key, binding);
73-
return binding;
75+
return this;
7476
}
7577

7678
/**

packages/context/test/unit/context.unit.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,6 @@ describe('Context', () => {
7171
expect(result).to.be.true();
7272
});
7373

74-
it('accepts a binding', () => {
75-
const binding = new Binding('foo').to('bar');
76-
expect(ctx.bind(binding)).to.be.exactly(binding);
77-
const result = ctx.contains('foo');
78-
expect(result).to.be.true();
79-
});
80-
8174
it('returns a binding', () => {
8275
const binding = ctx.bind('foo');
8376
expect(binding).to.be.instanceOf(Binding);
@@ -87,6 +80,30 @@ describe('Context', () => {
8780
const key = 'a' + BindingKey.PROPERTY_SEPARATOR + 'b';
8881
expect(() => ctx.bind(key)).to.throw(/Binding key .* cannot contain/);
8982
});
83+
84+
it('rejects rebinding of a locked key', () => {
85+
ctx.bind('foo').lock();
86+
expect(() => ctx.bind('foo')).to.throw(
87+
'Cannot rebind key "foo" to a locked binding',
88+
);
89+
});
90+
});
91+
92+
describe('add', () => {
93+
it('accepts a binding', () => {
94+
const binding = new Binding('foo').to('bar');
95+
ctx.add(binding);
96+
expect(ctx.getBinding(binding.key)).to.be.exactly(binding);
97+
const result = ctx.contains('foo');
98+
expect(result).to.be.true();
99+
});
100+
101+
it('rejects rebinding of a locked key', () => {
102+
ctx.bind('foo').lock();
103+
expect(() => ctx.add(new Binding('foo'))).to.throw(
104+
'Cannot rebind key "foo" to a locked binding',
105+
);
106+
});
90107
});
91108

92109
describe('contains', () => {

packages/core/src/component.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {Constructor, Provider, BoundValue} from '@loopback/context';
6+
import {Constructor, Provider, BoundValue, Binding} from '@loopback/context';
77
import {Server} from './server';
88
import {Application, ControllerClass} from './application';
99

1010
/**
11-
* A map of name/class pairs for binding providers
11+
* A map of provider classes to be bound to a context
1212
*/
1313
export interface ProviderMap {
1414
[key: string]: Constructor<Provider<BoundValue>>;
1515
}
1616

17+
/**
18+
* A map of classes to be bound to a context
19+
*/
20+
export interface ClassMap {
21+
[key: string]: Constructor<BoundValue>;
22+
}
23+
1724
/**
1825
* A component declares a set of artifacts so that they cane be contributed to
1926
* an application as a group
@@ -23,17 +30,46 @@ export interface Component {
2330
* An array of controller classes
2431
*/
2532
controllers?: ControllerClass[];
33+
2634
/**
27-
* A map of name/class pairs for binding providers
35+
* A map of providers to be bound to the application context
36+
* * For example:
37+
* ```ts
38+
* {
39+
* 'authentication.strategies.ldap': LdapStrategyProvider
40+
* }
41+
* ```
2842
*/
2943
providers?: ProviderMap;
44+
45+
/**
46+
* A map of classes to be bound to the application context.
47+
*
48+
* For example:
49+
* ```ts
50+
* {
51+
* 'rest.body-parsers.xml': XmlBodyParser
52+
* }
53+
* ```
54+
*/
55+
classes?: ClassMap;
56+
3057
/**
3158
* A map of name/class pairs for servers
3259
*/
3360
servers?: {
3461
[name: string]: Constructor<Server>;
3562
};
3663

64+
/**
65+
* An array of bindings to be aded to the application context. For example,
66+
* ```ts
67+
* const bindingX = new Binding('x').to('Value X');
68+
* this.bindings = [bindingX]
69+
* ```
70+
*/
71+
bindings?: Binding[];
72+
3773
/**
3874
* Other properties
3975
*/
@@ -49,9 +85,9 @@ export interface Component {
4985
* @param {Component} component
5086
*/
5187
export function mountComponent(app: Application, component: Component) {
52-
if (component.controllers) {
53-
for (const controllerCtor of component.controllers) {
54-
app.controller(controllerCtor);
88+
if (component.classes) {
89+
for (const classKey in component.classes) {
90+
app.bind(classKey).toClass(component.classes[classKey]);
5591
}
5692
}
5793

@@ -61,6 +97,18 @@ export function mountComponent(app: Application, component: Component) {
6197
}
6298
}
6399

100+
if (component.bindings) {
101+
for (const binding of component.bindings) {
102+
app.add(binding);
103+
}
104+
}
105+
106+
if (component.controllers) {
107+
for (const controllerCtor of component.controllers) {
108+
app.controller(controllerCtor);
109+
}
110+
}
111+
64112
if (component.servers) {
65113
for (const serverKey in component.servers) {
66114
app.server(component.servers[serverKey], serverKey);

packages/core/test/unit/application.unit.ts

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {Constructor, Context} from '@loopback/context';
76
import {expect} from '@loopback/testlab';
8-
import {Application, Component, Server} from '../..';
7+
import {Application, Server, Component, CoreBindings} from '../..';
8+
import {
9+
Context,
10+
Constructor,
11+
Binding,
12+
Provider,
13+
inject,
14+
} from '@loopback/context';
915

1016
describe('Application', () => {
1117
describe('controller binding', () => {
@@ -35,10 +41,8 @@ describe('Application', () => {
3541

3642
describe('component binding', () => {
3743
let app: Application;
38-
class MyController {}
39-
class MyComponent implements Component {
40-
controllers = [MyController];
41-
}
44+
45+
class MyComponent implements Component {}
4246

4347
beforeEach(givenApp);
4448

@@ -56,6 +60,73 @@ describe('Application', () => {
5660
);
5761
});
5862

63+
it('binds controllers from a component', () => {
64+
class MyController {}
65+
66+
class MyComponentWithControllers implements Component {
67+
controllers = [MyController];
68+
}
69+
70+
app.component(MyComponentWithControllers);
71+
expect(
72+
app.getBinding('controllers.MyController').valueConstructor,
73+
).to.be.exactly(MyController);
74+
});
75+
76+
it('binds bindings from a component', () => {
77+
const binding = new Binding('foo');
78+
class MyComponentWithBindings implements Component {
79+
bindings = [binding];
80+
}
81+
82+
app.component(MyComponentWithBindings);
83+
expect(app.getBinding('foo')).to.be.exactly(binding);
84+
});
85+
86+
it('binds classes from a component', () => {
87+
class MyClass {}
88+
89+
class MyComponentWithClasses implements Component {
90+
classes = {'my-class': MyClass};
91+
}
92+
93+
app.component(MyComponentWithClasses);
94+
expect(app.contains('my-class')).to.be.true();
95+
expect(app.getBinding('my-class').valueConstructor).to.be.exactly(
96+
MyClass,
97+
);
98+
expect(app.getSync('my-class')).to.be.instanceof(MyClass);
99+
});
100+
101+
it('binds providers from a component', () => {
102+
class MyProvider implements Provider<string> {
103+
value() {
104+
return 'my-str';
105+
}
106+
}
107+
108+
class MyComponentWithProviders implements Component {
109+
providers = {'my-provider': MyProvider};
110+
}
111+
112+
app.component(MyComponentWithProviders);
113+
expect(app.contains('my-provider')).to.be.true();
114+
expect(app.getSync('my-provider')).to.be.eql('my-str');
115+
});
116+
117+
it('binds from a component constructor', () => {
118+
class MyComponentWithDI implements Component {
119+
constructor(@inject(CoreBindings.APPLICATION_INSTANCE) ctx: Context) {
120+
// Programmatically bind to the context
121+
ctx.bind('foo').to('bar');
122+
}
123+
}
124+
125+
app.component(MyComponentWithDI);
126+
expect(app.contains('foo')).to.be.true();
127+
expect(app.getSync('foo')).to.be.eql('bar');
128+
});
129+
59130
function givenApp() {
60131
app = new Application();
61132
}

0 commit comments

Comments
 (0)