Skip to main content

oxihuman_core/
config_reader.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5use std::collections::HashMap;
6
7/// Reads key=value configuration from text.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct ConfigReader {
11    values: HashMap<String, String>,
12    sections: Vec<String>,
13}
14
15impl Default for ConfigReader {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21#[allow(dead_code)]
22impl ConfigReader {
23    pub fn new() -> Self {
24        Self {
25            values: HashMap::new(),
26            sections: Vec::new(),
27        }
28    }
29
30    pub fn parse(text: &str) -> Self {
31        let mut reader = Self::new();
32        let mut current_section = String::new();
33        for line in text.lines() {
34            let trimmed = line.trim();
35            if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
36                continue;
37            }
38            if trimmed.starts_with('[') && trimmed.ends_with(']') {
39                current_section = trimmed[1..trimmed.len() - 1].to_string();
40                if !reader.sections.contains(&current_section) {
41                    reader.sections.push(current_section.clone());
42                }
43                continue;
44            }
45            if let Some((key, value)) = trimmed.split_once('=') {
46                let full_key = if current_section.is_empty() {
47                    key.trim().to_string()
48                } else {
49                    format!("{}.{}", current_section, key.trim())
50                };
51                reader.values.insert(full_key, value.trim().to_string());
52            }
53        }
54        reader
55    }
56
57    pub fn get(&self, key: &str) -> Option<&str> {
58        self.values.get(key).map(|s| s.as_str())
59    }
60
61    pub fn get_or(&self, key: &str, default: &str) -> String {
62        self.values
63            .get(key)
64            .cloned()
65            .unwrap_or_else(|| default.to_string())
66    }
67
68    pub fn get_int(&self, key: &str) -> Option<i64> {
69        self.values.get(key).and_then(|v| v.parse().ok())
70    }
71
72    pub fn get_float(&self, key: &str) -> Option<f64> {
73        self.values.get(key).and_then(|v| v.parse().ok())
74    }
75
76    pub fn get_bool(&self, key: &str) -> Option<bool> {
77        self.values.get(key).and_then(|v| match v.as_str() {
78            "true" | "1" | "yes" => Some(true),
79            "false" | "0" | "no" => Some(false),
80            _ => None,
81        })
82    }
83
84    pub fn contains(&self, key: &str) -> bool {
85        self.values.contains_key(key)
86    }
87
88    pub fn count(&self) -> usize {
89        self.values.len()
90    }
91
92    pub fn sections(&self) -> &[String] {
93        &self.sections
94    }
95
96    pub fn keys_in_section(&self, section: &str) -> Vec<String> {
97        let prefix = format!("{section}.");
98        self.values
99            .keys()
100            .filter(|k| k.starts_with(&prefix))
101            .cloned()
102            .collect()
103    }
104
105    pub fn all_keys(&self) -> Vec<&str> {
106        self.values.keys().map(|k| k.as_str()).collect()
107    }
108
109    pub fn set(&mut self, key: &str, value: &str) {
110        self.values.insert(key.to_string(), value.to_string());
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_empty() {
120        let r = ConfigReader::new();
121        assert_eq!(r.count(), 0);
122    }
123
124    #[test]
125    fn test_parse_simple() {
126        let r = ConfigReader::parse("key=value\nfoo=bar");
127        assert_eq!(r.get("key"), Some("value"));
128        assert_eq!(r.get("foo"), Some("bar"));
129    }
130
131    #[test]
132    fn test_parse_sections() {
133        let r = ConfigReader::parse("[section]\nk=v");
134        assert_eq!(r.get("section.k"), Some("v"));
135        assert_eq!(r.sections(), &["section"]);
136    }
137
138    #[test]
139    fn test_comments_ignored() {
140        let r = ConfigReader::parse("# comment\n; another\nk=v");
141        assert_eq!(r.count(), 1);
142    }
143
144    #[test]
145    fn test_get_int() {
146        let r = ConfigReader::parse("n=42");
147        assert_eq!(r.get_int("n"), Some(42));
148    }
149
150    #[test]
151    fn test_get_float() {
152        let r = ConfigReader::parse("f=2.75");
153        let v = r.get_float("f").expect("should succeed");
154        assert!((v - 2.75).abs() < 1e-6);
155    }
156
157    #[test]
158    fn test_get_bool() {
159        let r = ConfigReader::parse("a=true\nb=false\nc=yes");
160        assert_eq!(r.get_bool("a"), Some(true));
161        assert_eq!(r.get_bool("b"), Some(false));
162        assert_eq!(r.get_bool("c"), Some(true));
163    }
164
165    #[test]
166    fn test_get_or_default() {
167        let r = ConfigReader::new();
168        assert_eq!(r.get_or("missing", "default"), "default");
169    }
170
171    #[test]
172    fn test_contains() {
173        let r = ConfigReader::parse("k=v");
174        assert!(r.contains("k"));
175        assert!(!r.contains("x"));
176    }
177
178    #[test]
179    fn test_set() {
180        let mut r = ConfigReader::new();
181        r.set("k", "v");
182        assert_eq!(r.get("k"), Some("v"));
183    }
184}