#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum YamlScalar {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct YamlError {
pub line: usize,
pub message: String,
}
impl std::fmt::Display for YamlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "YAML error at line {}: {}", self.line, self.message)
}
}
#[derive(Debug, Clone, Default)]
pub struct YamlDocument {
entries: Vec<(String, YamlScalar)>,
}
impl YamlDocument {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: impl Into<String>, val: YamlScalar) {
self.entries.push((key.into(), val));
}
pub fn get(&self, key: &str) -> Option<&YamlScalar> {
self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub fn parse_scalar_line(
line: &str,
lineno: usize,
) -> Result<Option<(String, YamlScalar)>, YamlError> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return Ok(None);
}
let mut parts = line.splitn(2, ':');
let key = parts
.next()
.map(|s| s.trim().to_string())
.unwrap_or_default();
let raw = parts.next().map(|s| s.trim()).unwrap_or("");
if key.is_empty() {
return Err(YamlError {
line: lineno,
message: "empty key".to_string(),
});
}
let scalar = parse_scalar(raw);
Ok(Some((key, scalar)))
}
pub fn parse_scalar(raw: &str) -> YamlScalar {
if raw == "~" || raw == "null" || raw.is_empty() {
return YamlScalar::Null;
}
if raw == "true" || raw == "yes" {
return YamlScalar::Bool(true);
}
if raw == "false" || raw == "no" {
return YamlScalar::Bool(false);
}
if let Ok(i) = raw.parse::<i64>() {
return YamlScalar::Int(i);
}
if let Ok(f) = raw.parse::<f64>() {
return YamlScalar::Float(f);
}
let s = if (raw.starts_with('"') && raw.ends_with('"'))
|| (raw.starts_with('\'') && raw.ends_with('\''))
{
raw[1..raw.len().saturating_sub(1)].to_string()
} else {
raw.to_string()
};
YamlScalar::Str(s)
}
pub fn parse_yaml(input: &str) -> Result<YamlDocument, YamlError> {
let mut doc = YamlDocument::new();
for (i, line) in input.lines().enumerate() {
if let Some((k, v)) = parse_scalar_line(line, i + 1)? {
doc.insert(k, v);
}
}
Ok(doc)
}
pub fn get_int(doc: &YamlDocument, key: &str) -> Option<i64> {
doc.get(key).and_then(|v| {
if let YamlScalar::Int(i) = v {
Some(*i)
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_doc() {
assert!(YamlDocument::new().is_empty());
}
#[test]
fn test_parse_int() {
let doc = parse_yaml("port: 9090\n").expect("should succeed");
assert_eq!(get_int(&doc, "port"), Some(9090));
}
#[test]
fn test_parse_bool_true() {
let doc = parse_yaml("enabled: true\n").expect("should succeed");
assert_eq!(doc.get("enabled"), Some(&YamlScalar::Bool(true)));
}
#[test]
fn test_parse_bool_false() {
let doc = parse_yaml("enabled: false\n").expect("should succeed");
assert_eq!(doc.get("enabled"), Some(&YamlScalar::Bool(false)));
}
#[test]
fn test_parse_null() {
let doc = parse_yaml("x: null\n").expect("should succeed");
assert_eq!(doc.get("x"), Some(&YamlScalar::Null));
}
#[test]
fn test_parse_string() {
let doc = parse_yaml("name: oxihuman\n").expect("should succeed");
assert_eq!(
doc.get("name"),
Some(&YamlScalar::Str("oxihuman".to_string()))
);
}
#[test]
fn test_comment_skipped() {
let doc = parse_yaml("# comment\nkey: 1\n").expect("should succeed");
assert_eq!(doc.len(), 1);
}
#[test]
fn test_parse_float() {
let doc = parse_yaml("ratio: 3.14\n").expect("should succeed");
assert!(matches!(doc.get("ratio"), Some(YamlScalar::Float(_))));
}
#[test]
fn test_insert_get() {
let mut doc = YamlDocument::new();
doc.insert("k", YamlScalar::Int(7));
assert_eq!(doc.get("k"), Some(&YamlScalar::Int(7)));
}
#[test]
fn test_yes_no_boolean() {
assert_eq!(parse_scalar("yes"), YamlScalar::Bool(true));
assert_eq!(parse_scalar("no"), YamlScalar::Bool(false));
}
}