use pretty_assertions::assert_eq; use confique::{Config, Partial}; fn validate_not_1234(foo: &u32) -> Result<(), &'static str> { if *foo == 1234 { Err("bad password") } else { Ok(()) } } #[test] #[should_panic(expected = "default config value for `Conf::foo` cannot be \ deserialized: Error(\"validation failed: bad password\")")] fn invalid_default_panics_function() { #[derive(Config)] #[allow(dead_code)] struct Conf { #[config(default = 1234, validate = validate_not_1234)] foo: u32, } let _ = ::Partial::default_values(); } #[test] #[should_panic(expected = "default config value for `Conf::foo` cannot be \ deserialized: Error(\"validation failed: ugly number\")")] fn invalid_default_panics_assert_like() { #[derive(Config)] #[allow(dead_code)] struct Conf { #[config(default = 1234, validate(*foo != 1234, "ugly number"))] foo: u32, } let _ = ::Partial::default_values(); } #[test] fn assert_like() { #[derive(Config)] #[allow(dead_code)] #[config(partial_attr(derive(Debug, PartialEq)))] struct Conf { #[config( env = "AL_REQ", validate(req.is_ascii(), "non-ASCII characters ~req are not allowed"), )] req: String, #[config( env = "AL_DEF", default = "root", validate(def.is_ascii(), "non-ASCII characters ~def are not allowed"), )] def: String, #[config( env = "AL_OPT", validate(opt.is_ascii(), "non-ASCII characters ~opt are not allowed"), )] opt: Option, } type Partial = ::Partial; // Defaults assert_eq!(Partial::default_values(), Partial { req: None, def: Some("root".into()), opt: None, }); // From env std::env::set_var("AL_REQ", "jürgen"); assert_err_contains(Partial::from_env(), "non-ASCII characters ~req are not allowed"); std::env::set_var("AL_REQ", "cat"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat".into()), def: None, opt: None, }); std::env::set_var("AL_DEF", "I ❤️ fluffy animals"); assert_err_contains(Partial::from_env(), "non-ASCII characters ~def are not allowed"); std::env::set_var("AL_DEF", "dog"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat".into()), def: Some("dog".into()), opt: None, }); std::env::set_var("AL_OPT", "Μου αρέσουν τα χνουδωτά ζώα"); assert_err_contains(Partial::from_env(), "non-ASCII characters ~opt are not allowed"); std::env::set_var("AL_OPT", "fox"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat".into()), def: Some("dog".into()), opt: Some("fox".into()), }); // From file assert_err_contains( toml::from_str::(r#"req = "jürgen""#), "non-ASCII characters ~req are not allowed", ); assert_err_contains( toml::from_str::(r#"def = "I ❤️ fluffy animals""#), "non-ASCII characters ~def are not allowed", ); assert_err_contains( toml::from_str::(r#"opt = "Μου αρέσουν τα χνουδωτά ζώα""#), "non-ASCII characters ~opt are not allowed", ); assert_eq!( toml::from_str::("req = \"cat\"\ndef = \"dog\"\nopt = \"fox\"").unwrap(), Partial { req: Some("cat".into()), def: Some("dog".into()), opt: Some("fox".into()), }, ); } fn assert_is_ascii(s: &String) -> Result<(), &'static str> { if !s.is_ascii() { Err("non-ASCII characters are not allowed") } else { Ok(()) } } #[test] fn function() { #[derive(Config)] #[allow(dead_code)] #[config(partial_attr(derive(Debug, PartialEq)))] struct Conf { #[config(env = "FN_REQ", validate = assert_is_ascii)] req: String, #[config(env = "FN_DEF", default = "root", validate = assert_is_ascii)] def: String, #[config(env = "FN_OPT", validate = assert_is_ascii)] opt: Option, } type Partial = ::Partial; // Defaults assert_eq!(Partial::default_values(), Partial { req: None, def: Some("root".into()), opt: None, }); // From env std::env::set_var("FN_REQ", "jürgen"); assert_err_contains(Partial::from_env(), "non-ASCII characters are not allowed"); std::env::set_var("FN_REQ", "cat"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat".into()), def: None, opt: None, }); std::env::set_var("FN_DEF", "I ❤️ fluffy animals"); assert_err_contains(Partial::from_env(), "non-ASCII characters are not allowed"); std::env::set_var("FN_DEF", "dog"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat".into()), def: Some("dog".into()), opt: None, }); std::env::set_var("FN_OPT", "Μου αρέσουν τα χνουδωτά ζώα"); assert_err_contains(Partial::from_env(), "non-ASCII characters are not allowed"); std::env::set_var("FN_OPT", "fox"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat".into()), def: Some("dog".into()), opt: Some("fox".into()), }); // From file assert_err_contains( toml::from_str::(r#"req = "jürgen""#), "non-ASCII characters are not allowed", ); assert_err_contains( toml::from_str::(r#"def = "I ❤️ fluffy animals""#), "non-ASCII characters are not allowed", ); assert_err_contains( toml::from_str::(r#"opt = "Μου αρέσουν τα χνουδωτά ζώα""#), "non-ASCII characters are not allowed", ); assert_eq!( toml::from_str::("req = \"cat\"\ndef = \"dog\"\nopt = \"fox\"").unwrap(), Partial { req: Some("cat".into()), def: Some("dog".into()), opt: Some("fox".into()), }, ); } fn deserialize_append<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let mut s = ::deserialize(deserializer)?; s.push_str("-henlo"); Ok(s) } #[test] fn assert_like_with_deserializer() { #[derive(Config)] #[allow(dead_code)] #[config(partial_attr(derive(Debug, PartialEq)))] struct Conf { #[config( env = "ALD_REQ", deserialize_with = deserialize_append, validate(req.is_ascii(), "non-ASCII characters ~req are not allowed"), )] req: String, #[config( env = "ALD_DEF", default = "root", deserialize_with = deserialize_append, validate(def.is_ascii(), "non-ASCII characters ~def are not allowed"), )] def: String, #[config( env = "ALD_OPT", deserialize_with = deserialize_append, validate(opt.is_ascii(), "non-ASCII characters ~opt are not allowed"), )] opt: Option, } type Partial = ::Partial; // Defaults assert_eq!(Partial::default_values(), Partial { req: None, def: Some("root-henlo".into()), opt: None, }); // From env std::env::set_var("ALD_REQ", "jürgen"); assert_err_contains(Partial::from_env(), "non-ASCII characters ~req are not allowed"); std::env::set_var("ALD_REQ", "cat"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat-henlo".into()), def: None, opt: None, }); std::env::set_var("ALD_DEF", "I ❤️ fluffy animals"); assert_err_contains(Partial::from_env(), "non-ASCII characters ~def are not allowed"); std::env::set_var("ALD_DEF", "dog"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat-henlo".into()), def: Some("dog-henlo".into()), opt: None, }); std::env::set_var("ALD_OPT", "Μου αρέσουν τα χνουδωτά ζώα"); assert_err_contains(Partial::from_env(), "non-ASCII characters ~opt are not allowed"); std::env::set_var("ALD_OPT", "fox"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat-henlo".into()), def: Some("dog-henlo".into()), opt: Some("fox-henlo".into()), }); // From file assert_err_contains( toml::from_str::(r#"req = "jürgen""#), "non-ASCII characters ~req are not allowed", ); assert_err_contains( toml::from_str::(r#"def = "I ❤️ fluffy animals""#), "non-ASCII characters ~def are not allowed", ); assert_err_contains( toml::from_str::(r#"opt = "Μου αρέσουν τα χνουδωτά ζώα""#), "non-ASCII characters ~opt are not allowed", ); assert_eq!( toml::from_str::("req = \"cat\"\ndef = \"dog\"\nopt = \"fox\"").unwrap(), Partial { req: Some("cat-henlo".into()), def: Some("dog-henlo".into()), opt: Some("fox-henlo".into()), }, ); } #[test] fn function_with_deserializer() { #[derive(Config)] #[allow(dead_code)] #[config(partial_attr(derive(Debug, PartialEq)))] struct Conf { #[config( env = "FND_REQ", validate = assert_is_ascii, deserialize_with = deserialize_append, )] req: String, #[config( env = "FND_DEF", default = "root", validate = assert_is_ascii, deserialize_with = deserialize_append, )] def: String, #[config( env = "FND_OPT", validate = assert_is_ascii, deserialize_with = deserialize_append, )] opt: Option, } type Partial = ::Partial; // Defaults assert_eq!(Partial::default_values(), Partial { req: None, def: Some("root-henlo".into()), opt: None, }); // From env std::env::set_var("FND_REQ", "jürgen"); assert_err_contains(Partial::from_env(), "non-ASCII characters are not allowed"); std::env::set_var("FND_REQ", "cat"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat-henlo".into()), def: None, opt: None, }); std::env::set_var("FND_DEF", "I ❤️ fluffy animals"); assert_err_contains(Partial::from_env(), "non-ASCII characters are not allowed"); std::env::set_var("FND_DEF", "dog"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat-henlo".into()), def: Some("dog-henlo".into()), opt: None, }); std::env::set_var("FND_OPT", "Μου αρέσουν τα χνουδωτά ζώα"); assert_err_contains(Partial::from_env(), "non-ASCII characters are not allowed"); std::env::set_var("FND_OPT", "fox"); assert_eq!(Partial::from_env().unwrap(), Partial { req: Some("cat-henlo".into()), def: Some("dog-henlo".into()), opt: Some("fox-henlo".into()), }); // From file assert_err_contains( toml::from_str::(r#"req = "jürgen""#), "non-ASCII characters are not allowed", ); assert_err_contains( toml::from_str::(r#"def = "I ❤️ fluffy animals""#), "non-ASCII characters are not allowed", ); assert_err_contains( toml::from_str::(r#"opt = "Μου αρέσουν τα χνουδωτά ζώα""#), "non-ASCII characters are not allowed", ); assert_eq!( toml::from_str::("req = \"cat\"\ndef = \"dog\"\nopt = \"fox\"").unwrap(), Partial { req: Some("cat-henlo".into()), def: Some("dog-henlo".into()), opt: Some("fox-henlo".into()), }, ); } fn validate_vec(v: &Vec) -> Result<(), &'static str> { if v.len() < 3 { return Err("list too short"); } Ok(()) } #[test] fn parse_env() { #[derive(Config)] #[allow(dead_code)] #[config(partial_attr(derive(Debug, PartialEq)))] struct Conf { #[config( env = "PE_FUN", parse_env = confique::env::parse::list_by_comma, validate = validate_vec, )] function: Vec, #[config( env = "PE_AL", parse_env = confique::env::parse::list_by_colon, validate(assert_like.len() >= 3, "list too ~req short"), )] assert_like: Vec, #[config( env = "PE_FUN_OPT", parse_env = confique::env::parse::list_by_semicolon, validate = validate_vec, )] function_opt: Option>, #[config( env = "PE_AL_OPT", parse_env = confique::env::parse::list_by_space, validate(assert_like_opt.len() >= 3, "list too ~opt short"), )] assert_like_opt: Option>, } type Partial = ::Partial; std::env::set_var("PE_FUN", "1,2"); assert_err_contains(Partial::from_env(), "list too short"); std::env::set_var("PE_FUN", "1,2,3"); assert_eq!(Partial::from_env().unwrap(), Partial { function: Some(vec![1, 2, 3]), assert_like: None, function_opt: None, assert_like_opt: None, }); std::env::set_var("PE_AL", "1:2"); assert_err_contains(Partial::from_env(), "list too ~req short"); std::env::set_var("PE_AL", "1:2:3"); assert_eq!(Partial::from_env().unwrap(), Partial { function: Some(vec![1, 2, 3]), assert_like: Some(vec![1, 2, 3]), function_opt: None, assert_like_opt: None, }); std::env::set_var("PE_FUN_OPT", "1;2"); assert_err_contains(Partial::from_env(), "list too short"); std::env::set_var("PE_FUN_OPT", "1;2;3"); assert_eq!(Partial::from_env().unwrap(), Partial { function: Some(vec![1, 2, 3]), assert_like: Some(vec![1, 2, 3]), function_opt: Some(vec![1, 2, 3]), assert_like_opt: None, }); std::env::set_var("PE_AL_OPT", "1 2"); assert_err_contains(Partial::from_env(), "list too ~opt short"); std::env::set_var("PE_AL_OPT", "1 2 3"); assert_eq!(Partial::from_env().unwrap(), Partial { function: Some(vec![1, 2, 3]), assert_like: Some(vec![1, 2, 3]), function_opt: Some(vec![1, 2, 3]), assert_like_opt: Some(vec![1, 2, 3]), }); } #[test] fn struct_validation() { #[derive(Config, PartialEq, Debug)] #[allow(dead_code)] #[config(validate = Self::validate)] struct Conf { foo: Option, bar: Option, } impl Conf { fn validate(&self) -> Result<(), &'static str> { if !(self.foo.is_some() ^ self.bar.is_some()) { return Err("exactly one of foo and bar must be set"); } Ok(()) } } let load = |s: &str| { let partial = toml::from_str::<::Partial>(s).unwrap(); Conf::from_partial(partial) }; assert_eq!(load("foo = 123").unwrap(), Conf { foo: Some(123), bar: None, }); assert_eq!(load("bar = 27").unwrap(), Conf { foo: None, bar: Some(27), }); assert_err_contains(load(""), "exactly one of foo and bar must be set"); assert_err_contains(load("foo = 123\nbar=27"), "exactly one of foo and bar must be set"); } #[track_caller] fn assert_err_contains(r: Result, expected: &str) { let e = r.map(|_| ()).unwrap_err(); let s = format!("{e:#}"); if !s.contains(expected) { panic!("expected error msg to contain '{expected}', but it doesn't: \n{s}"); } }