diff --git a/src/error.rs b/src/error.rs index 2d738b94..c8c1435d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -78,6 +78,21 @@ pub enum ConfigError { key: Option, }, + /// Custom message + At { + /// Error being extended with a path + error: Box, + + /// The URI that references the source that the value came from. + /// Example: `/path/to/config.json` or `Environment` or `etcd://localhost` + // TODO: Why is this called Origin but FileParse has a uri field? + origin: Option, + + /// The key in the configuration hash of this value (if available where the + /// error is generated). + key: Option, + }, + /// Custom message Message(String), @@ -130,7 +145,17 @@ impl ConfigError { key: Some(key.into()), }, - _ => self, + Self::At { origin, error, .. } => Self::At { + error, + origin, + key: Some(key.into()), + }, + + other => Self::At { + error: Box::new(other), + origin: None, + key: Some(key.into()), + }, } } @@ -157,8 +182,17 @@ impl ConfigError { expected, key: Some(concat(key)), }, + Self::At { error, origin, key } => Self::At { + error, + origin, + key: Some(concat(key)), + }, Self::NotFound(key) => Self::NotFound(concat(Some(key))), - _ => self, + other => Self::At { + error: Box::new(other), + origin: None, + key: Some(concat(None)), + }, } } @@ -217,6 +251,24 @@ impl fmt::Display for ConfigError { Ok(()) } + ConfigError::At { + ref error, + ref origin, + ref key, + } => { + write!(f, "{error}")?; + + if let Some(ref key) = *key { + write!(f, " for key `{key}`")?; + } + + if let Some(ref origin) = *origin { + write!(f, " in {origin}")?; + } + + Ok(()) + } + ConfigError::FileParse { ref cause, ref uri } => { write!(f, "{cause}")?; diff --git a/tests/testsuite/deserialize-invalid-type.json b/tests/testsuite/deserialize-invalid-type.json new file mode 100644 index 00000000..6435ab7b --- /dev/null +++ b/tests/testsuite/deserialize-invalid-type.json @@ -0,0 +1,5 @@ +{ + "place": { + "name": "Torre di Pisa" + } +} diff --git a/tests/testsuite/deserialize-missing-field.json b/tests/testsuite/deserialize-missing-field.json new file mode 100644 index 00000000..31bc3579 --- /dev/null +++ b/tests/testsuite/deserialize-missing-field.json @@ -0,0 +1,3 @@ +{ + "inner": { "value": 42 } +} diff --git a/tests/testsuite/errors.rs b/tests/testsuite/errors.rs index 74edd41a..e53b8fa6 100644 --- a/tests/testsuite/errors.rs +++ b/tests/testsuite/errors.rs @@ -5,7 +5,7 @@ use config::{Config, ConfigError, File, FileFormat, Map, Value}; #[test] #[cfg(feature = "json")] -fn test_error_path_index_bounds() { +fn test_path_index_bounds() { let c = Config::builder() .add_source(File::from_str( r#" @@ -28,7 +28,7 @@ fn test_error_path_index_bounds() { #[test] #[cfg(feature = "json")] -fn test_error_path_index_negative_bounds() { +fn test_path_index_negative_bounds() { let c = Config::builder() .add_source(File::from_str( r#" @@ -51,7 +51,7 @@ fn test_error_path_index_negative_bounds() { #[test] #[cfg(feature = "json")] -fn test_error_parse() { +fn test_parse() { let res = Config::builder() .add_source(File::from_str( r#" @@ -72,7 +72,23 @@ fn test_error_parse() { #[test] #[cfg(feature = "json")] -fn test_error_type() { +fn test_root_not_table() { + let e = Config::builder() + .add_source(File::from_str(r#"false"#, FileFormat::Json)) + .build() + .unwrap_err(); + match e { + ConfigError::FileParse { cause, .. } => assert_eq!( + "invalid type: boolean `false`, expected a map", + format!("{cause}") + ), + _ => panic!("Wrong error: {:?}", e), + } +} + +#[test] +#[cfg(feature = "json")] +fn test_get_invalid_type() { let c = Config::builder() .add_source(File::from_str( r#" @@ -96,43 +112,85 @@ fn test_error_type() { #[test] #[cfg(feature = "json")] -fn test_error_deser_whole() { - #[derive(Deserialize, Debug)] - struct Place { - #[allow(dead_code)] - name: usize, // is actually s string - } +fn test_get_invalid_type_file() { + let c = Config::builder() + .add_source(File::new( + "tests/testsuite/get-invalid-type.json", + FileFormat::Json, + )) + .build() + .unwrap(); - #[derive(Deserialize, Debug)] - struct Output { + let res = c.get::("boolean_s_parse"); + + assert!(res.is_err()); + assert_data_eq!( + res.unwrap_err().to_string(), + str![[ + r#"invalid type: string "fals", expected a boolean for key `boolean_s_parse` in tests/testsuite/get-invalid-type.json"# + ]] + ); +} + +#[test] +#[cfg(feature = "json")] +fn test_get_missing_field() { + #[derive(Debug, Deserialize)] + struct InnerSettings { #[allow(dead_code)] - place: Place, + value: u32, + #[allow(dead_code)] + value2: u32, } let c = Config::builder() .add_source(File::from_str( r#" { - "place": { - "name": "Torre di Pisa" - } + "inner": { "value": 42 } } -"#, + "#, FileFormat::Json, )) .build() .unwrap(); - let res = c.try_deserialize::(); + let res = c.get::("inner"); assert_data_eq!( res.unwrap_err().to_string(), - str![[r#"invalid type: string "Torre di Pisa", expected an integer for key `place.name`"#]] + str!["missing field `value2` for key `inner`"] + ); +} + +#[test] +#[cfg(feature = "json")] +fn test_get_missing_field_file() { + #[derive(Debug, Deserialize)] + struct InnerSettings { + #[allow(dead_code)] + value: u32, + #[allow(dead_code)] + value2: u32, + } + + let c = Config::builder() + .add_source(File::new( + "tests/testsuite/get-missing-field.json", + FileFormat::Json, + )) + .build() + .unwrap(); + + let res = c.get::("inner"); + assert_data_eq!( + res.unwrap_err().to_string(), + str!["missing field `value2` for key `inner`"] ); } #[test] #[cfg(feature = "json")] -fn test_error_type_detached() { +fn test_get_bool_invalid_type() { let c = Config::builder() .add_source(File::from_str( r#" @@ -145,24 +203,23 @@ fn test_error_type_detached() { .build() .unwrap(); - let value = c.get::("boolean_s_parse").unwrap(); - let res = value.try_deserialize::(); + let res = c.get_bool("boolean_s_parse"); assert!(res.is_err()); assert_data_eq!( res.unwrap_err().to_string(), - str![[r#"invalid type: string "fals", expected a boolean"#]] + str![[r#"invalid type: string "fals", expected a boolean for key `boolean_s_parse`"#]] ); } #[test] #[cfg(feature = "json")] -fn test_error_type_get_bool() { +fn test_get_table_invalid_type() { let c = Config::builder() .add_source(File::from_str( r#" { - "boolean_s_parse": "fals" + "debug": true } "#, FileFormat::Json, @@ -170,18 +227,18 @@ fn test_error_type_get_bool() { .build() .unwrap(); - let res = c.get_bool("boolean_s_parse"); + let res = c.get_table("debug"); assert!(res.is_err()); assert_data_eq!( res.unwrap_err().to_string(), - str![[r#"invalid type: string "fals", expected a boolean for key `boolean_s_parse`"#]] + str!["invalid type: boolean `true`, expected a map for key `debug`"] ); } #[test] #[cfg(feature = "json")] -fn test_error_type_get_table() { +fn test_get_array_invalid_type() { let c = Config::builder() .add_source(File::from_str( r#" @@ -194,23 +251,23 @@ fn test_error_type_get_table() { .build() .unwrap(); - let res = c.get_table("debug"); + let res = c.get_array("debug"); assert!(res.is_err()); assert_data_eq!( res.unwrap_err().to_string(), - str!["invalid type: boolean `true`, expected a map for key `debug`"] + str!["invalid type: boolean `true`, expected an array for key `debug`"] ); } #[test] #[cfg(feature = "json")] -fn test_error_type_get_array() { +fn test_value_deserialize_invalid_type() { let c = Config::builder() .add_source(File::from_str( r#" { - "debug": true + "boolean_s_parse": "fals" } "#, FileFormat::Json, @@ -218,17 +275,18 @@ fn test_error_type_get_array() { .build() .unwrap(); - let res = c.get_array("debug"); + let value = c.get::("boolean_s_parse").unwrap(); + let res = value.try_deserialize::(); assert!(res.is_err()); assert_data_eq!( res.unwrap_err().to_string(), - str!["invalid type: boolean `true`, expected an array for key `debug`"] + str![[r#"invalid type: string "fals", expected a boolean"#]] ); } #[test] -fn test_error_enum_de() { +fn test_value_deserialize_enum() { #[derive(Debug, Deserialize, PartialEq, Eq)] enum Diode { Off, @@ -262,38 +320,44 @@ fn test_error_enum_de() { #[test] #[cfg(feature = "json")] -fn error_with_path() { - #[derive(Debug, Deserialize)] - struct Inner { +fn test_deserialize_invalid_type() { + #[derive(Deserialize, Debug)] + struct Place { #[allow(dead_code)] - test: i32, + name: usize, // is actually s string } - #[derive(Debug, Deserialize)] - struct Outer { + #[derive(Deserialize, Debug)] + struct Output { #[allow(dead_code)] - inner: Inner, + place: Place, } - const CFG: &str = r#" + + let c = Config::builder() + .add_source(File::from_str( + r#" { - "inner": { - "test": "ABC" + "place": { + "name": "Torre di Pisa" } } -"#; - - let e = Config::builder() - .add_source(File::from_str(CFG, FileFormat::Json)) +"#, + FileFormat::Json, + )) .build() - .unwrap() - .try_deserialize::() - .unwrap_err(); + .unwrap(); + let res = c.try_deserialize::(); + let e = res.unwrap_err(); + assert_data_eq!( + e.to_string(), + str![[r#"invalid type: string "Torre di Pisa", expected an integer for key `place.name`"#]] + ); if let ConfigError::Type { key: Some(path), .. } = e { - assert_eq!(path, "inner.test"); + assert_eq!(path, "place.name"); } else { panic!("Wrong error {:?}", e); } @@ -301,18 +365,109 @@ fn error_with_path() { #[test] #[cfg(feature = "json")] -fn test_error_root_not_table() { - match Config::builder() - .add_source(File::from_str(r#"false"#, FileFormat::Json)) +fn test_deserialize_invalid_type_file() { + #[derive(Deserialize, Debug)] + struct Place { + #[allow(dead_code)] + name: usize, // is actually s string + } + + #[derive(Deserialize, Debug)] + struct Output { + #[allow(dead_code)] + place: Place, + } + + let c = Config::builder() + .add_source(File::new( + "tests/testsuite/deserialize-invalid-type.json", + FileFormat::Json, + )) .build() + .unwrap(); + + let res = c.try_deserialize::(); + let e = res.unwrap_err(); + assert_data_eq!( + e.to_string(), + str![[ + r#"invalid type: string "Torre di Pisa", expected an integer for key `place.name` in tests/testsuite/deserialize-invalid-type.json"# + ]] + ); + if let ConfigError::Type { + key: Some(path), .. + } = e { - Ok(_) => panic!("Should not merge if root is not a table"), - Err(e) => match e { - ConfigError::FileParse { cause, .. } => assert_eq!( - "invalid type: boolean `false`, expected a map", - format!("{cause}") - ), - _ => panic!("Wrong error: {:?}", e), - }, + assert_eq!(path, "place.name"); + } else { + panic!("Wrong error {:?}", e); } } + +#[test] +#[cfg(feature = "json")] +fn test_deserialize_missing_field() { + #[derive(Debug, Deserialize)] + struct Settings { + #[allow(dead_code)] + inner: InnerSettings, + } + + #[derive(Debug, Deserialize)] + struct InnerSettings { + #[allow(dead_code)] + value: u32, + #[allow(dead_code)] + value2: u32, + } + + let c = Config::builder() + .add_source(File::from_str( + r#" +{ + "inner": { "value": 42 } +} + "#, + FileFormat::Json, + )) + .build() + .unwrap(); + + let res = c.try_deserialize::(); + assert_data_eq!( + res.unwrap_err().to_string(), + str!["missing field `value2` for key `inner`"] + ); +} + +#[test] +#[cfg(feature = "json")] +fn test_deserialize_missing_field_file() { + #[derive(Debug, Deserialize)] + struct Settings { + #[allow(dead_code)] + inner: InnerSettings, + } + + #[derive(Debug, Deserialize)] + struct InnerSettings { + #[allow(dead_code)] + value: u32, + #[allow(dead_code)] + value2: u32, + } + + let c = Config::builder() + .add_source(File::new( + "tests/testsuite/deserialize-missing-field.json", + FileFormat::Json, + )) + .build() + .unwrap(); + + let res = c.try_deserialize::(); + assert_data_eq!( + res.unwrap_err().to_string(), + str!["missing field `value2` for key `inner`"] + ); +} diff --git a/tests/testsuite/get-invalid-type.json b/tests/testsuite/get-invalid-type.json new file mode 100644 index 00000000..e1799c27 --- /dev/null +++ b/tests/testsuite/get-invalid-type.json @@ -0,0 +1,3 @@ +{ + "boolean_s_parse": "fals" +} diff --git a/tests/testsuite/get-missing-field.json b/tests/testsuite/get-missing-field.json new file mode 100644 index 00000000..31bc3579 --- /dev/null +++ b/tests/testsuite/get-missing-field.json @@ -0,0 +1,3 @@ +{ + "inner": { "value": 42 } +} diff --git a/tests/testsuite/log.rs b/tests/testsuite/log.rs index 29bf7eca..d8cda030 100644 --- a/tests/testsuite/log.rs +++ b/tests/testsuite/log.rs @@ -53,6 +53,6 @@ fn test_load_level_lowercase() { assert!(s.is_err()); assert_data_eq!( s.unwrap_err().to_string(), - str!["enum Level does not have variant constructor error"] + str!["enum Level does not have variant constructor error for key `log`"] ); }