1use serde::Deserialize;
6use std::sync::OnceLock;
7
8pub static CONFIG: OnceLock<BabyriteConfig> = OnceLock::new();
10
11#[derive(Deserialize, Debug)]
13pub struct EnvConfig {
14 pub discord_api_token: String,
16 #[serde(default)]
18 #[serde(deserialize_with = "crate::config::empty_string_as_none")]
19 pub config_file_path: Option<String>,
20}
21
22impl EnvConfig {
23 pub fn get() -> &'static EnvConfig {
27 static ENV_CONFIG: OnceLock<EnvConfig> = OnceLock::new();
28 ENV_CONFIG
29 .get_or_init(|| envy::from_env().expect("Failed to load environment configuration."))
30 }
31}
32
33#[derive(Deserialize, Debug, Default)]
38pub struct BabyriteConfig {
39 #[serde(default)]
45 pub json_logging: bool,
46 #[serde(default)]
48 pub log: LogConfig,
49 #[serde(default)]
51 pub features: FeatureConfig,
52 #[serde(default)]
54 pub github: GitHubConfig,
55}
56
57#[derive(Deserialize, Debug)]
62#[serde(default)]
63pub struct LogConfig {
64 pub level: String,
69 pub format: Option<LogFormat>,
72}
73
74impl Default for LogConfig {
75 fn default() -> Self {
76 Self {
77 level: "babyrite=info".to_string(),
78 format: None,
79 }
80 }
81}
82
83#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
85#[serde(rename_all = "lowercase")]
86pub enum LogFormat {
87 Compact,
89 Json,
91}
92
93#[derive(Deserialize, Debug)]
98#[serde(default)]
99pub struct FeatureConfig {
100 pub github_permalink: bool,
104}
105
106impl Default for FeatureConfig {
107 fn default() -> Self {
108 Self {
109 github_permalink: true,
110 }
111 }
112}
113
114#[derive(Deserialize, Debug)]
118#[serde(default)]
119pub struct GitHubConfig {
120 pub max_lines: usize,
124}
125
126impl Default for GitHubConfig {
127 fn default() -> Self {
128 Self { max_lines: 50 }
129 }
130}
131
132#[derive(thiserror::Error, Debug)]
134pub enum BabyriteConfigError {
135 #[error("Failed to read configuration file.")]
137 Read,
138 #[error("Failed to parse configuration file.")]
140 Parse,
141 #[error("Failed to set configuration file.")]
143 Set,
144}
145
146impl BabyriteConfig {
147 pub fn init() -> Result<(), BabyriteConfigError> {
152 let config = match &EnvConfig::get().config_file_path {
153 Some(p) => {
154 let buffer = std::fs::read_to_string(p).map_err(|_| BabyriteConfigError::Read)?;
155 toml::from_str(&buffer).map_err(|_| BabyriteConfigError::Parse)?
156 }
157 None => BabyriteConfig::default(),
158 };
159 CONFIG.set(config).map_err(|_| BabyriteConfigError::Set)
160 }
161
162 pub fn get() -> &'static BabyriteConfig {
168 CONFIG.get().expect("Failed to get configuration.")
169 }
170
171 pub fn resolved_log_format(&self) -> LogFormat {
177 self.log.format.unwrap_or({
178 if self.json_logging {
179 LogFormat::Json
180 } else {
181 LogFormat::Compact
182 }
183 })
184 }
185}
186
187pub fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
189where
190 D: serde::Deserializer<'de>,
191{
192 let opt = Option::<String>::deserialize(deserializer)?;
193 Ok(opt.filter(|s| !s.is_empty()))
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn default_config() {
202 let config = BabyriteConfig::default();
203 assert!(!config.json_logging);
204 assert_eq!(config.log.level, "babyrite=info");
205 assert_eq!(config.log.format, None);
206 assert_eq!(config.resolved_log_format(), LogFormat::Compact);
207 assert!(config.features.github_permalink);
208 assert_eq!(config.github.max_lines, 50);
209 }
210
211 #[test]
212 fn deserialize_empty_config() {
213 let config: BabyriteConfig = toml::from_str("").unwrap();
214 assert!(!config.json_logging);
215 assert_eq!(config.log.level, "babyrite=info");
216 assert_eq!(config.log.format, None);
217 assert_eq!(config.resolved_log_format(), LogFormat::Compact);
218 assert!(config.features.github_permalink);
219 assert_eq!(config.github.max_lines, 50);
220 }
221
222 #[test]
223 fn deserialize_log_section() {
224 let toml_str = r#"
225 [log]
226 level = "babyrite=debug"
227 format = "json"
228 "#;
229 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
230 assert_eq!(config.log.level, "babyrite=debug");
231 assert_eq!(config.log.format, Some(LogFormat::Json));
232 assert_eq!(config.resolved_log_format(), LogFormat::Json);
233 }
234
235 #[test]
236 fn resolved_log_format_falls_back_to_json_logging() {
237 let toml_str = r#"
240 json_logging = true
241 "#;
242 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
243 assert_eq!(config.log.format, None);
244 assert_eq!(config.resolved_log_format(), LogFormat::Json);
245 }
246
247 #[test]
248 fn log_format_overrides_json_logging() {
249 let toml_str = r#"
251 json_logging = true
252
253 [log]
254 format = "compact"
255 "#;
256 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
257 assert_eq!(config.resolved_log_format(), LogFormat::Compact);
258 }
259
260 #[test]
261 fn deserialize_full_config() {
262 let toml_str = r#"
263 json_logging = true
264
265 [features]
266 github_permalink = false
267
268 [github]
269 max_lines = 100
270 "#;
271 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
272 assert!(config.json_logging);
273 assert!(!config.features.github_permalink);
274 assert_eq!(config.github.max_lines, 100);
275 }
276
277 #[test]
278 fn deserialize_partial_config() {
279 let toml_str = r#"
280 json_logging = true
281 "#;
282 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
283 assert!(config.json_logging);
284 assert!(config.features.github_permalink);
286 assert_eq!(config.github.max_lines, 50);
287 }
288
289 #[derive(Deserialize)]
290 struct EmptyStringAsNone {
291 #[serde(default, deserialize_with = "empty_string_as_none")]
292 value: Option<String>,
293 }
294
295 #[test]
296 fn empty_string_as_none_with_empty() {
297 let t: EmptyStringAsNone = toml::from_str(r#"value = """#).unwrap();
298 assert!(t.value.is_none());
299 }
300
301 #[test]
302 fn empty_string_as_none_with_value() {
303 let t: EmptyStringAsNone = toml::from_str(r#"value = "hello""#).unwrap();
304 assert_eq!(t.value.as_deref(), Some("hello"));
305 }
306
307 #[test]
308 fn empty_string_as_none_absent() {
309 let t: EmptyStringAsNone = toml::from_str("").unwrap();
310 assert!(t.value.is_none());
311 }
312}