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

[cli_config] First version #45

Merged
merged 10 commits into from
Mar 31, 2023
Merged

[cli_config] First version #45

merged 10 commits into from
Mar 31, 2023

Conversation

dcharkes
Copy link
Contributor

@dcharkes dcharkes commented Mar 24, 2023

This is an initial version for package:cli_config.

The only class in the API is the Config class.

/// A hierarchical configuration.
///
/// Configuration can be provided from three sources: commandline arguments,
/// environment variables and configuration files. This configuration makes
/// these accessible via a uniform API.
///
/// Configuration can be provided via the three sources as follows:
/// 1. commandline argument defines as `-Dsome_key=some_value`,
/// 2. environment variables as `SOME_KEY=some_value`, and
/// 3. config files as JSON or YAML as `{'some_key': 'some_value'}`.
///
/// The default lookup behavior is that commandline argument defines take
/// precedence over environment variables, which take precedence over the
/// configuration file.
///
/// If a single value is requested from this configuration, the first source
/// that can provide the value will provide it. For example
/// `config.string('some_key')` with `{'some_key': 'file_value'}` in the
/// config file and `-Dsome_key=cli_value` as commandline argument returns
/// `'cli_value'`. The implication is that you can not remove keys from the
/// configuration file, only overwrite or append them.
///
/// If a list value is requested from this configuration, the values provided
/// by the various sources can be combined or not. For example
/// `config.stringList('some_key', combineAllConfigs: true)` returns
/// `['cli_value', 'file_value']`.
///
/// The config is hierarchical in nature, using `.` as the hierarchy separator
/// for lookup and commandline defines. The hierarchy should be materialized in
/// the JSON or YAML configuration file. For environment variables `__` is used
/// as hierarchy separator.
///
/// Hierarchical configuration can be provided via the three sources as follows:
/// 1. commandline argument defines as `-Dsome_key.some_nested_key=some_value`,
/// 2. environment variables as `SOME_KEY__SOME_NESTED_KEY=some_value`, and
/// 3. config files as JSON or YAML as
///    ```yaml
///    some_key:
///      some_nested_key:
///        some_value
///    ```
///
/// The config is opinionated on the format of the keys in the sources.
/// * Command-line argument keys should be lower-cased alphanumeric
///   characters or underscores, with `.` for hierarchy.
/// * Environment variables keys should be upper-cased alphanumeric
///    characters or underscores, with `__` for hierarchy.
/// * Config files keys should be lower-cased alphanumeric
///   characters or underscores.
///
/// In the API they are made available lower-cased and with underscores, and
/// `.` as hierarchy separator.
class Config { }

This PR is to solicit feedback on multiple levels.

Our format opinions for end users.

What do we accept as valid hierarchical identifers in YAML/JSON/CLI-defines/environment variables?

We prefer underscores in identifiers. (pubspec.yaml and jnigen.yaml do underscores, however ffigen.yaml dashes.) Should we consider something else?

What about hierarchy in environment variables where we cannot use .. Do we use __?

What syntax do we prefer for CLI defines? -Dkey.nested=value (typical define style)? Or --set key.nested=value (Helm style)? Or --key.nested=value (typical CLI style).

Programmable API for package authors

class Config {
  /// Lookup a list of paths in this config.
  ///
  /// If [combineAllConfigs] combines results from cli, environment, and
  /// config file. Otherwise, precedence rules apply.
  ///
  /// If provided, [splitCliPattern] splits cli defines.
  ///
  /// If provided, [splitEnvironmentPattern] splits environment values.
  ///
  /// If [resolveFileUri], resolves the paths in config file relative to the
  /// config file.
  List<Uri>? optionalPathList(
    String key, {
    core.bool combineAllConfigs = true,
    String? splitCliPattern,
    String? splitEnvironmentPattern,
    core.bool resolveFileUri = true,
  });

  // ...
}

A single class with a bunch of getters. The getters define how to treat the values in the various "provider"s. For example whether to split the strings on a character (e.g. splitting PATH on :). The getters also define on whether to combine the values from the various providers or not.

The error flow for package authors

Currently, both creating a Config and failures on config values can throw FormatExceptions. A YAML file could have the wrong format, but also a required config key could be missing which also makes the config the wrong format.

Should we consider having a dedicated type of exception instead?

Implementation considerations.

Please do not yet review those, let's get the other concerns right first.

@coveralls
Copy link

coveralls commented Mar 24, 2023

Pull Request Test Coverage Report for Build 4542279393

  • 235 of 235 (100.0%) changed or added relevant lines in 8 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 100.0%

Totals Coverage Status
Change from base Build 4505384911: 0.0%
Covered Lines: 235
Relevant Lines: 235

💛 - Coveralls

@dcharkes
Copy link
Contributor Author

@lrhn your thoughts on our APIs are always appreciated!
@jakemac53 @natebosch @devoncarew Any other packages besides JNIgen and FFIgen that we should take a look at while deciding our opinion on the config formats? Do we have any other standards going on in our ecosystem?
cc @mkustermann

pkgs/cli_config/lib/src/cli_parser.dart Outdated Show resolved Hide resolved
CliProvider(this._cli);

@override
String? getOptionalString(String key) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, config.string('key') vs config.readString('key') or config.loadString('key'). I guess we don't care about the verb so much.

Unfortunately, config.bool('key') doesn't work, because bool is a keyword.

config.stringValue('key') seems more bloated than using a verb.

get is the shortest verb.

Copy link
Member

@HosseinYousefi HosseinYousefi Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like get because this looks essentially like a map.

An alternative solution is to create a CliResult object that operator [] returns. Then config.getOptionalString('key') becomes config['key'].as<String>(), .as<bool>() and ...

.as<T>() for an unsupported T throws a FormatException.

Edit: Oops, as is also a keyword! maybe .value<String>()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API here is similar to shared_preferences. And there we have getString and getStringList as well.

Copy link
Contributor

@jakemac53 jakemac53 Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use read, or consider dropping the entire verb and using boolean instead of bool? read is only one more character.

I don't ultimately feel super strongly for this particular case, but I do care that Dart owned/maintained packages follow effective Dart guidelines, so we don't set an example that is contrary to effective Dart. Ultimately people will imitate the types of apis we put out there, and use them as justification for their own, unaware of any deeper reasoning. In fact shared_preferences is a perfect example of this, in this exact thread...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/flutter/packages/blame/dd8b54101c056190d15e5a1f6ad2b491657f23c2/packages/shared_preferences/lib/shared_preferences.dart I don't seen any code review for our shared_preferences initial PR that introduced this API.

Since read is a verb that communicates not that much here (also, technically speaking, part or the reading has happened already when reading in the various sources), it is a good argument for not having the verb. And being in the case that it should be a getter even though it has is a method with arguments.

The issue here is that the no-verb solution conflicts with keywords. And choosing boolean, integer, and doublee, would create confusion because they are spelled the same as the type they return.

@munificent touched this part of the style guide last, any neat ideas of what to do with conflicts with keywords?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd drop the verb. Changing get to read, which still has no semantic meaning different from "get", "fetch", "lookup" or "find", is not an improvement.

Don't use words that say nothing. Document and name getter-like methods like get getters (noun phrases).
That's what this is.

I'd use string, stringValue, stringValueOf or (more dubious) stringOf.

For bool and int, where we may don't want to get naming conflicts (we can, they are not reserved words, they just shadow the core types), we can use boolean and integer, or boolValue/intValue.

Or just use int and bool and deal with the conflicts:

import "dart:core";
import "dart:core" as core;

....

   core.bool bool(String key) { ... }

(You need to use core.bool everywhere in the class, highly annoying.)

pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/provider.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/environment_provider.dart Outdated Show resolved Hide resolved
Copy link
Contributor Author

@dcharkes dcharkes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jakemac53 and @natebosch !

pkgs/cli_config/lib/src/provider.dart Outdated Show resolved Hide resolved
CliProvider(this._cli);

@override
String? getOptionalString(String key) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, config.string('key') vs config.readString('key') or config.loadString('key'). I guess we don't care about the verb so much.

Unfortunately, config.bool('key') doesn't work, because bool is a keyword.

config.stringValue('key') seems more bloated than using a verb.

get is the shortest verb.

pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Show resolved Hide resolved
@dcharkes dcharkes marked this pull request as ready for review March 27, 2023 11:41
Copy link
Member

@HosseinYousefi HosseinYousefi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks @dcharkes!

Copy link
Member

@lrhn lrhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial comments, didn't get through everything (or much of anything).

pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved

static const boolStrings = {
'0': false,
'1': true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend not accepting 0/1.

I'd accept "yes"/"no" before that, but instead of pre-determining which strings you want to accept, I'd parameterize the function below ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 1 and 0 come from environment variable practises. If we don't want to accept that by default from the other two sources (file/commandline-defines) maybe we should have a map for both env vars and cl defines. (The file source must provide a boolean.)

I like the idea of parameterizing, but I would want to provide a default for people.

CliProvider(this._cli);

@override
String? getOptionalString(String key) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd drop the verb. Changing get to read, which still has no semantic meaning different from "get", "fetch", "lookup" or "find", is not an improvement.

Don't use words that say nothing. Document and name getter-like methods like get getters (noun phrases).
That's what this is.

I'd use string, stringValue, stringValueOf or (more dubious) stringOf.

For bool and int, where we may don't want to get naming conflicts (we can, they are not reserved words, they just shadow the core types), we can use boolean and integer, or boolValue/intValue.

Or just use int and bool and deal with the conflicts:

import "dart:core";
import "dart:core" as core;

....

   core.bool bool(String key) { ... }

(You need to use core.bool everywhere in the class, highly annoying.)

Copy link
Contributor Author

@dcharkes dcharkes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone with Lasse's approach and just went for bool and made the rest in the file core.bool.

pkgs/cli_config/lib/src/config.dart Outdated Show resolved Hide resolved
@dcharkes dcharkes requested a review from lrhn March 30, 2023 07:12
@dcharkes
Copy link
Contributor Author

It's been quiet here. If there's anymore feedback, please let me know. Otherwise I'll go ahead and merge this.

@dcharkes dcharkes merged commit 5994406 into main Mar 31, 2023
@dcharkes dcharkes deleted the cli-config-dev branch March 31, 2023 05:20
mosuem pushed a commit that referenced this pull request Aug 13, 2024
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.0 to 4.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/checkout/releases">actions/checkout's releases</a>.</em></p>
<blockquote>
<h2>v4.1.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update CODEOWNERS to Launch team by <a href="https://github.com/joshmgross"><code>@�joshmgross</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1510">actions/checkout#1510</a></li>
<li>Correct link to GitHub Docs by <a href="https://github.com/peterbe"><code>@�peterbe</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1511">actions/checkout#1511</a></li>
<li>Link to release page from what's new section by <a href="https://github.com/cory-miller"><code>@�cory-miller</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1514">actions/checkout#1514</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/joshmgross"><code>@�joshmgross</code></a> made their first contribution in <a href="https://redirect.github.com/actions/checkout/pull/1510">actions/checkout#1510</a></li>
<li><a href="https://github.com/peterbe"><code>@�peterbe</code></a> made their first contribution in <a href="https://redirect.github.com/actions/checkout/pull/1511">actions/checkout#1511</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/checkout/compare/v4.1.0...v4.1.1">https://github.com/actions/checkout/compare/v4.1.0...v4.1.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/checkout/commit/b4ffde65f46336ab88eb53be808477a3936bae11"><code>b4ffde6</code></a> Link to release page from what's new section (<a href="https://redirect.github.com/actions/checkout/issues/1514">#1514</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/8530928916aaef40f59e6f221989ccb31f5759e7"><code>8530928</code></a> Correct link to GitHub Docs (<a href="https://redirect.github.com/actions/checkout/issues/1511">#1511</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/7cdaf2fbc075e6f3b9ca94cfd6cec5adc8a75622"><code>7cdaf2f</code></a> Update CODEOWNERS to Launch team (<a href="https://redirect.github.com/actions/checkout/issues/1510">#1510</a>)</li>
<li>See full diff in <a href="https://github.com/actions/checkout/compare/8ade135a41bc03ea155e62e844d188df1ea18608...b4ffde65f46336ab88eb53be808477a3936bae11">compare view</a></li>
</ul>
</details>
<br />

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4.1.0&new-version=4.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants