Skip to main content

huml_rs/
lib.rs

1use std::collections::HashMap;
2
3mod parser;
4pub mod serde;
5#[cfg(test)]
6pub mod standard_tests;
7
8pub use parser::{
9    parse_document_root, parse_empty_dict, parse_empty_list, parse_huml, parse_inline_dict,
10    parse_inline_list, parse_scalar, IResult, ParseError, HUML_VERSION,
11};
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum HumlValue {
15    String(String),
16    Number(HumlNumber),
17    Boolean(bool),
18    Null,
19    List(Vec<HumlValue>),
20    Dict(HashMap<String, HumlValue>),
21}
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum HumlNumber {
25    Integer(i64),
26    Float(f64),
27    Nan,
28    Infinity(bool), // true = positive, false = negative
29}
30
31#[derive(Debug, Clone, PartialEq)]
32pub struct HumlDocument {
33    pub version: Option<String>,
34    pub root: HumlValue,
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn parses_simple_scalar_document() {
43        let (_, doc) = parse_huml("\"hello\"").expect("should parse");
44        assert_eq!(doc.root, HumlValue::String("hello".into()));
45    }
46
47    #[test]
48    fn parses_inline_list() {
49        if let HumlValue::List(values) = parse_inline_list("1, 2, 3").unwrap().1 {
50            assert_eq!(values.len(), 3);
51        } else {
52            panic!("expected list");
53        }
54    }
55
56    #[test]
57    fn parses_multiline_dict_document() {
58        let input = r#"
59key1: "value"
60key2::
61  nested: 1
62"#;
63        let (_, doc) = parse_huml(input).expect("should parse");
64        if let HumlValue::Dict(map) = doc.root {
65            assert!(map.contains_key("key1"));
66            assert!(map.contains_key("key2"));
67        } else {
68            panic!("expected dict");
69        }
70    }
71
72    #[test]
73    fn multiline_string_preserves_dedented() {
74        let input = r#"text: """
75  line one
76  line two
77  line three
78""""#;
79        let (_, doc) = parse_huml(input).expect("should parse");
80        if let HumlValue::Dict(map) = doc.root {
81            if let Some(HumlValue::String(s)) = map.get("text") {
82                assert_eq!(s, "line one\nline two\nline three");
83            } else {
84                panic!("expected string value");
85            }
86        } else {
87            panic!("expected dict");
88        }
89    }
90
91    #[test]
92    fn multiline_string_preserves_extra_spaces() {
93        let input = r#"text: """
94    line with extra spaces
95  line with minimal spaces
96      line with many spaces
97""""#;
98        let (_, doc) = parse_huml(input).expect("should parse");
99        if let HumlValue::Dict(map) = doc.root {
100            if let Some(HumlValue::String(s)) = map.get("text") {
101                assert_eq!(
102                    s,
103                    "  line with extra spaces\nline with minimal spaces\n    line with many spaces"
104                );
105            } else {
106                panic!("expected string value");
107            }
108        } else {
109            panic!("expected dict");
110        }
111    }
112
113    #[test]
114    fn multiline_string_with_empty_lines() {
115        let input = r#"text: """
116  first line
117
118  third line
119""""#;
120        let (_, doc) = parse_huml(input).expect("should parse");
121        if let HumlValue::Dict(map) = doc.root {
122            if let Some(HumlValue::String(s)) = map.get("text") {
123                assert_eq!(s, "first line\n\nthird line");
124            } else {
125                panic!("expected string value");
126            }
127        } else {
128            panic!("expected dict");
129        }
130    }
131
132    #[test]
133    fn multiline_string_minimal_indent() {
134        let input = r#"x: """
135first
136second
137""""#;
138        let (_, doc) = parse_huml(input).expect("should parse");
139        if let HumlValue::Dict(map) = doc.root {
140            if let Some(HumlValue::String(s)) = map.get("x") {
141                assert_eq!(s, "first\nsecond");
142            } else {
143                panic!("expected string value");
144            }
145        } else {
146            panic!("expected dict");
147        }
148    }
149
150    #[test]
151    fn backticks_multiline_string_rejected() {
152        let input = r#"text: ```
153  line one
154```"#;
155        assert!(parse_huml(input).is_err());
156    }
157
158    #[test]
159    fn multiline_string_in_list() {
160        let input = r#"items::
161  - """
162    line one
163    line two
164  """
165  - "regular string""#;
166        let (_, doc) = parse_huml(input).expect("should parse");
167        if let HumlValue::Dict(map) = doc.root {
168            if let Some(HumlValue::List(items)) = map.get("items") {
169                assert_eq!(items.len(), 2);
170                if let HumlValue::String(s) = &items[0] {
171                    assert_eq!(s, "line one\nline two");
172                } else {
173                    panic!("expected string in list");
174                }
175            } else {
176                panic!("expected list");
177            }
178        } else {
179            panic!("expected dict");
180        }
181    }
182
183    #[test]
184    fn duplicate_key_error_before_malformed_value() {
185        // This test ensures duplicate key errors are reported before parsing malformed values
186        let input = r#"
187key: "first"
188key: [this is malformed
189"#;
190        let result = parse_huml(input);
191        assert!(result.is_err());
192        let err_msg = result.unwrap_err().to_string();
193        // Should get duplicate key error, not a parse error from the malformed value
194        assert!(err_msg.contains("duplicate key"));
195    }
196}