1use std::{collections::HashMap, fs, path::Path};
2
3use crate::error::{Result, ZusError};
4
5#[derive(Debug, Clone)]
7pub struct ConfigSection {
8 pub name: String,
10 values: HashMap<String, String>,
12}
13
14impl ConfigSection {
15 pub fn new(name: String) -> Self {
16 Self {
17 name,
18 values: HashMap::new(),
19 }
20 }
21
22 pub fn get_string(&self, key: &str, default: &str) -> String {
24 self.values.get(key).cloned().unwrap_or_else(|| default.to_string())
25 }
26
27 pub fn get_integer(&self, key: &str, default: i32) -> i32 {
29 self.values.get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
30 }
31
32 pub fn get_u32(&self, key: &str, default: u32) -> u32 {
34 self.values.get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
35 }
36
37 pub fn get_u64(&self, key: &str, default: u64) -> u64 {
39 self.values.get(key).and_then(|v| v.parse().ok()).unwrap_or(default)
40 }
41
42 pub fn get_bool(&self, key: &str, default: bool) -> bool {
44 self
45 .values
46 .get(key)
47 .map(|v| {
48 let v = v.to_lowercase();
49 v == "true" || v == "1" || v == "yes"
50 })
51 .unwrap_or(default)
52 }
53
54 pub fn get_all_pairs(&self) -> HashMap<String, String> {
56 self.values.clone()
57 }
58
59 pub fn set(&mut self, key: String, value: String) {
61 self.values.insert(key, value);
62 }
63
64 pub fn contains_key(&self, key: &str) -> bool {
66 self.values.contains_key(key)
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct ConfigProfile {
73 sections: HashMap<String, ConfigSection>,
74}
75
76impl ConfigProfile {
77 pub fn new() -> Self {
79 Self {
80 sections: HashMap::new(),
81 }
82 }
83
84 pub fn init<P: AsRef<Path>>(config_file: P) -> Result<Self> {
96 let content =
97 fs::read_to_string(config_file).map_err(|e| ZusError::Config(format!("Failed to read config file: {e}")))?;
98
99 let mut profile = ConfigProfile::new();
100 let mut current_section: Option<String> = None;
101
102 for (line_num, line) in content.lines().enumerate() {
103 let line = line.trim();
104
105 if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
107 continue;
108 }
109
110 if line.starts_with('[') && line.ends_with(']') {
112 let section_name = line[1..line.len() - 1].trim().to_string();
113 current_section = Some(section_name.clone());
114 profile
115 .sections
116 .entry(section_name.clone())
117 .or_insert_with(|| ConfigSection::new(section_name));
118 continue;
119 }
120
121 if let Some(equals_pos) = line.find('=') {
123 let key = line[..equals_pos].trim().to_string();
124 let value = line[equals_pos + 1..].trim().to_string();
125
126 if let Some(ref section_name) = current_section {
127 if let Some(section) = profile.sections.get_mut(section_name) {
128 section.set(key, value);
129 }
130 } else {
131 return Err(ZusError::Config(format!(
132 "Key-value pair at line {} outside of section",
133 line_num + 1
134 )));
135 }
136 } else {
137 return Err(ZusError::Config(format!(
138 "Invalid config line {}: {}",
139 line_num + 1,
140 line
141 )));
142 }
143 }
144
145 Ok(profile)
146 }
147
148 pub fn get_section(&self, name: &str) -> Option<&ConfigSection> {
150 self.sections.get(name)
151 }
152
153 pub fn get_section_mut(&mut self, name: &str) -> Option<&mut ConfigSection> {
155 self.sections.get_mut(name)
156 }
157
158 pub fn get_string(&self, section: &str, key: &str, default: &str) -> String {
160 self
161 .get_section(section)
162 .map(|s| s.get_string(key, default))
163 .unwrap_or_else(|| default.to_string())
164 }
165
166 pub fn get_integer(&self, section: &str, key: &str, default: i32) -> i32 {
168 self
169 .get_section(section)
170 .map(|s| s.get_integer(key, default))
171 .unwrap_or(default)
172 }
173
174 pub fn get_u32(&self, section: &str, key: &str, default: u32) -> u32 {
176 self
177 .get_section(section)
178 .map(|s| s.get_u32(key, default))
179 .unwrap_or(default)
180 }
181
182 pub fn get_u64(&self, section: &str, key: &str, default: u64) -> u64 {
184 self
185 .get_section(section)
186 .map(|s| s.get_u64(key, default))
187 .unwrap_or(default)
188 }
189
190 pub fn get_bool(&self, section: &str, key: &str, default: bool) -> bool {
192 self
193 .get_section(section)
194 .map(|s| s.get_bool(key, default))
195 .unwrap_or(default)
196 }
197}
198
199impl Default for ConfigProfile {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use {super::*, std::io::Write, tempfile::NamedTempFile};
208
209 #[test]
210 fn test_parse_config() {
211 let config_content = r#"
212# This is a comment
213[zooserver]
214netdevname=eth0
215srvport=2181
216maxthreadnum=40
217master=127.0.0.1:2181
218
219[zusnet]
220AsyncCallbackThreadNum=20
221ClientServiceThreadNum=4
222COMPRESS=true
223idleconntimeout=300
224maxconnects=59000
225MonitorOamAddr=zns://127.0.0.1:2181/zus/zusmonitord/
226AlarmOamAddr=zns://127.0.0.1:2181/zus/zusalarmd/
227"#;
228
229 let mut temp_file = NamedTempFile::new().unwrap();
230 temp_file.write_all(config_content.as_bytes()).unwrap();
231 temp_file.flush().unwrap();
232
233 let profile = ConfigProfile::init(temp_file.path()).unwrap();
234
235 let zooserver = profile.get_section("zooserver").unwrap();
237 assert_eq!(zooserver.get_string("netdevname", ""), "eth0");
238 assert_eq!(zooserver.get_string("srvport", ""), "2181");
239 assert_eq!(zooserver.get_integer("maxthreadnum", 0), 40);
240
241 let zusnet = profile.get_section("zusnet").unwrap();
243 assert_eq!(zusnet.get_u32("AsyncCallbackThreadNum", 0), 20);
244 assert_eq!(zusnet.get_u32("ClientServiceThreadNum", 0), 4);
245 assert!(zusnet.get_bool("COMPRESS", false));
246 assert_eq!(zusnet.get_u64("idleconntimeout", 0), 300);
247 assert_eq!(zusnet.get_u64("maxconnects", 0), 59000);
248
249 assert_eq!(profile.get_string("zooserver", "srvport", ""), "2181");
251 assert_eq!(profile.get_integer("zooserver", "maxthreadnum", 0), 40);
252 assert!(profile.get_bool("zusnet", "COMPRESS", false));
253 }
254
255 #[test]
256 fn test_empty_section() {
257 let config_content = r#"
258[section1]
259
260[section2]
261key=value
262"#;
263
264 let mut temp_file = NamedTempFile::new().unwrap();
265 temp_file.write_all(config_content.as_bytes()).unwrap();
266 temp_file.flush().unwrap();
267
268 let profile = ConfigProfile::init(temp_file.path()).unwrap();
269
270 assert!(profile.get_section("section1").is_some());
271 assert!(profile.get_section("section2").is_some());
272 }
273
274 #[test]
275 fn test_defaults() {
276 let profile = ConfigProfile::new();
277
278 assert_eq!(profile.get_string("missing", "key", "default"), "default");
279 assert_eq!(profile.get_integer("missing", "key", 42), 42);
280 assert!(profile.get_bool("missing", "key", true));
281 }
282}