diff --git a/docs/site/Binding.md b/docs/site/Binding.md index 5ba0cc0e1b0b..111c464b6f76 100644 --- a/docs/site/Binding.md +++ b/docs/site/Binding.md @@ -113,6 +113,21 @@ class MyValueProvider implements Provider { binding.toProvider(MyValueProvider); ``` +#### An alias + +An alias is the key with optional path to resolve the value from another +binding. For example, if we want to get options from RestServer for the API +explorer, we can configure the `apiExplorer.options` to be resolved from +`servers.RestServer.options#apiExplorer`. + +```ts +ctx.bind('servers.RestServer.options').to({apiExplorer: {path: '/explorer'}}); +ctx + .bind('apiExplorer.options') + .toAlias('servers.RestServer.options#apiExplorer'); +const apiExplorerOptions = await ctx.get('apiExplorer.options'); // => {path: '/explorer'} +``` + ### Configure the scope We allow a binding to be resolved within a context using one of the following diff --git a/packages/context/src/__tests__/unit/binding.unit.ts b/packages/context/src/__tests__/unit/binding.unit.ts index 55b700ab64ff..2e90c96976f4 100644 --- a/packages/context/src/__tests__/unit/binding.unit.ts +++ b/packages/context/src/__tests__/unit/binding.unit.ts @@ -191,6 +191,64 @@ describe('Binding', () => { }); }); + describe('toAlias(bindingKeyWithPath)', async () => { + it('binds to another binding with sync value', () => { + ctx.bind('parent.options').to({child: {disabled: true}}); + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = ctx.getSync('child.options'); + expect(childOptions).to.eql({disabled: true}); + }); + + it('binds to another binding before the it is bound', () => { + ctx.bind('child.options').toAlias('parent.options#child'); + ctx.bind('parent.options').to({child: {disabled: true}}); + const childOptions = ctx.getSync('child.options'); + expect(childOptions).to.eql({disabled: true}); + }); + + it('binds to another binding with async value', async () => { + ctx + .bind('parent.options') + .toDynamicValue(() => Promise.resolve({child: {disabled: true}})); + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = await ctx.get('child.options'); + expect(childOptions).to.eql({disabled: true}); + }); + + it('reports error if alias binding cannot be resolved', () => { + ctx.bind('child.options').toAlias('parent.options#child'); + expect(() => ctx.getSync('child.options')).to.throw( + /The key 'parent.options' is not bound to any value in context/, + ); + }); + + it('reports error if alias binding cannot be resolved - async', async () => { + ctx.bind('child.options').toAlias('parent.options#child'); + return expect(ctx.get('child.options')).to.be.rejectedWith( + /The key 'parent.options' is not bound to any value in context/, + ); + }); + + it('allows optional if alias binding cannot be resolved', () => { + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = ctx.getSync('child.options', {optional: true}); + expect(childOptions).to.be.undefined(); + }); + + it('allows optional if alias binding cannot be resolved - async', async () => { + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = await ctx.get('child.options', {optional: true}); + expect(childOptions).to.be.undefined(); + }); + + it('sets type to ALIAS', () => { + const childBinding = ctx + .bind('child.options') + .toAlias('parent.options#child'); + expect(childBinding.type).to.equal(BindingType.ALIAS); + }); + }); + describe('apply(templateFn)', () => { it('applies a template function', async () => { binding.apply(b => { diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index a434b625e6b0..ce72d7cde9c7 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -117,6 +117,10 @@ export enum BindingType { * A provider class with `value()` function to get the value */ PROVIDER = 'Provider', + /** + * A alias to another binding key with optional path + */ + ALIAS = 'Alias', } // tslint:disable-next-line:no-any @@ -497,6 +501,24 @@ export class Binding { return this; } + /** + * Bind the key to an alias of another binding + * @param keyWithPath Target binding key with optional path, + * such as `servers.RestServer.options#apiExplorer` + */ + toAlias(keyWithPath: BindingAddress) { + /* istanbul ignore if */ + if (debug.enabled) { + debug('Bind %s to alias %s', this.key, keyWithPath); + } + this._type = BindingType.ALIAS; + this._setGetValue((ctx, optionsOrSession) => { + const options = asResolutionOptions(optionsOrSession); + return ctx.getValueOrPromise(keyWithPath, options); + }); + return this; + } + /** * Unlock the binding */