oxihuman_core/
yaml_parser.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq)]
9pub enum YamlScalar {
10 Null,
11 Bool(bool),
12 Int(i64),
13 Float(f64),
14 Str(String),
15}
16
17#[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#[derive(Debug, Clone, Default)]
32pub struct YamlDocument {
33 entries: Vec<(String, YamlScalar)>,
34}
35
36impl YamlDocument {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn insert(&mut self, key: impl Into<String>, val: YamlScalar) {
44 self.entries.push((key.into(), val));
45 }
46
47 pub fn get(&self, key: &str) -> Option<&YamlScalar> {
49 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
50 }
51
52 pub fn len(&self) -> usize {
54 self.entries.len()
55 }
56
57 pub fn is_empty(&self) -> bool {
59 self.entries.is_empty()
60 }
61}
62
63pub 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
88pub 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 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
116pub 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
127pub 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 assert!(YamlDocument::new().is_empty());
146 }
147
148 #[test]
149 fn test_parse_int() {
150 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 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 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 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 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 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 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 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 assert_eq!(parse_scalar("yes"), YamlScalar::Bool(true));
212 assert_eq!(parse_scalar("no"), YamlScalar::Bool(false));
213 }
214}