Giter VIP home page Giter VIP logo

envy's Introduction

envy Github Actions Coverage Status Software License crates.io Latest API docs

deserialize environment variables into typesafe structs

๐Ÿ“ฆ install

Run cargo add envy or add the following to your Cargo.toml file.

[dependencies]
envy = "0.4"

๐Ÿคธ usage

A typical envy usage looks like the following. Assuming your rust program looks something like this...

๐Ÿ’ก These examples use Serde's derive feature

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
  foo: u16,
  bar: bool,
  baz: String,
  boom: Option<u64>
}

fn main() {
    match envy::from_env::<Config>() {
       Ok(config) => println!("{:#?}", config),
       Err(error) => panic!("{:#?}", error)
    }
}

... export some environment variables

$ FOO=8080 BAR=true BAZ=hello yourapp

You should be able to access a completely typesafe config struct deserialized from env vars.

Envy assumes an env var exists for each struct field with a matching name in all uppercase letters. i.e. A struct field foo_bar would map to an env var named FOO_BAR.

Structs with Option type fields will successfully be deserialized when their associated env var is absent.

Envy also supports deserializing Vecs from comma separated env var values.

Because envy is built on top of serde, you can use all of serde's attributes to your advantage.

For instance let's say your app requires a field but would like a sensible default when one is not provided.

/// provides default value for zoom if ZOOM env var is not set
fn default_zoom() -> u16 {
  32
}

#[derive(Deserialize, Debug)]
struct Config {
  foo: u16,
  bar: bool,
  baz: String,
  boom: Option<u64>,
  #[serde(default="default_zoom")]
  zoom: u16
}

The following will yield an application configured with a zoom of 32

$ FOO=8080 BAR=true BAZ=hello yourapp

The following will yield an application configured with a zoom of 10

$ FOO=8080 BAR=true BAZ=hello ZOOM=10 yourapp

The common pattern for prefixing env var names for a specific app is supported using the envy::prefixed(prefix) interface. Asumming your env vars are prefixed with APP_ the above example may instead look like

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
  foo: u16,
  bar: bool,
  baz: String,
  boom: Option<u64>
}

fn main() {
    match envy::prefixed("APP_").from_env::<Config>() {
       Ok(config) => println!("{:#?}", config),
       Err(error) => panic!("{:#?}", error)
    }
}

the expectation would then be to export the same environment variables prefixed with APP_

$ APP_FOO=8080 APP_BAR=true APP_BAZ=hello yourapp

๐Ÿ‘ญ Consider this crate a cousin of envy-store, a crate for deserializing AWS parameter store values into typesafe structs and recap, a crate for deserializing named regex capture groups into typesafe structs.

Doug Tangren (softprops) 2016-2024

envy's People

Contributors

barskern avatar d-e-s-o avatar dependabot-preview[bot] avatar eliad-wiz avatar enrichman avatar j-brn avatar jayvdb avatar kimond avatar magnet avatar rhysd avatar rnestler avatar softprops avatar tobz1000 avatar walfie avatar zsiciarz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

envy's Issues

Update codecov

Lets move away from kcov and put our money on tarpaulin

Can't handle capitalised rename

๐Ÿ› Bug description

Given the following main.rs:

#[derive(serde::Deserialize, Clone, Debug, serde::Serialize)]
pub struct Person {
    #[serde(rename(deserialize = "surname"))]
    pub surname: String,
    #[serde(skip_serializing, rename(deserialize = "FIRST_NAME"))]
    pub first_name: String,
}


fn main() -> anyhow::Result<()> {

    println!("FIRST_NAME: {}", std::env::var("FIRST_NAME").unwrap());
    println!("surname: {}", std::env::var("surname").unwrap());

    let person = envy::from_env::<Person>().map_err(|e| {
        anyhow::anyhow!(e).context("Could not read Person from environment")
    })?;

    println!("Person: {:?}", person);

    Ok(())
}

it fails to read the FIRST_NAME environment variable:

$ surname=smith FIRST_NAME=john cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/envy_bug`
FIRST_NAME: john
surname: smith
Error: Could not Person from environment

Caused by:
    missing value for field FIRST_NAME

๐Ÿค” Expected Behavior

It should have been able to read the FIRST_NAME environment variable.

๐Ÿ‘Ÿ Steps to reproduce

Run the above code.

๐ŸŒ Your environment

I used the following Cargo.toml:

[package]
name = "envy_bug"
version = "0.1.0"
authors = ["hottomato4"]
edition = "2018"
publish = ["github"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

serde = { version = "1", features = [ "derive" ] }
envy = "0.4"
anyhow = "1"

and the following Cargo.lock

# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "anyhow"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"

[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
 "serde",
]

[[package]]
name = "envy_bug"
version = "0.1.0"
dependencies = [
 "anyhow",
 "envy",
 "serde",
]

[[package]]
name = "proc-macro2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
 "unicode-xid",
]

[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "syn"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-xid",
]

[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "anyhow"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"

[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
 "serde",
]

[[package]]
name = "envy_bug"
version = "0.1.0"
dependencies = [
 "anyhow",
 "envy",
 "serde",
]

[[package]]
name = "proc-macro2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
 "unicode-xid",
]

[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "syn"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-xid",
]

[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"

Also:

$ rustup show
Default host: x86_64-unknown-linux-gnu
rustup home:  /opt/rust

installed targets for active toolchain
--------------------------------------

x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.52.1 (9bc8c42bb 2021-05-09)
$ cargo --version
cargo 1.52.0 (69767412a 2021-04-21)

Unable to Deserialize Enum from String

๐Ÿ› Bug description

When trying to deserialize an untagged enum from a string, deserialization fails.

๐Ÿค” Expected Behavior

Having a field logging_format which is an enum should deserialize properly from the environment.

๐Ÿ‘Ÿ Steps to reproduce

use std::default::Default;

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct Config {
    #[serde(default)]
    pub logging_format: LoggingFormat,
}

#[derive(Debug, Deserialize)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum LoggingFormat {
    Json,
    Plain,
}

impl Default for LoggingFormat {
    fn default() -> Self {
        LoggingFormat::Json
    }
}

I'm deserializing this with:

let config: Config = envy::from_env().unwrap();

I'm executing my program with:

LOGGING_FORMAT=json cargo run

And I'm seeing the following output:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Custom("data did not match any variant of untagged enum LoggingFormat")', src/libcore/result.rs:997:5
stack backtrace:
   0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
             at src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:39
   1: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:70
   2: std::panicking::default_hook::{{closure}}
             at src/libstd/sys_common/backtrace.rs:58
             at src/libstd/panicking.rs:200
   3: std::panicking::default_hook
             at src/libstd/panicking.rs:215
   4: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:478
   5: std::panicking::continue_panic_fmt
             at src/libstd/panicking.rs:385
   6: rust_begin_unwind
             at src/libstd/panicking.rs:312
   7: core::panicking::panic_fmt
             at src/libcore/panicking.rs:85
   8: core::result::unwrap_failed
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858/src/libcore/macros.rs:16
   9: <core::result::Result<T, E>>::unwrap
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858/src/libcore/result.rs:798
  10: api::main
             at src/main.rs:9
  11: std::rt::lang_start::{{closure}}
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858/src/libstd/rt.rs:64
  12: std::panicking::try::do_call
             at src/libstd/rt.rs:49
             at src/libstd/panicking.rs:297
  13: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:92
  14: std::rt::lang_start_internal
             at src/libstd/panicking.rs:276
             at src/libstd/panic.rs:388
             at src/libstd/rt.rs:48
  15: std::rt::lang_start
             at /rustc/2aa4c46cfdd726e97360c2734835aa3515e8c858/src/libstd/rt.rs:64
  16: main

I have tried variants of the string value of the enum like JSON, json, Json, etc.

๐ŸŒ Your environment

  • envy version: 0.4.0
  • Host platform: x86_64-unknown-linux-gnu
  • Target platform: x86_64-unknown-linux-musl
  • Host OS: Ubuntu 18.04 Bionic
  • Rust: 1.33.0

Failing to read env field

According to #65 env files should work with combination from dotenvy, but they don't seem to for me.

env file:
.env

REQUIRED_SALEOR_VERSION=">=3.11.7<4"
SALEOR_APP_ID="dummy-saleor-app-rs"
APP_API_BASE_URL="http://localhost:8000/graphql/"
APL="redis"
APL_URL="redis://redis:6379/2"
LOG_LEVEL="DEBUG"

running:
config.rs

use crate::saleor::AplType;
use tracing::Level;

#[derive(Debug, Deserialize)]
#[serde(remote = "Level")]
pub enum LocalTracingLevel {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub struct Config {
    pub required_saleor_version: String,
    pub saleor_app_id: String,
    pub app_api_base_url: String,
    pub apl: AplType,
    pub apl_url: String,
    #[serde(with = "LocalTracingLevel")]
    pub log_level: tracing::Level,
}

impl Config {
    pub fn load() -> Result<Self, envy::Error> {
        dotenvy::dotenv().unwrap();

        for (key, value) in std::env::vars() {
            println!("{key}: {value}");
        }
        let env = envy::from_env::<Config>();
        dbg!(&env);
        env
    }
}

Error/stdout:

REQUIRED_SALEOR_VERSION: >=3.11.7<4
SALEOR_APP_ID: dummy-saleor-app-rs
APP_API_BASE_URL: http://localhost:8000/graphql/
APL: redis
APL_URL: redis://redis:6379/2
LOG_LEVEL: DEBUG
[src/config.rs:36:9] &env = Err(
    MissingValue(
        "REQUIRED_SALEOR_VERSION",
    ),
)
Error: missing value for field REQUIRED_SALEOR_VERSION

#[serde(rename = "")] not supported

๐Ÿ› Bug description

When using serde to rename a struct field, the code compiles, however the code will not find the renamed variable, nor the original name's one.

๐Ÿค” Expected Behavior

envy uses the new name in order to successfully find the environment variable

๐Ÿ‘Ÿ Steps to reproduce

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Config {
  #[serde(rename = "DATABASE_URL")]
  database_url: String
}

fn main() {
  let config = envy::from_env::<Config>().unwrap();
  dbg!(config);
}

๐ŸŒ Your environment

Linux

envy version: 0.4.2

should now be using stable rustfmt in ci

๐Ÿ› Bug description

Describe your issue in detail.

now that rustfmt is stable we no longer need to be using rustfmt-preview in ci

๐Ÿค” Expected Behavior

๐Ÿ‘Ÿ Steps to reproduce

๐ŸŒ Your environment

envy version:

Add "separator" to parse nested structs

๐Ÿ’ก Feature description

In config-rs, there's an option to define a separator which allow users to structure their env vars. I think an example will illustrate better:

#[derive(Deserialize)]
struct Config {
    database: Database
}

#[derive(Deserialize)]
struct Database {
    name: String,
}

If we define a separator, let's say "__", we could create this Config by setting DATABASE__NAME=foo.

Would it be possible and desirable for envy to include it?

Alternatives

It is possible to get the same behavior using the flatten attribute from serde (as noticed in #15). The drawback is that it demands a good amount of boilerplate because we would have to serde's rename every field in the nested struct if we wanted to use this separator idea.

Proposal: Merge efforts in some way

Definitely like what you have here.

@softprops I've been working on a very general configuration solution that has become a superset of this crate's functionality. I'm proposing using config internally in envy.

For an example, here is the example from the readme converted to use config.

#[macro_use]
extern crate serde_derive;
extern crate config;

#[derive(Deserialize, Debug)]
struct Config {
  foo: u16,
  bar: bool,
  baz: String,
  boom: Option<u64>
}

fn main() {
    let mut c = config::Config::new();

    // Not possible to error in this case but the `merge` function is very generic
    c.merge(config::Environment::new()).unwrap();

    match c.try_into::<Config>() {
       Ok(config) => println!("{:#?}", config),
       Err(error) => panic!("{:#?}", error)
    }
}

A couple things you'd gain from using config under-the-hood of envy:

  • Prefix ( would fix #12 ) with config::Environment::with_prefix("SOME_APP")
  • Better Errors, omitting a required key looks like (in the example):
    thread 'main' panicked at 'missing field `baz`', src/main.rs:21:21
    

Support newtype structs

๐Ÿ› Bug description

Envy ignores the newtype hint from Serde and fails to deserialize "newtype" structs.

๐Ÿค” Expected Behavior

Envy should deserialize newtype structs. It makes little sense in Envy's case to handle them as tuple struct with 1 item.

Nested struct

#[derive(Deserialize, Debug)]
struct A {
  u: i32,
  y: String,
}
#[derive(Deserialize, Debug)]
struct B {
 a: A,
}

Is possible to de-serialize above? I would expected to work this as A_U=5 A_Y="Test" or A=U=5,y=Test but it don't.

rename_all attribute does not work

๐Ÿ› Bug description

When I use #[serde(rename_all = "camelCase")] attribute, envy seems to unable to find target variable.
I tried another case such as SCREAMING_SNAKE_CASE but it also fails.

#[derive(Deserialize, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
struct Bar {
    hoge_hoge: String
}

#[test]
fn deserialize_with_rename_all() {
    let data = vec![(String::from("hogeHoge"), String::from("yey"))];
    
    // it returns "Err(MissingValue("hogeHoge"))"
    from_iter::<_, Bar>(data);
}

๐Ÿค” Expected Behavior

envy should find corresponding environmental variable.

๐Ÿ‘Ÿ Steps to reproduce

#[derive(Deserialize, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
struct Bar {
    hoge_hoge: String
}

#[test]
fn deserialize_with_rename_all() {
    let data = vec![(String::from("hogeHoge"), String::from("yey"))];
    
    // it returns "Err(MissingValue("hogeHoge"))"
    from_iter::<_, Bar>(data);
}

๐ŸŒ Your environment

macOS 10.15.6
rustc 1.47.0
envy version: 0.4.2

Fallback to .env file like dotenv in Node

First of all, thanks for the fantastic library!

๐Ÿ’ก Feature description

I think it would've been great to support dotenv-like behavior in envy.
That Node library can read environment variables from either environment variables directly (like envy does) or via .env file, which is super convenient during the local development, cause we can have .env.example with some default values for the development environment.

๐Ÿ’ป Basic example

As an example, I can provide my own pet project where I played around with envy to actually make it work as I want.

As you can see, in load_config we, first of all, try to load env variables as usual with envy and if it fails, we try to load and parse .env file into the environment variables and re-read them again. And only after that, the final fail happens. This way, I can run the project from CLion without any additional configuration and explicit default values in the code.

Of course, it could be re-implemented in a different way, if it was to become a part of envy.

Map deserialization syntax?

I can't find any documentation on how to pass map fields to be deserialized.

For example, given

use std::collections::HashMap;
use serde_derive::Deserialize;

#[derive(Debug, Default, Deserialize)]
struct Config {
    services: HashMap<String, String>
}

envy::from_env::<Config>()

How does one actually pass values to services?

I've tried SERVICES=a=test,b=test, which results in Err(Custom("invalid type: string \"a=test,b=test\", expected a map")) and SERVICES_A=test which causes Err(MissingValue("services"))

Is there a way to do this?

envy version: 0.4.2

Inconsistent results when running multiple unit tests that use envy

๐Ÿ› Bug description

I am testing constructing a struct from different sets of environment variables using serde and envy. Each test begins by clearing the environment variables with std::env::remove_var() and then setting new values with std::env::set_var(). After the values are set, I print the currently set values with std::env::vars(), which are displayed correctly. After that, I construct the struct with envy, print what values it has and finally assert_eq! that they're correct. Using just a single test to test a certain set of values, it works every time, but using multiple tests to test different sets of environment variables causes the tests to fail randomly. This is because the values envy sets into the struct are not the ones the test just set, but the ones the previous test did. Since the order the tests are ran with cargo test isn't determenistic, the tests fail randomly.

๐Ÿค” Expected Behavior

envy gets the proper environment variables and sets them accordingly in all tests.

๐Ÿ‘Ÿ Steps to reproduce

use serde::Deserialize;

const PREFIX: &str = "TEST_";

#[derive(Debug, Deserialize)]
struct Config {
    pub value_1: String,
    pub value_2: String,
}

impl Config {
    fn from_env() -> Config {
        envy::prefixed(PREFIX).from_env::<Config>().unwrap()
    }
}

fn dump_env_lines(prefix: &str) -> Vec<String> {
    std::env::vars()
        .filter_map(|(k, v)| {
            if k.starts_with(prefix) {
                Some(format!("{}={}", k, v))
            } else {
                None
            }
        })
        .collect::<Vec<String>>()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;

    #[test]
    fn test_1() {
        env::remove_var("TEST_VALUE_1");
        env::remove_var("TEST_VALUE_2");

        env::set_var("TEST_VALUE_1", "Hello");
        env::set_var("TEST_VALUE_2", "World");

        println!("{:?}", dump_env_lines(PREFIX));
        let config = Config::from_env();
        println!("{:?}", config);

        assert_eq!(config.value_1, "Hello");
        assert_eq!(config.value_2, "World");
    }

    #[test]
    fn test_2() {
        env::remove_var("TEST_VALUE_1");
        env::remove_var("TEST_VALUE_2");

        env::set_var("TEST_VALUE_1", "Test");
        env::set_var("TEST_VALUE_2", "Value");

        println!("{:?}", dump_env_lines(PREFIX));
        let config = Config::from_env();
        println!("{:?}", config);

        assert_eq!(config.value_1, "Test");
        assert_eq!(config.value_2, "Value");
    }
}

Example output of one failed test run:

---- tests::test_2 stdout ----
["TEST_VALUE_1=Test", "TEST_VALUE_2=Value"]
Config { value_1: "Hello", value_2: "World" }
thread 'tests::test_2' panicked at 'assertion failed: `(left == right)`
  left: `"Hello"`,
 right: `"Test"`', src/lib.rs:62:9

The output shows that the Config struct has the values Hello and World, which are the ones set in the previously ran test, instead of the values Test and Value the current test previously set.

๐ŸŒ Your environment

  • OS: Ubuntu 20.04
  • rustc 1.45.0 (5c1f21c3b 2020-07-13) (latest stable at the time of writing)
  • cargo 1.45.0 (744bd1fbb 2020-06-15)
  • envy version: 0.4.1
  • serde version: 1.0.114

Proposal: improve error descriptions (capitalize variables and respect prefix)

Hey @softprops,
I encountered the problem of error accuracy when working with envy.

This issue seems vaguely relevant, but it's missing any description, so I decided to create a new one.

The problem

Basically given code like this

struct Env {
    pub some_var: String
}

fn main() {
    let _env = envy::prefixed("ABC_")
        .from_env::<Env>().unwrap();
}

I'm getting an error like this:

MissingValue("some_var")

where I'd like to see an error like this:

MissingValue("ABC_SOME_VAR")

Quick solution

I was able to overcome this in my project by a quick hack:

panic!("Missing value: ABC_{}", err.to_string().split_whitespace().last().unwrap().to_uppercase());

but obviously it would be better if envy just had nicer errors in the first place.

Proposal

I would be happy to introduce a PR, improving the error messages and adjusting the behavior for prefixed variables. Let me know if you're interested in seeing this, is the proposed improvement acceptable, and I'll work on it.

add `fn to_vec` for easily printing config values in any simple format

๐Ÿ’ก Feature description

Personally I would like to be able to easily print out my config in a bash format.
Then I could copy one of the config's env vars from my app's output, change the env var and restart my binary.
Others may want to output their config's env vars in a YAML-like syntax and change them in their kubernetes config.

To provide those use cases with a simple and straight-forward solution, I propose to add the following function to envy:

pub fn to_vec<T: Serialize>(value: &T) -> Result<Vec<(String, String)>> { ... }

๐Ÿ’ป Basic example

use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Default)]
struct Config {
    foo: u16,
    bar: String,
}

for (key, value) in envy::to_vec(&Config::default())? {
    println!("{key}={value:?}");
}
// stdout:
// FOO="0"
// BAR="" 

Alternatives

Use std::fmt::Debug instead

This has the following downsides:

  • Fields aren't SCREAMING_SNAKE_CASE and it's not feasible to output a specific format with Debug.
  • If you have a password field or other secret, you can no longer use derive(Debug).
    With serde you could use #[serde(skip_serializing)].

Write a custom Serializer or fn to_custom_format for every format instead

I think for simple formats writing a Serializer is more complicated than a function using to_vec.

Add a fn to_iter instead

pub fn to_iter<T: Serialize>(value: &T) -> impl Iterator<Item = Result<(String, String)>> + '_ { ... }

I really like this solution, because it is very flexible.
At the least this alternative should be a candidate to add in the future as well.

Implementing this function without using a std::vec::IntoIter is a bit awkward because the Serialize trait does not seem like it's designed for this kind of step-wise serialization to an Iterator.

Printing an Iterator<Item = Result<(String, String)>> similar to the example above is a bit more complicated:

for result in envy::to_iter(&Config::default())? {
    let (key, value) = result?;
    println!("{key}={value:?}");
}

or when ignoring Err values:

for (key, value) in envy::to_iter(&Config::default()).filter_map(Result::ok) {
    println!("{key}={value:?}");
}

Not adding anything to envy

There's definitely an argument to keep envy as simple as possible.
I'm really curious, what other people think about this idea.
Maybe there are simpler solutions that I've overlooked.

Is envy unmaintained?

Just wanted to ask whether or not this crate is still being maintained or not.

If it isn't, that's fine - I'd simply like to know whether or not I should just fork the crate to fix the problems that I have. Please don't take this issue as pressure :)

Rename a field to a word consists of capitals only

๐Ÿ› Bug description

Parsing finishes with error Error::MissingValue if I rename a field of parsed structure to a word consists of capitals only.

๐Ÿค” Expected Behavior

Should be parsed without errors.

๐Ÿ‘Ÿ Steps to reproduce

Define structure for parsing

#[derive(Deserialize, Debug)]
pub struct DB {
    #[serde(rename="DB_USER")]
    pub user: String,
    #[serde(rename="DB_PASS")]
    pub pass: String,
    #[serde(rename="DB_NAME")]
    pub name: String,
    #[serde(default="host", rename="DB_HOST")]
    pub host: String,
    #[serde(default="port", rename="DB_PORT")]
    pub port: u16,
}

let db_config = envy::from_env::<settings::DB>().unwrap();

Make export DB_USER=postgres before running, run and get panic value: MissingValue("DB_USER")

๐ŸŒ Your environment

Archlinux

envy version:
0.4.0

So. I see this code in lib.rs

impl<Iter: Iterator<Item = (String, String)>> Iterator for Vars<Iter> {
    type Item = (VarName, Val);

    fn next(&mut self) -> Option<Self::Item> {
        self.0
            .next()
            .map(|(k, v)| (VarName(k.to_lowercase()), Val(k, v)))
    }
}

So I understand why the k is got lowercased. I get the expected behavior if I remove this conversion but it's not a solution because I lose feature of automatic comparison of a field of parsed structure and an environment variable. So my question is how to determine if a field has Serde tag rename? I can use the determining in order to prevent the conversion only for "renamed" fields.

Empty environment variable is deserialized to a non-empty vector

๐Ÿ› Bug description

When trying to deserialize an empty environment variable into a Vec<String>, envy creates a vector with an empty string instead of an empty vector.

๐Ÿค” Expected Behavior

envy should return an empty vector.

๐Ÿ‘Ÿ Steps to reproduce

Run this example with VALUES="":

use serde::Deserialize;

#[derive(Deserialize)]
struct Config {
    values: Vec<String>,
}

fn main() {
    match envy::from_env::<Config>() {
        Ok(config) => println!("provided values: {:?}", config.values),
        Err(err) => println!("error parsing config from env: {}", err),
    }
}
$ VALUES="" cargo run -q --example empty-vec
provided values: [""]

๐ŸŒ Your environment

envy version: v0.4.1-2-geb2e88c

Serde flatten

๐Ÿ› Bug description

Using the flatten attribute from serde almost works but breaks in the case of non string values in flattened structs. In this case config always parses size as a string. However, if I put the size attribute directly in Config then everything works.

๐Ÿค” Expected Behavior

The usize value in the flattened struct should parse

๐Ÿ‘Ÿ Steps to reproduce

#[derive(Deserialize)]
struct Config {
   #[serde(flatten)]
   pub subconfig: Subconfig
}

#[derive(Deserialize)]
struct Subconfig {
   pub size: usize
}

๐ŸŒ Your environment

nightly-x86_64-unknown-linux-gnu (default)
rustc 1.33.0-nightly (a8a2a887d 2018-12-16)

envy version:
latest

How to deserialize Option<T>

Sorry, if the answer is obvious, but I could not find an example in the documentation nor the tests for this crate.

What should the content of an environment variable be if I want to deserialize a Option<String>? A non-existent variable results in None, but how to get a Some(_)?

Configurable prefix

For example if my app is named "barstool" I'll probably be looking for BARSTOOL_BAZ, BARSTOOL_FOO, etc.

Does not work correctly with serde's deny_unknown_fields

๐Ÿ› Bug description

With a struct that uses serde's deny_unknown_fields container attribute using non-prefixed keys envy kind-of correctly fails with the error unknown field an_other_env_variable, expected one of ....

Arguably that pragma does not make sense for that case, but might be necessary for other (de)serializations.

๐Ÿค” Expected Behavior

It ignores the other environment variables.

๐Ÿ‘Ÿ Steps to reproduce

use serde::Deserialize;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {
    size: Option<u32>,
}

fn main() {
    match envy::from_env::<Config>() {
        Ok(config) => println!("provided config.size {:?}", config.size),
        Err(err) => println!("error parsing config from env: {}", err),
    }
}

envy version: 0.4.1

variable as Vec<PathBuf> where paths could contain commas?

The readme says that it supports parsing into Vec by separating the values by commas.
If I want to parse a variable as Vec<PathBuf> where each path could contain commas, would it work by putting that path in single quotes?
E.g.

FOLDERS='C:\Foo,Bar',D:\Baz

resulting in vec![r"C:\Foo,Bar", r"D:\Baz"]?

And if there is a space after the separating comma, it won't work, right?

Continue parsing for more missing required values

๐Ÿ’ก Feature description

Continue parsing for more missing required values (instead stopping at the first one).

๐Ÿ˜ Motivation

This can provide more information to the user/devops/developer in order to setup all missing values at once (instead of one by one).

๐Ÿ’ป Basic example

Today, this:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Config {
    host: String,
    port: String,
}

fn main() {
    match envy::prefixed("APP_").from_env::<Config>() {
        Ok(config) => println!("{:#?}", config),
        Err(err) => {
            eprintln!("error: {:#?}", err);
            std::process::exit(1);
        }
    }
}

running with:

cargo run

outputs this:

error: MissingValue(
    "host",
)

The desired behaviour would output this instead:

error: MissingValues(
    [
        "host", // Or even better: "APP_HOST", considering prefix
        "port" // Or even better: "APP_PORT", considering prefix
    ]
)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.