Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(context): allow @config.* to specify the target binding key #3329

Merged
merged 1 commit into from
Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions docs/site/Context.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,9 +599,9 @@ export class RestServer {
}
```

The `@config.*` decorators can take an optional `configPath` parameter to allow
the configuration value to be a deep property of the bound value. For example,
`@config('port')` injects `RestServerConfig.port` to the target.
The `@config.*` decorators can take an optional `propertyPath` parameter to
allow the configuration value to be a deep property of the bound value. For
example, `@config('port')` injects `RestServerConfig.port` to the target.

```ts
export class MyRestServer {
Expand All @@ -617,6 +617,25 @@ export class MyRestServer {
}
```

We also allow `@config.*` to be resolved from another binding than the current
one:

```ts
export class MyRestServer {
constructor(
// Inject the `rest.host` from the application config
@config({fromBinding: 'application', propertyPath: 'rest.host'})
host: string,
// Inject the `rest.port` from the application config
@config({fromBinding: 'application', propertyPath: 'rest.port'})
port: number,
) {
// ...
}
// ...
}
```

Now we can use `context.configure()` to provide configuration for target
bindings.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('Context bindings - injecting configuration for bound artifacts', () =>
expect(server1.configObj).to.eql({port: 3000});
});

it('allows configPath for injection', async () => {
it('allows propertyPath for injection', async () => {
class RestServerWithPort {
constructor(@config('port') public port: number) {}
}
Expand All @@ -78,6 +78,69 @@ describe('Context bindings - injecting configuration for bound artifacts', () =>
expect(server1.port).to.eql(3000);
});

it('allows propertyPath for injection metadata', async () => {
class RestServerWithPort {
constructor(@config({propertyPath: 'port'}) public port: number) {}
}

// Bind configuration
ctx
.configure('servers.rest.server1')
.toDynamicValue(() => Promise.resolve({port: 3000}));

// Bind RestServer
ctx.bind('servers.rest.server1').toClass(RestServerWithPort);

// Resolve an instance of RestServer
// Expect server1.config to be `{port: 3000}
const server1 = await ctx.get<RestServerWithPort>('servers.rest.server1');
expect(server1.port).to.eql(3000);
});

it('allows propertyPath & fromBinding for injection metadata', async () => {
class RestServerWithPort {
constructor(
@config({propertyPath: 'port', fromBinding: 'restServer'})
public port: number,
) {}
}

// Bind configuration
ctx
.configure('restServer')
.toDynamicValue(() => Promise.resolve({port: 3000}));

// Bind RestServer
ctx.bind('servers.rest.server1').toClass(RestServerWithPort);

// Resolve an instance of RestServer
// Expect server1.config to be `{port: 3000}
const server1 = await ctx.get<RestServerWithPort>('servers.rest.server1');
expect(server1.port).to.eql(3000);
});

it('allows propertyPath parameter & fromBinding for injection metadata', async () => {
class RestServerWithPort {
constructor(
@config('port', {fromBinding: 'restServer'})
public port: number,
) {}
}

// Bind configuration
ctx
.configure('restServer')
.toDynamicValue(() => Promise.resolve({port: 3000}));

// Bind RestServer
ctx.bind('servers.rest.server1').toClass(RestServerWithPort);

// Resolve an instance of RestServer
// Expect server1.config to be `{port: 3000}
const server1 = await ctx.get<RestServerWithPort>('servers.rest.server1');
expect(server1.port).to.eql(3000);
});

const LOGGER_KEY = 'loggers.Logger';
it('injects a getter function to access config', async () => {
class Logger {
Expand Down Expand Up @@ -111,6 +174,44 @@ describe('Context bindings - injecting configuration for bound artifacts', () =>
expect(configObj).to.be.undefined();
});

it('injects a getter function with fromBinding to access config', async () => {
class MyService {
constructor(
@config.getter({fromBinding: LOGGER_KEY})
public configGetter: Getter<LoggerConfig | undefined>,
) {}
}

// Bind logger configuration
ctx.configure(LOGGER_KEY).to({level: 'INFO'});

// Bind MyService
ctx.bind('services.MyService').toClass(MyService);

const myService = await ctx.get<MyService>('services.MyService');
const configObj = await myService.configGetter();
expect(configObj).to.eql({level: 'INFO'});
});

it('injects a getter function with propertyPath, {fromBinding} to access config', async () => {
class MyService {
constructor(
@config.getter('level', {fromBinding: LOGGER_KEY})
public levelGetter: Getter<string | undefined>,
) {}
}

// Bind logger configuration
ctx.configure(LOGGER_KEY).to({level: 'INFO'});

// Bind MyService
ctx.bind('services.MyService').toClass(MyService);

const myService = await ctx.get<MyService>('services.MyService');
const configObj = await myService.levelGetter();
expect(configObj).to.eql('INFO');
});

it('injects a view to access config', async () => {
class Logger {
constructor(
Expand Down Expand Up @@ -161,6 +262,56 @@ describe('Context bindings - injecting configuration for bound artifacts', () =>
expect(level).to.eql('DEBUG');
});

it('injects a view to access config with {fromBinding, propertyPath}', async () => {
class MyService {
constructor(
@config.view({fromBinding: LOGGER_KEY, propertyPath: 'level'})
public configView: ContextView<string>,
) {}
}

// Bind logger configuration
ctx.configure(LOGGER_KEY).to({level: 'INFO'});

// Bind MyService
ctx.bind('services.MyService').toClass(MyService);

const myService = await ctx.get<MyService>('services.MyService');
let level = await myService.configView.singleValue();
expect(level).to.eql('INFO');

// Update logger configuration
ctx.configure(LOGGER_KEY).to({level: 'DEBUG'});

level = await myService.configView.singleValue();
expect(level).to.eql('DEBUG');
});

it('injects a view to access config with parameter, {fromBinding}', async () => {
class MyService {
constructor(
@config.view('level', {fromBinding: LOGGER_KEY})
public configView: ContextView<string>,
) {}
}

// Bind logger configuration
ctx.configure(LOGGER_KEY).to({level: 'INFO'});

// Bind MyService
ctx.bind('services.MyService').toClass(MyService);

const myService = await ctx.get<MyService>('services.MyService');
let level = await myService.configView.singleValue();
expect(level).to.eql('INFO');

// Update logger configuration
ctx.configure(LOGGER_KEY).to({level: 'DEBUG'});

level = await myService.configView.singleValue();
expect(level).to.eql('DEBUG');
});

it('rejects injection of config view if the target type is not ContextView', async () => {
class Logger {
constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(store.optionXY).to.eql('y');
});

it('injects config if the configPath is not present', () => {
it('injects config if the propertyPath is not present', () => {
class Store {
constructor(@config() public configObj: object) {}
}
Expand All @@ -778,7 +778,7 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(store.configObj).to.eql({x: 1, y: 'a'});
});

it("injects config if the configPath is ''", () => {
it("injects config if the propertyPath is ''", () => {
class Store {
constructor(@config('') public configObj: object) {}
}
Expand All @@ -789,7 +789,7 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(store.configObj).to.eql({x: 1, y: 'a'});
});

it('injects config with configPath', () => {
it('injects config with propertyPath', () => {
class Store {
constructor(@config('x') public optionX: number) {}
}
Expand All @@ -800,7 +800,7 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(store.optionX).to.eql(1);
});

it('injects undefined option if configPath not found', () => {
it('injects undefined option if propertyPath not found', () => {
class Store {
constructor(@config('not-exist') public option: string | undefined) {}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/context/src/__tests__/unit/context-config.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Context binding configuration', () => {
expect(await ctx.getConfig('foo')).to.eql({x: 1});
});

it('gets config for a binding with configPath', async () => {
it('gets config for a binding with propertyPath', async () => {
ctx
.configure('foo')
.toDynamicValue(() => Promise.resolve({a: {x: 0, y: 0}}));
Expand Down Expand Up @@ -91,7 +91,7 @@ describe('Context binding configuration', () => {
expect(ctx.getConfigSync('foo')).to.eql({x: 1});
});

it('gets config for a binding with configPath', () => {
it('gets config for a binding with propertyPath', () => {
ctx.configure('foo').to({x: 1});
expect(ctx.getConfigSync('foo', 'x')).to.eql(1);
expect(ctx.getConfigSync('foo', 'y')).to.be.undefined();
Expand All @@ -109,7 +109,7 @@ describe('Context binding configuration', () => {
class MyConfigResolver implements ConfigurationResolver {
getConfigAsValueOrPromise<ConfigValueType>(
key: BindingAddress<unknown>,
configPath?: string,
propertyPath?: string,
resolutionOptions?: ResolutionOptions,
): ValueOrPromise<ConfigValueType | undefined> {
return (`Dummy config for ${key}` as unknown) as ConfigValueType;
Expand Down
16 changes: 8 additions & 8 deletions packages/context/src/binding-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ConfigurationResolver {
* Resolve config for the binding key
*
* @param key - Binding key
* @param configPath - Property path for the option. For example, `x.y`
* @param propertyPath - Property path for the option. For example, `x.y`
* requests for `<config>.x.y`. If not set, the `config` object will be
* returned.
* @param resolutionOptions - Options for the resolution.
Expand All @@ -30,7 +30,7 @@ export interface ConfigurationResolver {
*/
getConfigAsValueOrPromise<ConfigValueType>(
key: BindingAddress<unknown>,
configPath?: string,
propertyPath?: string,
resolutionOptions?: ResolutionOptions,
): ValueOrPromise<ConfigValueType | undefined>;
}
Expand All @@ -43,11 +43,11 @@ export class DefaultConfigurationResolver implements ConfigurationResolver {

getConfigAsValueOrPromise<ConfigValueType>(
key: BindingAddress<unknown>,
configPath?: string,
propertyPath?: string,
resolutionOptions?: ResolutionOptions,
): ValueOrPromise<ConfigValueType | undefined> {
configPath = configPath || '';
const configKey = configBindingKeyFor(key, configPath);
propertyPath = propertyPath || '';
const configKey = configBindingKeyFor(key, propertyPath);

const options: ResolutionOptions = Object.assign(
{optional: true},
Expand All @@ -60,14 +60,14 @@ export class DefaultConfigurationResolver implements ConfigurationResolver {
/**
* Create binding key for configuration of the binding
* @param key - Binding key for the target binding
* @param configPath - Property path for the configuration
* @param propertyPath - Property path for the configuration
*/
export function configBindingKeyFor<ConfigValueType = unknown>(
key: BindingAddress,
configPath?: string,
propertyPath?: string,
) {
return BindingKey.create<ConfigValueType>(
BindingKey.buildKeyForConfig<ConfigValueType>(key).toString(),
configPath,
propertyPath,
);
}
Loading