Skip to content

Commit 10b703c

Browse files
committed
feat(core): allow components to expose an array of bindings
1 parent 591a13a commit 10b703c

File tree

5 files changed

+135
-27
lines changed

5 files changed

+135
-27
lines changed

packages/context/src/binding.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ export class Binding<T = BoundValue> {
318318
*
319319
* @param provider The value provider to use.
320320
*/
321-
public toProvider(providerClass: Constructor<Provider<T>>): this {
321+
toProvider(providerClass: Constructor<Provider<T>>): this {
322322
/* istanbul ignore if */
323323
if (debug.enabled) {
324324
debug('Bind %s to provider %s', this.key, providerClass.name);
@@ -375,4 +375,14 @@ export class Binding<T = BoundValue> {
375375
}
376376
return json;
377377
}
378+
379+
/**
380+
* A static method to create a binding so that we can do
381+
* `Binding.bind('foo').to('bar');` as `new Binding('foo').to('bar')` is not
382+
* easy to read.
383+
* @param key Binding key
384+
*/
385+
static bind(key: string): Binding {
386+
return new Binding(key);
387+
}
378388
}

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.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: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
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

@@ -14,6 +14,10 @@ export interface ProviderMap {
1414
[key: string]: Constructor<Provider<BoundValue>>;
1515
}
1616

17+
export interface ClassMap {
18+
[key: string]: Constructor<BoundValue>;
19+
}
20+
1721
/**
1822
* A component declares a set of artifacts so that they cane be contributed to
1923
* an application as a group
@@ -27,13 +31,21 @@ export interface Component {
2731
* A map of name/class pairs for binding providers
2832
*/
2933
providers?: ProviderMap;
34+
35+
classes?: ClassMap;
36+
3037
/**
3138
* A map of name/class pairs for servers
3239
*/
3340
servers?: {
3441
[name: string]: Constructor<Server>;
3542
};
3643

44+
/**
45+
* An array of bindings
46+
*/
47+
bindings?: Binding[];
48+
3749
/**
3850
* Other properties
3951
*/
@@ -49,15 +61,33 @@ export interface Component {
4961
* @param {Component} component
5062
*/
5163
export function mountComponent(app: Application, component: Component) {
52-
if (component.controllers) {
53-
for (const controllerCtor of component.controllers) {
54-
app.controller(controllerCtor);
64+
if (component.classes) {
65+
for (const classKey in component.classes) {
66+
const binding = Binding.bind(classKey).toClass(
67+
component.classes[classKey],
68+
);
69+
app.add(binding);
5570
}
5671
}
5772

5873
if (component.providers) {
5974
for (const providerKey in component.providers) {
60-
app.bind(providerKey).toProvider(component.providers[providerKey]);
75+
const binding = Binding.bind(providerKey).toProvider(
76+
component.providers[providerKey],
77+
);
78+
app.add(binding);
79+
}
80+
}
81+
82+
if (component.bindings) {
83+
for (const binding of component.bindings) {
84+
app.add(binding);
85+
}
86+
}
87+
88+
if (component.controllers) {
89+
for (const controllerCtor of component.controllers) {
90+
app.controller(controllerCtor);
6191
}
6292
}
6393

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {expect} from '@loopback/testlab';
7-
import {Application, Server, Component} from '../../index';
8-
import {Context, Constructor} from '@loopback/context';
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,9 +41,26 @@ describe('Application', () => {
3541

3642
describe('component binding', () => {
3743
let app: Application;
44+
const binding = new Binding('foo');
3845
class MyController {}
46+
class MyClass {}
47+
class MyProvider implements Provider<string> {
48+
value() {
49+
return 'my-str';
50+
}
51+
}
3952
class MyComponent implements Component {
4053
controllers = [MyController];
54+
bindings = [binding];
55+
classes = {'my-class': MyClass};
56+
providers = {'my-provider': MyProvider};
57+
}
58+
59+
class MyComponentWithDI implements Component {
60+
constructor(@inject(CoreBindings.APPLICATION_INSTANCE) ctx: Context) {
61+
// Porgramatically bind to the context
62+
ctx.bind('foo').to('bar');
63+
}
4164
}
4265

4366
beforeEach(givenApp);
@@ -56,6 +79,32 @@ describe('Application', () => {
5679
);
5780
});
5881

82+
it('binds bindings from a component', () => {
83+
app.component(MyComponent);
84+
expect(app.contains('controllers.MyController')).to.be.true();
85+
expect(app.getBinding('foo')).to.be.exactly(binding);
86+
});
87+
88+
it('binds classes from a component', () => {
89+
app.component(MyComponent);
90+
expect(app.contains('my-class')).to.be.true();
91+
expect(app.getBinding('my-class').valueConstructor).to.be.exactly(
92+
MyClass,
93+
);
94+
});
95+
96+
it('binds providers from a component', () => {
97+
app.component(MyComponent);
98+
expect(app.contains('my-provider')).to.be.true();
99+
expect(app.getSync('my-provider')).to.be.eql('my-str');
100+
});
101+
102+
it('binds from a component constructor', () => {
103+
app.component(MyComponentWithDI);
104+
expect(app.contains('foo')).to.be.true();
105+
expect(app.getSync('foo')).to.be.eql('bar');
106+
});
107+
59108
function givenApp() {
60109
app = new Application();
61110
}

0 commit comments

Comments
 (0)