1use core::fmt;
14use std::collections::BTreeMap;
15
16#[derive(Clone, Default, PartialEq)]
22pub struct DogsConfig {
23 pub dog: BTreeMap<String, toml::Table>,
25}
26
27impl fmt::Debug for DogsConfig {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.debug_struct("DogsConfig")
30 .field("dog", &format_args!("<{} tables>", self.dog.len()))
31 .finish()
32 }
33}
34
35impl DogsConfig {
36 pub fn load(source: Option<&str>) -> Result<Self, DogsConfigError> {
42 let Some(source) = source else {
43 return Ok(Self::default());
44 };
45 let dog = toml::from_str(source).map_err(DogsConfigError::Toml)?;
46 Ok(Self { dog })
47 }
48}
49
50#[non_exhaustive]
55pub enum DogsConfigError {
56 Toml(toml::de::Error),
58}
59
60impl fmt::Debug for DogsConfigError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::Toml(err) => f.debug_tuple("Toml").field(&err.message()).finish(),
75 }
76 }
77}
78
79impl fmt::Display for DogsConfigError {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::Toml(err) => write!(f, "invalid TOML in dogs.toml: {err}"),
83 }
84 }
85}
86
87impl core::error::Error for DogsConfigError {
88 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
89 match self {
90 Self::Toml(err) => Some(err),
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn a_missing_file_loads_as_an_empty_map() {
101 let config = DogsConfig::load(None).expect("None is not an error");
102 assert!(config.dog.is_empty());
103 }
104
105 #[test]
106 fn sections_are_keyed_by_name_with_no_prefix() {
107 let source = "[metrics]\nbind = \"127.0.0.1:9615\"\n\n[bark.sinks]\noncall = { kind = \"discord\" }\n";
108 let config = DogsConfig::load(Some(source)).expect("valid TOML");
109 assert_eq!(
110 config.dog.keys().collect::<Vec<_>>(),
111 vec!["bark", "metrics"]
112 );
113 assert_eq!(
114 config.dog["metrics"]["bind"].as_str(),
115 Some("127.0.0.1:9615")
116 );
117 }
118
119 #[test]
120 fn invalid_toml_is_a_named_error() {
121 let err = DogsConfig::load(Some("[metrics")).expect_err("unterminated table header");
122 assert!(matches!(err, DogsConfigError::Toml(_)));
123 }
124
125 #[test]
129 fn debug_redacts_the_source_a_parse_error_carries() {
130 let source =
131 "[bark.sinks]\noncall = { url = \"https://discord.com/api/webhooks/SECRET\" }\n[oops\n";
132 let err = DogsConfig::load(Some(source)).expect_err("unterminated table header");
133 assert_eq!(
134 format!("{err:?}"),
135 "Toml(\"invalid table header\\nexpected `.`, `]`\")"
136 );
137 assert!(
141 err.to_string().contains("line 3, column 6"),
142 "Display keeps its line-and-column context: {err}"
143 );
144 assert!(
145 !err.to_string().contains("SECRET"),
146 "and it quotes only the line that failed: {err}"
147 );
148 }
149
150 #[test]
151 fn debug_redacts_every_dog_section() {
152 let source =
153 "[bark.sinks]\noncall = { url = \"https://discord.com/api/webhooks/SECRET\" }\n";
154 let config = DogsConfig::load(Some(source)).expect("valid TOML");
155 assert_eq!(format!("{config:?}"), "DogsConfig { dog: <1 tables> }");
156 }
157}