mehcode / config-rs

⚙️ Layered configuration system for Rust applications (with strong support for 12-factor applications).
Apache License 2.0
2.43k stars 206 forks source link

Cannot get config-rs to read env variables #391

Open brsnik opened 1 year ago

brsnik commented 1 year ago

Getting the following error when trying to get an environmental variable from config-rs:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: configuration property "DEV_SERVER_ADDRESS" not found', src/app/config.rs:14:27 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


use config::Config;
use lazy_static;

lazy_static! {
    #[derive(Debug)]
    pub static ref CONFIG: Config = Config::builder()
        .add_source(config::Environment::with_prefix("APP_NAME").separator("_"))
        .add_source(config::File::with_name("AppName"))
        .build()
        .unwrap();
}

pub fn get<'a, T: serde::Deserialize<'a>>(key: &str) -> T {
    CONFIG.get::<T>(&key).unwrap()
}

ENV variable:

export APP_NAME_DEV_SERVER_ADDRESS="testtt dev addr"

Trying to get the variable in main:

#[actix_web::main]
async fn main() -> result::Result<(), io::Error> {

    let dev_server_address: String = app::config::get("DEV_SERVER_ADDRESS");
    println!("DEV_SERVER_ADDRESS: {}", dev_server_address);
...

What am I doing wrong?

matthiasbeyer commented 1 year ago

Hi!

Thanks for reporting this! Please have a look at the code in #392 - maybe I am missing something, because that example works for me (assuming you use config-rs from master).

The only difference I spot is that you're using the actix_web::main macro for your main function, but I doubt that this makes a difference...

ianling commented 1 year ago

FWIW, I also cannot get this working with 0.13.2 and nested structs in my config.

When I use .add_source(config::Environment::with_prefix("TEST")) then I can successfully use environment variables like TEST_MYSTRUCT.MYFIELD

However, when I use .add_source(config::Environment::with_prefix("TEST").separator("_")), no environment variables seem to work at all:

Something about separator(...) does not seem to behave as I would expect

brsnik commented 1 year ago

Hi!

Thanks for reporting this! Please have a look at the code in #392 - maybe I am missing something, because that example works for me (assuming you use config-rs from master).

The only difference I spot is that you're using the actix_web::main macro for your main function, but I doubt that this makes a difference...

Your example is exactly what I have, but it does not work for me. As for actix_web::main I am trying to load up env variables and then start an Actix web server in main.

matthiasbeyer commented 1 year ago

Any chance you can share the original code? Maybe a link to a git repository? So I can investigate a bit better?

brsnik commented 1 year ago

@matthiasbeyer Sure thing, sent an invitation for: https://github.com/brsnik/actix-config-rs

brsnik commented 1 year ago

Okay so, it works in lowercase app::config::get("dev_server_address") and without the separator separator("_") as @ianling suggested.

matthiasbeyer commented 1 year ago

meh.

All this is really inconvenient. I hope I can make this better in the future with the rewrite I am planning.

ianling commented 1 year ago

Here is the problem:

https://github.com/mehcode/config-rs/blob/master/src/env.rs#L222

            // If separator is given replace with `.`
            if !separator.is_empty() {
                key = key.replace(separator, ".");
            }

If I have a struct in my config with a snake-case field (e.g. person.first_name) and I try to set it using the environment variable PERSON_FIRST_NAME, this block transforms the key into person.first.name instead of person.first_name

The (bad) solution that immediately springs to mind is to only replace the first occurrence of the separator with a dot using replacen(..., 1) instead of replace(...), but then that leaves structs whose names contain snake-case broken, as well as deeply nested structs:

Hard saying what a good solution would be for this, as I can't think of a way for this crate to know for sure if the environment variable PERSON_NAME_FIRST_NAME is supposed to mean person.name.first_name or person_name.first_name or even just person_name_first_name....

brsnik commented 1 year ago

I honestly fail to see the need for the separator whatsoever. Keys like person.first.name and person.first_name are incredibly confusing. Should be snake case all the way.

ianling commented 1 year ago

This crate allows you to parse a config into an actual struct using .try_deserialize(), rather than retrieving keys using strings:

https://github.com/mehcode/config-rs/blob/c8ee9f1335fb093cda3debabe15ad0f7a3ab9071/examples/env-list/main.rs

My use case is ultimately setting values in my config struct using environment variables, but this gets messy in the cases I mentioned above due to some ambiguous combinations of dots and underscores.

jalcine commented 1 year ago

This is something I'm running into now myself and I'm having to resort to using #[serde(alias = "oneWord")] to allow fields like session_secret_key to be seen as sessionkey. Pretty undesirable, but it works.

In this vein, though probably not possible, if we could get config to know what kind of env to fill a field with in maybe some sort of more custom deserialization, that'd could allow people to specify the full env string a field could match to.

(Originally published at: https://jacky.wtf/2022/11/Uqih)

ianling commented 1 year ago

In this vein, though probably not possible, if we could get config to know what kind of env to fill a field with in maybe some sort of more custom deserialization, that'd could allow people to specify the full env string a field could match to.

That is how the popular Go module envconfig works: https://github.com/kelseyhightower/envconfig#struct-tag-support

That said, Go's syntax works a little better for this, in my opinion, since it's all on one line, vs having an additional #[serde(...)] line above each field in Rust.

nahuakang commented 1 year ago

What if for person.first_name you use a a dunderscore __ as the separator? Then you could write PERSON__FIRST_NAME and the replace would change it to PERSON.FIRST_NAME?

ivan-mudrak commented 1 year ago

It will work as expected if used like this:

 .add_source(Environment::with_prefix("app").prefix_separator("__").separator("__"))

then APP__BRAINTREE__MERCHANT_ID will be correctly parsed to Braintree.merchant_id with _snakecase setting name. Probably an example could be added to cover this case.

jnicholls commented 10 months ago

@nahuakang Great idea. A little weird at first, but it is a perfectly good solution to the problem @ivan-mudrak and @ianling mention above with snake_case struct field names. I'll take that over the serde field name aliasing approach.

haydenflinner commented 10 months ago

It would be good to surface this in the docs, leaving a comment here so I remember to come back later and do this.

I just spent half an hour running a debugger getting to the bottom of this, only to realize that fundamentally _ as the separator while having _ in the variable names will never work, based on the approach in the code. I assumed that it was doing something like 'for each variable in the config schema, check for an env-var', which would be predictable and scale with the size of the schema. But instead it's doing "check each envvar and if it matches the rules, try to insert it to the global config map", without any schema-enriched info, which doesn't work because _s get translated to .s. The approach taken also doesn't seem to surface a warning anywhere when a close-but-not-quite envvar was set but is having no impact.

It may also be good to default the separator to __, since _ will be so common in struct field names.