zus_common/
config.rs

1use std::{collections::HashMap, fs, path::Path};
2
3use crate::error::{Result, ZusError};
4
5/// Configuration section (matching C++ ConfigSection)
6#[derive(Debug, Clone)]
7pub struct ConfigSection {
8  /// Section name (e.g., "zooserver", "zusnet")
9  pub name: String,
10  /// Key-value pairs
11  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  /// Get a string value
23  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  /// Get an integer value
28  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  /// Get a u32 value
33  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  /// Get a u64 value
38  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  /// Get a boolean value
43  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  /// Get all key-value pairs
55  pub fn get_all_pairs(&self) -> HashMap<String, String> {
56    self.values.clone()
57  }
58
59  /// Set a value
60  pub fn set(&mut self, key: String, value: String) {
61    self.values.insert(key, value);
62  }
63
64  /// Check if a key exists
65  pub fn contains_key(&self, key: &str) -> bool {
66    self.values.contains_key(key)
67  }
68}
69
70/// Configuration profile (matching C++ ConfigProfile singleton)
71#[derive(Debug, Clone)]
72pub struct ConfigProfile {
73  sections: HashMap<String, ConfigSection>,
74}
75
76impl ConfigProfile {
77  /// Create a new empty config profile
78  pub fn new() -> Self {
79    Self {
80      sections: HashMap::new(),
81    }
82  }
83
84  /// Initialize from a config file
85  ///
86  /// Config file format (matching C++ .cfg format):
87  /// ```ini
88  /// [section1]
89  /// key1=value1
90  /// key2=value2
91  ///
92  /// [section2]
93  /// key.subkey=value
94  /// ```
95  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      // Skip empty lines and comments
106      if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
107        continue;
108      }
109
110      // Check for section header [section_name]
111      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      // Parse key=value pairs
122      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  /// Get a section by name
149  pub fn get_section(&self, name: &str) -> Option<&ConfigSection> {
150    self.sections.get(name)
151  }
152
153  /// Get a mutable section by name
154  pub fn get_section_mut(&mut self, name: &str) -> Option<&mut ConfigSection> {
155    self.sections.get_mut(name)
156  }
157
158  /// Get a string value from a specific section
159  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  /// Get an integer value from a specific section
167  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  /// Get a u32 value from a specific section
175  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  /// Get a u64 value from a specific section
183  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  /// Get a boolean value from a specific section
191  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    // Test zooserver section
236    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    // Test zusnet section
242    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    // Test convenience methods
250    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}