oxihuman-core 0.1.2

Core data structures, algorithms, and asset management for OxiHuman
Documentation
//! Minimal INI configuration file parser.

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IniSection {
    pub name: String,
    pub entries: Vec<(String, String)>,
}

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IniConfig {
    pub sections: Vec<IniSection>,
    pub default_section: String,
}

#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct IniParseError {
    pub line: usize,
    pub message: String,
}

#[allow(dead_code)]
pub fn parse_ini(text: &str) -> Result<IniConfig, IniParseError> {
    let mut cfg = IniConfig {
        sections: Vec::new(),
        default_section: String::from("default"),
    };
    let mut current_section = String::from("default");

    for (line_num, raw_line) in text.lines().enumerate() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
            continue;
        }
        if line.starts_with('[') {
            if !line.ends_with(']') {
                return Err(IniParseError {
                    line: line_num + 1,
                    message: format!("Malformed section header: {}", line),
                });
            }
            current_section = line[1..line.len() - 1].trim().to_string();
            if !cfg.sections.iter().any(|s| s.name == current_section) {
                cfg.sections.push(IniSection {
                    name: current_section.clone(),
                    entries: Vec::new(),
                });
            }
        } else if let Some(eq_pos) = line.find('=') {
            let key = line[..eq_pos].trim().to_string();
            let value = line[eq_pos + 1..].trim().to_string();
            if key.is_empty() {
                return Err(IniParseError {
                    line: line_num + 1,
                    message: String::from("Empty key"),
                });
            }
            // Find or create section
            if let Some(sec) = cfg.sections.iter_mut().find(|s| s.name == current_section) {
                sec.entries.push((key, value));
            } else {
                cfg.sections.push(IniSection {
                    name: current_section.clone(),
                    entries: vec![(key, value)],
                });
            }
        } else {
            return Err(IniParseError {
                line: line_num + 1,
                message: format!("Invalid line: {}", line),
            });
        }
    }
    Ok(cfg)
}

#[allow(dead_code)]
pub fn ini_get<'a>(cfg: &'a IniConfig, section: &str, key: &str) -> Option<&'a str> {
    cfg.sections
        .iter()
        .find(|s| s.name == section)
        .and_then(|s| s.entries.iter().find(|(k, _)| k == key))
        .map(|(_, v)| v.as_str())
}

#[allow(dead_code)]
pub fn ini_get_or<'a>(cfg: &'a IniConfig, section: &str, key: &str, default: &'a str) -> &'a str {
    ini_get(cfg, section, key).unwrap_or(default)
}

#[allow(dead_code)]
pub fn ini_set(cfg: &mut IniConfig, section: &str, key: &str, value: &str) {
    if let Some(sec) = cfg.sections.iter_mut().find(|s| s.name == section) {
        if let Some(entry) = sec.entries.iter_mut().find(|(k, _)| k == key) {
            entry.1 = value.to_string();
        } else {
            sec.entries.push((key.to_string(), value.to_string()));
        }
    } else {
        cfg.sections.push(IniSection {
            name: section.to_string(),
            entries: vec![(key.to_string(), value.to_string())],
        });
    }
}

#[allow(dead_code)]
pub fn ini_to_string(cfg: &IniConfig) -> String {
    let mut out = String::new();
    for sec in &cfg.sections {
        out.push_str(&format!("[{}]\n", sec.name));
        for (k, v) in &sec.entries {
            out.push_str(&format!("{} = {}\n", k, v));
        }
        out.push('\n');
    }
    out
}

#[allow(dead_code)]
pub fn ini_section_count(cfg: &IniConfig) -> usize {
    cfg.sections.len()
}

#[allow(dead_code)]
pub fn ini_key_count(cfg: &IniConfig, section: &str) -> usize {
    cfg.sections
        .iter()
        .find(|s| s.name == section)
        .map(|s| s.entries.len())
        .unwrap_or(0)
}

#[allow(dead_code)]
pub fn ini_has_section(cfg: &IniConfig, section: &str) -> bool {
    cfg.sections.iter().any(|s| s.name == section)
}

#[allow(dead_code)]
pub fn ini_has_key(cfg: &IniConfig, section: &str, key: &str) -> bool {
    cfg.sections
        .iter()
        .find(|s| s.name == section)
        .map(|s| s.entries.iter().any(|(k, _)| k == key))
        .unwrap_or(false)
}

#[allow(dead_code)]
pub fn ini_remove_key(cfg: &mut IniConfig, section: &str, key: &str) -> bool {
    if let Some(sec) = cfg.sections.iter_mut().find(|s| s.name == section) {
        let before = sec.entries.len();
        sec.entries.retain(|(k, _)| k != key);
        sec.entries.len() < before
    } else {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_empty() {
        let cfg = parse_ini("").expect("should succeed");
        assert_eq!(ini_section_count(&cfg), 0);
    }

    #[test]
    fn test_parse_single_section() {
        let text = "[server]\nhost = localhost\nport = 8080\n";
        let cfg = parse_ini(text).expect("should succeed");
        assert!(ini_has_section(&cfg, "server"));
        assert_eq!(ini_get(&cfg, "server", "host"), Some("localhost"));
        assert_eq!(ini_get(&cfg, "server", "port"), Some("8080"));
    }

    #[test]
    fn test_parse_multiple_sections() {
        let text = "[a]\nk1 = v1\n[b]\nk2 = v2\n";
        let cfg = parse_ini(text).expect("should succeed");
        assert_eq!(ini_section_count(&cfg), 2);
        assert_eq!(ini_get(&cfg, "a", "k1"), Some("v1"));
        assert_eq!(ini_get(&cfg, "b", "k2"), Some("v2"));
    }

    #[test]
    fn test_ini_get_or_fallback() {
        let cfg = parse_ini("[s]\nkey = val\n").expect("should succeed");
        let result = ini_get_or(&cfg, "s", "missing", "fallback");
        assert_eq!(result, "fallback");
    }

    #[test]
    fn test_ini_set_new_key() {
        let mut cfg = parse_ini("[s]\nk = v\n").expect("should succeed");
        ini_set(&mut cfg, "s", "new_key", "new_val");
        assert_eq!(ini_get(&cfg, "s", "new_key"), Some("new_val"));
    }

    #[test]
    fn test_ini_set_update_existing() {
        let mut cfg = parse_ini("[s]\nk = old\n").expect("should succeed");
        ini_set(&mut cfg, "s", "k", "new");
        assert_eq!(ini_get(&cfg, "s", "k"), Some("new"));
    }

    #[test]
    fn test_ini_to_string_roundtrip() {
        let text = "[section]\nfoo = bar\nbaz = 42\n\n";
        let cfg = parse_ini(text).expect("should succeed");
        let out = ini_to_string(&cfg);
        let cfg2 = parse_ini(&out).expect("should succeed");
        assert_eq!(ini_get(&cfg2, "section", "foo"), Some("bar"));
        assert_eq!(ini_get(&cfg2, "section", "baz"), Some("42"));
    }

    #[test]
    fn test_ini_remove_key() {
        let mut cfg = parse_ini("[s]\na = 1\nb = 2\n").expect("should succeed");
        let removed = ini_remove_key(&mut cfg, "s", "a");
        assert!(removed);
        assert!(!ini_has_key(&cfg, "s", "a"));
        assert!(ini_has_key(&cfg, "s", "b"));
    }

    #[test]
    fn test_ini_remove_nonexistent_key() {
        let mut cfg = parse_ini("[s]\na = 1\n").expect("should succeed");
        let removed = ini_remove_key(&mut cfg, "s", "z");
        assert!(!removed);
    }

    #[test]
    fn test_ini_key_count() {
        let cfg = parse_ini("[sec]\na = 1\nb = 2\nc = 3\n").expect("should succeed");
        assert_eq!(ini_key_count(&cfg, "sec"), 3);
        assert_eq!(ini_key_count(&cfg, "nonexistent"), 0);
    }

    #[test]
    fn test_parse_comments_skipped() {
        let text = "; comment\n[s]\n# another\nk = v\n";
        let cfg = parse_ini(text).expect("should succeed");
        assert_eq!(ini_key_count(&cfg, "s"), 1);
    }

    #[test]
    fn test_parse_invalid_line_error() {
        let result = parse_ini("[s]\nnot_valid_line\n");
        assert!(result.is_err());
    }

    #[test]
    fn test_ini_set_creates_section() {
        let mut cfg = parse_ini("").expect("should succeed");
        ini_set(&mut cfg, "new_section", "key", "val");
        assert!(ini_has_section(&cfg, "new_section"));
        assert_eq!(ini_get(&cfg, "new_section", "key"), Some("val"));
    }
}