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): support binding config and @inject.config #2259

Open
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
3 participants
@raymondfeng
Copy link
Member

raymondfeng commented Jan 16, 2019

Reactivation of #983 based on requirements illustrated by #2249.

  1. Add context.configure to configure bindings
  2. Add context.getConfig to look up configuration for a binding
  3. Add @injection.config to receive injection of configuration

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

@raymondfeng raymondfeng requested a review from bajtos as a code owner Jan 16, 2019

@raymondfeng raymondfeng force-pushed the simpler-binding-options branch 4 times, most recently from e3aa58a to a2a9c1a Jan 16, 2019

@bajtos

This comment has been minimized.

Copy link
Member

bajtos commented Jan 18, 2019

@raymondfeng In #983, we had a long discussion about different aspects and user requirements for configuration system. How is this pull request different from #983? Can you also explain which comments you have addressed (and how), and which parts you decided to leave unaddressed?

@raymondfeng

This comment has been minimized.

Copy link
Member Author

raymondfeng commented Jan 18, 2019

I removed the magic to resolve config values following the namespace hierarchy. The implementation now only uses a naming convention to find the bound config for a given binding key.

@raymondfeng raymondfeng force-pushed the simpler-binding-options branch 2 times, most recently from c388192 to d9664c0 Jan 26, 2019

@dhmlau dhmlau added this to the February 2019 milestone milestone Jan 28, 2019

@bajtos

This comment has been minimized.

Copy link
Member

bajtos commented Jan 29, 2019

I removed the magic to resolve config values following the namespace hierarchy. The implementation now only uses a naming convention to find the bound config for a given binding key

Cool, that should make the review easier.

How do you imagine the bigger picture and the end-to-end usage of this new feature?

Few stories to consider:

  • As an LB4 app developer, I'd like to have a single JSON file to define all configuration entries. How are we going to map from a JSON format to the API proposed in this pull request? Maybe you have a different approach in mind?

  • As an extension developer, I may want my extension to provide default configuration options that can be later overridden by app-level config provided by the user.

  • As an extension developer, I may want to contribute configuration for artifacts bound by a different extension. A contrived example: @loopback/rest-explorer may want to override certain configuration of @loopback/rest.

  • As an extension developer, I want to read configuration of bindings bound by a different extension. For example, @loopback/rest-explorer needs to find out the configuration controlling /openapi.json URL.

I think it's important to start from the outside (work top-to-bottom) and think about the intended end-to-end user experience first.

It may be best to start with a spike that will allow you to iterate faster, make it easier for all of us to keep the discussion at high level & focused on the intended user experience and internal design.

@bajtos
Copy link
Member

bajtos left a comment

We really need to answer the question of how app users are going to provide the configuration before we go deeper into implementation details.

/**
* Environment for resolution, such as `dev`, `test`, `staging`, and `prod`
*/
environment?: string;

This comment has been minimized.

@bajtos

bajtos Feb 12, 2019

Member

I am not happy about introducing environment to LoopBack runtime. From what I know, this approach is considered as an anti-pattern. It couples internal implementation of our framework with the environment-based decision making. Also in many cases the same environment may need different configuration, e.g. the application can be deployed in production environment to different geographic zones and thus will need different database connection strings.

IMO, all environment-specific aspects should be handled in a single place, and that's the single place where we are loading the configuration - e.g. from env variables, JSON files, Consul service, whatever. The concept of per-environment config should stay inside that layer and should not leak to our IoC implementation.

This is another reason why I want to address the "configurability" feature from a high-level perspective of users first (see my comment above).

This comment has been minimized.

@raymondfeng

raymondfeng Feb 12, 2019

Author Member

I removed the env.

@bajtos bajtos requested review from jannyHou , b-admike and hacksparrow Feb 12, 2019

@raymondfeng raymondfeng force-pushed the simpler-binding-options branch 2 times, most recently from ca95224 to e4ed5f1 Feb 12, 2019

@raymondfeng

This comment has been minimized.

Copy link
Member Author

raymondfeng commented Feb 12, 2019

  • As an LB4 app developer, I'd like to have a single JSON file to define all configuration entries. How are we going to map from a JSON format to the API proposed in this pull request? Maybe you have a different approach in mind?
  • As an extension developer, I may want my extension to provide default configuration options that can be later overridden by app-level config provided by the user.
  • As an extension developer, I may want to contribute configuration for artifacts bound by a different extension. A contrived example: @loopback/rest-explorer may want to override certain configuration of @loopback/rest.
  • As an extension developer, I want to read configuration of bindings bound by a different extension. For example, @loopback/rest-explorer needs to find out the configuration controlling /openapi.json URL.

I agree with the use cases but they are out of the scope of this PR.

The purpose of this PR should be very simple (precondition: there are other tiers that load/populate the configuration into the context chain) based on requirements from #2249:

  1. Define the convention to configure a bound entry in the context (the configuration should be decoupled from the target, both of them can be bound independently). This allows us to leverage our IoC/DI container for configuration resolution.

  2. Define sugar APIs on Context to provide/consume configuration for a given binding (configure() and getConfig())

  3. Define a @inject.config decorator to inject corresponding configuration into a class without the need to know the binding key.

@raymondfeng raymondfeng force-pushed the simpler-binding-options branch from 6d0f1f5 to 94f7a3d Feb 12, 2019

@bajtos

This comment has been minimized.

Copy link
Member

bajtos commented Feb 15, 2019

I agree with the use cases but they are out of the scope of this PR.

The purpose of this PR should be very simple (precondition: there are other tiers that load/populate the configuration into the context chain) based on requirements from #2249:

  1. Define the convention to configure a bound entry in the context (the configuration should be decoupled from the target, both of them can be bound independently). This allows us to leverage our IoC/DI container for configuration resolution.

  2. Define sugar APIs on Context to provide/consume configuration for a given binding (configure() and getConfig())

  3. Define a @inject.config decorator to inject corresponding configuration into a class without the need to know the binding key.

Sure, I agree it's good to work in small increments and thus keep the scope of this pull request small. My concern is that you are defining foundational building blocks for the app/extension configuration, but we don't know if they will be able to support our requirements. Once this new feature is published as part of our public API, it will become difficult to make breaking changes.

I am not asking you to create a full implementation for the use cases I have described above, a high-level description of the indented solution is good enough.

For example:

As an LB4 app developer, I'd like to have a single JSON file to define all configuration entries. How are we going to map from a JSON format to the API proposed in this pull request?

We can define a convention where the binding key is used as the key in the JSON file too. E.g.

{
  "rest.port": 3000,
  "rest.host": "localhost",
}

Is it just me who see a similarity with Java property files? Maybe we should use nesting instead of dot-separated property names? Anyhow, that's just an implementation detail we can figure out later.

As an extension developer, I may want my extension to provide default configuration options that can be later overridden by app-level config provided by the user.

Can we rely on the order in which the configuration bindings are created?

class MyApp {
  constructor() {
    app.component(RestComponent); // defines the default configuration
   
    // custom configuration - overrides the config values bound by components
  }
}

Would this become a pain point and we will need to look for different ways how to allow extensions to provide default values?

As an extension developer, I may want to contribute configuration for artifacts bound by a different extension. A contrived example: @loopback/rest-explorer may want to override certain configuration of @loopback/rest.

And here it becomes tricky. If we rely on the order in which the configuration was defined, then we need to ensure that @loopback/rest component is mounted first, @loopback/rest-explorer second.

Thoughts?

_As an extension developer, I want to read configuration of bindings bound by a different extension. For example, @loopback/rest-explorer needs to find out the configuration controlling /openapi.json URL.)

I think this is a similar problem. If we rely on the order in which components are registered, then the default configuration provided by @loobpack/rest will be available at the time when @loobpack/rest-explorer is being mounted. However, this will not take into account configuration supplied by the application for @loopback/rest after @loopback/rest-explorer was mounted. I think that means we need to use a getter function instead of obtaining the configured value directly, similarly to how we are using @inject.getter for current authenticated user.

Now we have a question very relevant to your pull request: do we want to support both flavors of config injection (value vs getter)?

I am arguing that we should always inject a getter function to support configuration changes made after the config was resolved.

For example, in your greeter extension example shown in #2249, GreeterExtensionPoint is bound in SINGLETON scope. Once the binding is resolved and the singleton is created, there is no way how to update it when the configuration changes. The following code will not work:

app.configure('greeter-extension-point').to({color: false});
(await app.get('greeter-extension-point')).greet('en', 'monochrome');
app.configure('greeter-extension-point').to({color: true});
(await app.get('greeter-extension-point')).greet('en', 'rainbow');
// ^^ this does not pick up the new setting!!!

If we change @inject.config to always resolve to a getter function, we force all configuration consumers to support config updates from the start.

Let's discuss.

@bajtos
Copy link
Member

bajtos left a comment

Few comments on implementation details, I haven't reviewed the tests yet.

return resolveList(bindings, b => {
// We need to clone the session so that resolution of multiple bindings
// can be tracked in parallel
return b.getValue(ctx, ResolutionSession.fork(session));
});
}

function resolveByTag(

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

Wasn't this resolver already added by a previous pull request? I remember seeing this change in other places. Or is it just the way how git is computing the diff?

@@ -331,21 +395,28 @@ export function describeInjectedArguments(
return meta || [];
}

function resolveByTag(
function resolveBindings(

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

Similarly here, I don't see how is this change relevant to binding configuration. Can you revert it please?

@@ -45,7 +46,7 @@ export interface InjectionMetadata {
*/
decorator?: string;
/**
* Control if the dependency is optional, default to false
* Control if the dependency is optional. Default to `false`.

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

I find both the old and the new versions difficult to parse. How about the following?

Suggested change Beta
* Control if the dependency is optional. Default to `false`.
* Control if the dependency is optional. Default value: `false`.
* @param key The key for the binding to be configured
*/
configure<ConfigValueType = BoundValue>(
key: BindingAddress<unknown> = '',

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

IIRC, BindingAddress has the template argument set to unknown by default, thus we should be able to simplify this line as follows:

Suggested change Beta
key: BindingAddress<unknown> = '',
key: BindingAddress = '',

Same comment applies to other places below that are using BindingAddress<unknown> too.

);
}

private getConfigResolver() {

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

I find it ugly to have both configResolver as a property and getConfigResolver as a getter function. Have you considered initializing this.configResolver right in the constructor?

configPath,
);

const options: ResolutionOptions = Object.assign(

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

How about the following?

const options: ResolutionOptions = {optional: true,  ...resolutionOptions};
`config: RestServerConfig`
- Given `RestServer` ctor argument is decorated with `@inject.config()`
- When I bind a configuration object `{port: 3000}` to
`$config.test:servers.rest.server1`

This comment has been minimized.

@bajtos

bajtos Feb 15, 2019

Member

Please update the binding keys in this markdown file to follow the format used by the real implementation. Do we actually need this markdown file at all?

@raymondfeng raymondfeng force-pushed the simpler-binding-options branch from 94f7a3d to 604b377 Feb 16, 2019

feat(context): support binding config and @inject.config
1. Add context.configure to configure bindings
2. Add context.getConfig to look up configuration for a binding
3. Add @injection.config to receive injection of configuration

@raymondfeng raymondfeng force-pushed the simpler-binding-options branch from 604b377 to 947b930 Feb 20, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment