confique icon indicating copy to clipboard operation
confique copied to clipboard

Think about interaction with clap to allow loading/overriding config values from CLI args

Open LukasKalbertodt opened this issue 1 year ago • 4 comments

Often it's useful to allow users to override some (or all) configuration values via command line. Confique should work nicely for that use case. The currently best way to do it is probably to convert the CLI values to a partial type (manually) and then add it via Builder::preloaded. I would like to investigate whether this can be made more convenient and with less duplicate code.

If this improvement has to be CLI-library specific, I am pretty sure I only want to support clap. I only ever use clap with the derive feature and I think it's the most mature library.

Just to throw some random ideas into this issue, maybe one can annotate config fields with #[config(clap(...))] and if any are annotated this way, we will generate an additional type containing the fields annotated that way that has the derive(clap::*) on it. And has a method to convert it to a partial config type. This extra type can then be flattened into the main clap type. But again, haven't thought about this too deeply yet.

LukasKalbertodt avatar Oct 23 '22 07:10 LukasKalbertodt

For the record, I also use Gumdrop in my actix-web projects, where I don't need the path arguments to support accepting non-UTF8-able POSIX paths (i.e. where they're just going to be a path to a server root folder or config file) and I want to benefit from the reduced compile times and smaller output binaries.

ssokolow avatar Oct 23 '22 07:10 ssokolow

Fair point. It's certainly worth considering how one could support multiple cli libraries. Or even be completely library-agnostic.

LukasKalbertodt avatar Oct 23 '22 07:10 LukasKalbertodt

I would also love to have an integration with clap. I followed your workaround described above and I find it quite ok. However I was not able to derive Config directly on my clap cli flag struct, because of defaults. I totally agree with your view, that the finally parsed config should not contain options any longer, but this lead me to have options in my clap struct to see if the user provided the flag. I expect the flag to take precedence over all other config options (since it is the most direct user input). So I had another struct with options removed and config derived on it, which I could then construct from the partials. But this misses out on the documentation benefits from clap.

I didn't see a better way of doing it for now, but I still find your crate awesome and clean and can live with the solution. Just wanted to raise the point of defaults in clap and confique.

hmuendel avatar Feb 06 '23 11:02 hmuendel

A different kind of approach to support overriding via the command line is to have a general flag that accepts a fragment of config and applies it, this is the approach taken by cargo --config and jj --config-toml. This seems relatively simple to do now with confique, and is maybe just worth adding an example of, I used something like:

#[derive(confique::Config, Debug)]
#[config(partial_attr(derive(Clone, Debug)))]
#[config(partial_attr(serde(deny_unknown_fields)))]
struct Config {
    ...
}

type Partial = <Config as confique::Config>::Partial;

impl Config {
    fn load(fragments: Vec<Partial>) -> Result<Self> {
        let dirs = ProjectDirs::from(...).ok_or_eyre("cannot get config directory")?;
        let mut builder = Config::builder();
        // reverse so that later fragments take precedence
        for fragment in fragments.into_iter().rev() {
            builder = builder.preloaded(fragment)
        }
        Ok(builder.env().file(dirs.config_dir().join("config.toml")).load()?)
    }
}

#[derive(Debug, Parser)]
struct App {
    /// Config overrides to apply, these should be fragments of the config file.
    #[arg(long = "config-toml", value_name = "TOML", value_parser = toml::from_str::<Partial>)]
    config_fragments: Vec<config::Partial>,

    ...
}

fn main() -> Result<()> {
    let app = App::parse();
    let config = Config::load(app.config_fragments)?;
    tracing::trace!(?config, "loaded config");

    ...
}

Nemo157 avatar Jul 19 '24 08:07 Nemo157