Skip to main content

oxihuman_core/
yaml_parser.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Minimal YAML parser stub (scalar key-value pairs only).
6
7/// A YAML scalar value.
8#[derive(Debug, Clone, PartialEq)]
9pub enum YamlScalar {
10    Null,
11    Bool(bool),
12    Int(i64),
13    Float(f64),
14    Str(String),
15}
16
17/// A YAML parse error.
18#[derive(Debug, Clone, PartialEq)]
19pub struct YamlError {
20    pub line: usize,
21    pub message: String,
22}
23
24impl std::fmt::Display for YamlError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "YAML error at line {}: {}", self.line, self.message)
27    }
28}
29
30/// A flat YAML document (key → scalar).
31#[derive(Debug, Clone, Default)]
32pub struct YamlDocument {
33    entries: Vec<(String, YamlScalar)>,
34}
35
36impl YamlDocument {
37    /// Create an empty document.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Insert a key-value pair.
43    pub fn insert(&mut self, key: impl Into<String>, val: YamlScalar) {
44        self.entries.push((key.into(), val));
45    }
46
47    /// Look up a scalar by key.
48    pub fn get(&self, key: &str) -> Option<&YamlScalar> {
49        self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
50    }
51
52    /// Return the number of entries.
53    pub fn len(&self) -> usize {
54        self.entries.len()
55    }
56
57    /// Return `true` if empty.
58    pub fn is_empty(&self) -> bool {
59        self.entries.is_empty()
60    }
61}
62
63/// Parse a single YAML scalar line (`key: value`).
64pub fn parse_scalar_line(
65    line: &str,
66    lineno: usize,
67) -> Result<Option<(String, YamlScalar)>, YamlError> {
68    let line = line.trim();
69    if line.is_empty() || line.starts_with('#') {
70        return Ok(None);
71    }
72    let mut parts = line.splitn(2, ':');
73    let key = parts
74        .next()
75        .map(|s| s.trim().to_string())
76        .unwrap_or_default();
77    let raw = parts.next().map(|s| s.trim()).unwrap_or("");
78    if key.is_empty() {
79        return Err(YamlError {
80            line: lineno,
81            message: "empty key".to_string(),
82        });
83    }
84    let scalar = parse_scalar(raw);
85    Ok(Some((key, scalar)))
86}
87
88/// Parse a raw scalar string to a `YamlScalar`.
89pub fn parse_scalar(raw: &str) -> YamlScalar {
90    if raw == "~" || raw == "null" || raw.is_empty() {
91        return YamlScalar::Null;
92    }
93    if raw == "true" || raw == "yes" {
94        return YamlScalar::Bool(true);
95    }
96    if raw == "false" || raw == "no" {
97        return YamlScalar::Bool(false);
98    }
99    if let Ok(i) = raw.parse::<i64>() {
100        return YamlScalar::Int(i);
101    }
102    if let Ok(f) = raw.parse::<f64>() {
103        return YamlScalar::Float(f);
104    }
105    /* strip optional quotes */
106    let s = if (raw.starts_with('"') && raw.ends_with('"'))
107        || (raw.starts_with('\'') && raw.ends_with('\''))
108    {
109        raw[1..raw.len().saturating_sub(1)].to_string()
110    } else {
111        raw.to_string()
112    };
113    YamlScalar::Str(s)
114}
115
116/// Parse a multiline YAML string.
117pub fn parse_yaml(input: &str) -> Result<YamlDocument, YamlError> {
118    let mut doc = YamlDocument::new();
119    for (i, line) in input.lines().enumerate() {
120        if let Some((k, v)) = parse_scalar_line(line, i + 1)? {
121            doc.insert(k, v);
122        }
123    }
124    Ok(doc)
125}
126
127/// Retrieve an integer value.
128pub fn get_int(doc: &YamlDocument, key: &str) -> Option<i64> {
129    doc.get(key).and_then(|v| {
130        if let YamlScalar::Int(i) = v {
131            Some(*i)
132        } else {
133            None
134        }
135    })
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_empty_doc() {
144        /* new doc is empty */
145        assert!(YamlDocument::new().is_empty());
146    }
147
148    #[test]
149    fn test_parse_int() {
150        /* integer scalar */
151        let doc = parse_yaml("port: 9090\n").expect("should succeed");
152        assert_eq!(get_int(&doc, "port"), Some(9090));
153    }
154
155    #[test]
156    fn test_parse_bool_true() {
157        /* true keyword */
158        let doc = parse_yaml("enabled: true\n").expect("should succeed");
159        assert_eq!(doc.get("enabled"), Some(&YamlScalar::Bool(true)));
160    }
161
162    #[test]
163    fn test_parse_bool_false() {
164        /* false keyword */
165        let doc = parse_yaml("enabled: false\n").expect("should succeed");
166        assert_eq!(doc.get("enabled"), Some(&YamlScalar::Bool(false)));
167    }
168
169    #[test]
170    fn test_parse_null() {
171        /* null value */
172        let doc = parse_yaml("x: null\n").expect("should succeed");
173        assert_eq!(doc.get("x"), Some(&YamlScalar::Null));
174    }
175
176    #[test]
177    fn test_parse_string() {
178        /* string value */
179        let doc = parse_yaml("name: oxihuman\n").expect("should succeed");
180        assert_eq!(
181            doc.get("name"),
182            Some(&YamlScalar::Str("oxihuman".to_string()))
183        );
184    }
185
186    #[test]
187    fn test_comment_skipped() {
188        /* comment lines are ignored */
189        let doc = parse_yaml("# comment\nkey: 1\n").expect("should succeed");
190        assert_eq!(doc.len(), 1);
191    }
192
193    #[test]
194    fn test_parse_float() {
195        /* float scalar */
196        let doc = parse_yaml("ratio: 3.14\n").expect("should succeed");
197        assert!(matches!(doc.get("ratio"), Some(YamlScalar::Float(_))));
198    }
199
200    #[test]
201    fn test_insert_get() {
202        /* insert and get */
203        let mut doc = YamlDocument::new();
204        doc.insert("k", YamlScalar::Int(7));
205        assert_eq!(doc.get("k"), Some(&YamlScalar::Int(7)));
206    }
207
208    #[test]
209    fn test_yes_no_boolean() {
210        /* yes/no are booleans */
211        assert_eq!(parse_scalar("yes"), YamlScalar::Bool(true));
212        assert_eq!(parse_scalar("no"), YamlScalar::Bool(false));
213    }
214}