Skip to content

Commit

Permalink
Merge eddad57 into 27a7e15
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jul 25, 2019
2 parents 27a7e15 + eddad57 commit bfe6293
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 55 deletions.
25 changes: 22 additions & 3 deletions docs/site/Context.md
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
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
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
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
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,
);
}

0 comments on commit bfe6293

Please sign in to comment.