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)]
41 pub json_logging: bool,
42 #[serde(default)]
44 pub features: FeatureConfig,
45 #[serde(default)]
47 pub github: GitHubConfig,
48}
49
50#[derive(Deserialize, Debug)]
54pub struct FeatureConfig {
55 #[serde(default = "default_true")]
59 pub github_permalink: bool,
60}
61
62impl Default for FeatureConfig {
63 fn default() -> Self {
64 Self {
65 github_permalink: default_true(),
66 }
67 }
68}
69
70#[derive(Deserialize, Debug)]
72pub struct GitHubConfig {
73 #[serde(default = "default_max_lines")]
77 pub max_lines: usize,
78}
79
80impl Default for GitHubConfig {
81 fn default() -> Self {
82 Self {
83 max_lines: default_max_lines(),
84 }
85 }
86}
87
88fn default_true() -> bool {
89 true
90}
91
92fn default_max_lines() -> usize {
93 50
94}
95
96#[derive(thiserror::Error, Debug)]
98pub enum BabyriteConfigError {
99 #[error("Failed to read configuration file.")]
101 Read,
102 #[error("Failed to parse configuration file.")]
104 Parse,
105 #[error("Failed to set configuration file.")]
107 Set,
108}
109
110impl BabyriteConfig {
111 pub fn init() -> anyhow::Result<(), BabyriteConfigError> {
116 let envs = EnvConfig::get();
117 match &envs.config_file_path {
118 Some(p) => {
119 let buffer = &std::fs::read_to_string(p).map_err(|_| BabyriteConfigError::Read)?;
120 let config: BabyriteConfig =
121 toml::from_str(buffer).map_err(|_| BabyriteConfigError::Parse)?;
122 Ok(CONFIG.set(config).map_err(|_| BabyriteConfigError::Parse)?)
123 }
124 None => Ok(CONFIG
125 .set(BabyriteConfig::default())
126 .map_err(|_| BabyriteConfigError::Set)?),
127 }
128 }
129
130 pub fn get() -> &'static BabyriteConfig {
136 CONFIG.get().expect("Failed to get configuration.")
137 }
138}
139
140pub fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
142where
143 D: serde::Deserializer<'de>,
144{
145 let opt = Option::<String>::deserialize(deserializer)?;
146 Ok(opt.filter(|s| !s.is_empty()))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn default_config() {
155 let config = BabyriteConfig::default();
156 assert!(!config.json_logging);
157 assert!(config.features.github_permalink);
158 assert_eq!(config.github.max_lines, 50);
159 }
160
161 #[test]
162 fn deserialize_empty_config() {
163 let config: BabyriteConfig = toml::from_str("").unwrap();
164 assert!(!config.json_logging);
165 assert!(config.features.github_permalink);
166 assert_eq!(config.github.max_lines, 50);
167 }
168
169 #[test]
170 fn deserialize_full_config() {
171 let toml_str = r#"
172 json_logging = true
173
174 [features]
175 github_permalink = false
176
177 [github]
178 max_lines = 100
179 "#;
180 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
181 assert!(config.json_logging);
182 assert!(!config.features.github_permalink);
183 assert_eq!(config.github.max_lines, 100);
184 }
185
186 #[test]
187 fn deserialize_partial_config() {
188 let toml_str = r#"
189 json_logging = true
190 "#;
191 let config: BabyriteConfig = toml::from_str(toml_str).unwrap();
192 assert!(config.json_logging);
193 assert!(config.features.github_permalink);
195 assert_eq!(config.github.max_lines, 50);
196 }
197
198 #[test]
199 fn empty_string_as_none_with_empty() {
200 #[derive(Deserialize)]
201 struct Test {
202 #[serde(default, deserialize_with = "empty_string_as_none")]
203 value: Option<String>,
204 }
205
206 let t: Test = toml::from_str(r#"value = """#).unwrap();
207 assert!(t.value.is_none());
208 }
209
210 #[test]
211 fn empty_string_as_none_with_value() {
212 #[derive(Deserialize)]
213 struct Test {
214 #[serde(default, deserialize_with = "empty_string_as_none")]
215 value: Option<String>,
216 }
217
218 let t: Test = toml::from_str(r#"value = "hello""#).unwrap();
219 assert_eq!(t.value.as_deref(), Some("hello"));
220 }
221
222 #[test]
223 fn empty_string_as_none_absent() {
224 #[derive(Deserialize)]
225 struct Test {
226 #[serde(default, deserialize_with = "empty_string_as_none")]
227 value: Option<String>,
228 }
229
230 let t: Test = toml::from_str("").unwrap();
231 assert!(t.value.is_none());
232 }
233}