Search code examples
rustactix-web

How do I pass the std::env::vars() object into a function


What I'm trying to do: pass env vars to a function:

I have a function which takes the std::env::vars() object and returns a database configuration object, like so:

pub fn config(mut vars: Vars) -> Config
{
    let mut result: Config = Config::new();
    result.dbname = vars.find(|kv| kv.0.eq("DB_NAME"))
        .map(|kv| kv.1);
    result.host = vars.find(|kv| kv.0.eq("DB_HOST"))
        .map(|kv| kv.1);
    println!("database host: {:?}", result.host);
    result.password = vars.find(|kv| kv.0.eq("DB_PASSWORD"))
        .map(|kv| kv.1);
    println!("database password: {:?}", result.password);
    println!("env password: {:?}"
             , vars.find(|kv| kv.0.eq("DB_PASSWORD"))
             .map(|kv| kv.1));
    result.port = vars.find(|kv| kv.0.eq("DB_PORT"))
        .map(|kv| kv.1)
        .map(|string| string.parse::<u16>().ok())
        .flatten();
    println!("database port: {:?}", result.port);
    result.user = vars.find(|kv| kv.0.eq("DB_USER"))
        .map(|kv| kv.1);
    result.manager =
       Some(ManagerConfig { recycling_method: RecyclingMethod::Fast });
    result
}

The problem: the Vars object is empty when passed

All of the above print statements show None for the given variables.

However, I know those variables are being set because when I print from the same object in main, the variables all show up:


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    println!("vars:");
    for (key, value) in std::env::vars() {
        println!("    {key}: {value}"); //works as expected
    }
    println!("env password: {:?}"
             , env::vars().find(|kv| kv.0.eq("DB_PASSWORD"))
             .map(|kv| kv.1));
    let port = environment::port(env::var("PORT"));
    HttpServer::new(|| {
        App::new()
            .app_data(
                Data::new(environment::config(std::env::vars()) //does not work as expected
                    .create_pool(None, tokio_postgres::NoTls)
                    .expect("Error initializing connection pool"))
            )
            .route("/", get().to(index_controller::index))
            .route("/username-available", post()
                   .to(user_controller::username_available2))
            .wrap(session::middleware())
    })
        .bind(("0.0.0.0", port))? 
        .run()
        .await
}

The question: why can I print std::env::vars() from main but not from a function?

The output of the program is, puzzlingly, as follows:

huntergatherer3-web-1       | vars:
huntergatherer3-web-1       |     PATH: /usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
huntergatherer3-web-1       |     HOSTNAME: 22f204119a79
huntergatherer3-web-1       |     DB_HOST: database
huntergatherer3-web-1       |     DB_NAME: huntergatherer
huntergatherer3-web-1       |     DB_PASSWORD: postgres_password
huntergatherer3-web-1       |     DB_PORT: 5432
huntergatherer3-web-1       |     DB_USER: postgres_user
huntergatherer3-web-1       |     PORT: 8082
huntergatherer3-web-1       |     RUST_LOG: debug
huntergatherer3-web-1       |     RUSTUP_HOME: /usr/local/rustup
huntergatherer3-web-1       |     CARGO_HOME: /usr/local/cargo
huntergatherer3-web-1       |     RUST_VERSION: 1.71.0
huntergatherer3-web-1       |     HOME: /root
huntergatherer3-web-1       | env password: Some("postgres_password")
huntergatherer3-web-1       | Got port 8082.
huntergatherer3-web-1       | database host: None
huntergatherer3-web-1       | database password: None
huntergatherer3-web-1       | env password: None
huntergatherer3-web-1       | database port: None
huntergatherer3-web-1       | database host: None
huntergatherer3-web-1       | database password: None
huntergatherer3-web-1       | env password: None
huntergatherer3-web-1       | database port: None

Solution

  • Vars implements Iterator. Advancing an iterator is a mutating operation; it consumes the item at the front of the iterator. In your function, you use .find() repeatedly, which advances the iterator until the item is found, discarding the items that it skips.

    In other words, unless Vars produces the environment variables in exactly the same order your code searches for them you will skip over variables you want later and they will no longer be available.

    If you want to take a Vars as an argument, you could simply iterate over vars and assign the results based on the current key, something like this:

    pub fn config(vars: Vars) -> Config {
        let mut result = Config::new();
    
        for (key, value) in vars {
            match key.as_str() {
                "DB_NAME" => result.dbname = Some(value),
                "DB_HOST" => result.host = Some(value),
                // and so on...
    
                // Catch-all arm to discard irrelevant variables.
                _ => {}
            }
        }
    
        result
    }
    

    There are other approaches as well, such as collecting Vars into a HashMap<String, String> or using std::env::var() to fetch each variable one at a time.