Skip to content

Commit ed402c4

Browse files
Merge pull request #39 from LukasKalbertodt/empty-string-env-is-unset-sometimes
Treat deserialization errors from empty env vars as unset env var
2 parents ce3b266 + 06db476 commit ed402c4

File tree

3 files changed

+58
-5
lines changed

3 files changed

+58
-5
lines changed

src/internal.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,18 @@ pub fn from_env_with_parser<T, E: std::error::Error + Send + Sync + 'static>(
6666
parse: fn(&str) -> Result<T, E>,
6767
) -> Result<Option<T>, Error> {
6868
let v = get_env_var!(key, field);
69-
parse(&v)
70-
.map(Some)
71-
.map_err(|err| {
69+
let is_empty = v.is_empty();
70+
match parse(&v) {
71+
Ok(v) => Ok(Some(v)),
72+
Err(_) if is_empty => Ok(None),
73+
Err(err) => Err(
7274
ErrorInner::EnvParseError {
7375
field: field.to_owned(),
7476
key: key.to_owned(),
7577
err: Box::new(err),
7678
}.into()
77-
})
79+
),
80+
}
7881
}
7982

8083
pub fn from_env_with_deserializer<T>(
@@ -83,9 +86,11 @@ pub fn from_env_with_deserializer<T>(
8386
deserialize: fn(crate::env::Deserializer) -> Result<T, crate::env::DeError>,
8487
) -> Result<Option<T>, Error> {
8588
let s = get_env_var!(key, field);
89+
let is_empty = s.is_empty();
8690

8791
match deserialize(crate::env::Deserializer::new(s)) {
8892
Ok(v) => Ok(Some(v)),
93+
Err(_) if is_empty => Ok(None),
8994
Err(e) => Err(ErrorInner::EnvDeserialization {
9095
key: key.into(),
9196
field: field.into(),

src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ pub use crate::{
337337
/// Assigns an environment variable to this field. In [`Partial::from_env`], the
338338
/// variable is checked and deserialized into the field if present.
339339
///
340+
/// If the env var is set to an empty string and if the field fails to
341+
/// parse/deserialize from it, it is treated as unset.
342+
///
340343
/// ### `parse_env`
341344
///
342345
/// ```ignore

tests/env.rs

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use serde::Deserialize;
2-
use confique::{Config};
2+
use confique::{Config, Partial};
3+
use pretty_assertions::assert_eq;
34

45
#[derive(Debug, Deserialize)]
56
enum Foo { A, B, C }
@@ -17,3 +18,47 @@ fn enum_env() {
1718
let conf = Conf::builder().env().load();
1819
assert!(matches!(conf, Ok(Conf { foo: Foo::B })));
1920
}
21+
22+
fn my_parser(s: &str) -> Result<u32, impl std::error::Error> {
23+
s.trim().parse()
24+
}
25+
26+
#[test]
27+
fn empty_error_is_unset() {
28+
#[derive(Config)]
29+
#[config(partial_attr(derive(PartialEq, Debug)))]
30+
#[allow(dead_code)]
31+
struct Conf {
32+
#[config(env = "EMPTY_ERROR_IS_UNSET_FOO")]
33+
foo: u32,
34+
35+
#[config(env = "EMPTY_ERROR_IS_UNSET_BAR", parse_env = my_parser)]
36+
bar: u32,
37+
38+
#[config(env = "EMPTY_ERROR_IS_UNSET_BAZ")]
39+
baz: String,
40+
}
41+
42+
type Partial = <Conf as Config>::Partial;
43+
44+
std::env::set_var("EMPTY_ERROR_IS_UNSET_FOO", "");
45+
assert_eq!(Partial::from_env().unwrap(), Partial {
46+
foo: None,
47+
bar: None,
48+
baz: None,
49+
});
50+
51+
std::env::set_var("EMPTY_ERROR_IS_UNSET_BAR", "");
52+
assert_eq!(Partial::from_env().unwrap(), Partial {
53+
foo: None,
54+
bar: None,
55+
baz: None,
56+
});
57+
58+
std::env::set_var("EMPTY_ERROR_IS_UNSET_BAZ", "");
59+
assert_eq!(Partial::from_env().unwrap(), Partial {
60+
foo: None,
61+
bar: None,
62+
baz: Some("".into()),
63+
});
64+
}

0 commit comments

Comments
 (0)