Skip to main content

hk_parser/
lib.rs

1use indexmap::IndexMap;
2use lazy_static::lazy_static;
3use regex::Regex;
4use std::collections::HashSet;
5use std::env;
6use std::fs::File;
7use std::io::{self, BufRead, BufReader, Write};
8use std::path::Path;
9use std::str::FromStr;
10use thiserror::Error;
11use colored::Colorize;
12
13/// Represents the structure of a .hk file.
14/// Sections are top-level keys in the outer IndexMap to preserve order.
15pub type HkConfig = IndexMap<String, HkValue>;
16
17lazy_static! {
18    static ref INTERPOL_RE: Regex = Regex::new(r"\$\{([^}]+)\}").unwrap();
19}
20
21/// Enum for values in the .hk config: supports strings, numbers, booleans, arrays, and maps.
22#[derive(Debug, Clone, PartialEq)]
23pub enum HkValue {
24    String(String),
25    Number(f64),
26    Bool(bool),
27    Array(Vec<HkValue>),
28    Map(IndexMap<String, HkValue>),
29}
30
31impl HkValue {
32    pub fn as_string(&self) -> Result<String, HkError> {
33        match self {
34            Self::String(s) => Ok(s.clone()),
35            Self::Number(n) => Ok(n.to_string()),
36            Self::Bool(b) => Ok(b.to_string()),
37            _ => Err(HkError::TypeMismatch {
38                expected: "string".to_string(),
39                found: format!("{:?}", self),
40            }),
41        }
42    }
43
44    pub fn as_number(&self) -> Result<f64, HkError> {
45        if let Self::Number(n) = self {
46            Ok(*n)
47        } else {
48            Err(HkError::TypeMismatch {
49                expected: "number".to_string(),
50                found: format!("{:?}", self),
51            })
52        }
53    }
54
55    pub fn as_bool(&self) -> Result<bool, HkError> {
56        if let Self::Bool(b) = self {
57            Ok(*b)
58        } else {
59            Err(HkError::TypeMismatch {
60                expected: "bool".to_string(),
61                found: format!("{:?}", self),
62            })
63        }
64    }
65
66    pub fn as_array(&self) -> Result<&Vec<HkValue>, HkError> {
67        if let Self::Array(a) = self {
68            Ok(a)
69        } else {
70            Err(HkError::TypeMismatch {
71                expected: "array".to_string(),
72                found: format!("{:?}", self),
73            })
74        }
75    }
76
77    pub fn as_map(&self) -> Result<&IndexMap<String, HkValue>, HkError> {
78        if let Self::Map(m) = self {
79            Ok(m)
80        } else {
81            Err(HkError::TypeMismatch {
82                expected: "map".to_string(),
83                found: format!("{:?}", self),
84            })
85        }
86    }
87}
88
89/// Custom error type for parsing .hk files.
90#[derive(Error, Debug)]
91pub enum HkError {
92    #[error("IO error: {0}")]
93    Io(#[from] io::Error),
94    #[error("Parse error at line {line}, column {column}: {message}")]
95    Parse {
96        line: u32,
97        column: usize,
98        message: String,
99    },
100    #[error("Type mismatch: expected {expected}, found {found}")]
101    TypeMismatch { expected: String, found: String },
102    #[error("Missing field: {0}")]
103    MissingField(String),
104    #[error("Invalid reference: {0}")]
105    InvalidReference(String),
106    #[error("Cyclic reference detected: {0}")]
107    CyclicReference(String),
108    #[error("Key conflict: {0}")]
109    KeyConflict(String),
110}
111
112impl HkError {
113    pub fn pretty_print(&self, source: &str) {
114        match self {
115            Self::Parse { line, column, message } => {
116                eprintln!("{} {}", "error:".red().bold(), "parse error".red().bold());
117                eprintln!("  {} at {}:{}", "→".red(), line, column);
118                if let Some(line_content) = source.lines().nth((*line - 1) as usize) {
119                    eprintln!("\n  {}", line_content);
120                    eprintln!("  {}{}", " ".repeat(*column), "^".red().bold());
121                    eprintln!("  {}", message.red());
122                } else {
123                    eprintln!("  {}", message.red());
124                }
125
126                if message.contains("tag \"=>\"") {
127                    eprintln!("\n{} {}", "Hint:".yellow().bold(), "Try: key => value".cyan());
128                } else if message.contains("tag \"[\"") {
129                    eprintln!("\n{} {}", "Hint:".yellow().bold(), "Sections must start with [name]".cyan());
130                } else if message.contains("take_while1") {
131                    eprintln!("\n{} {}", "Hint:".yellow().bold(), "Keys can only contain letters, digits, '_', '-', '.'".cyan());
132                }
133            }
134            Self::TypeMismatch { expected, found } => {
135                eprintln!("{} {}", "error:".red().bold(), "type mismatch".red().bold());
136                eprintln!("  expected {}, got {}", expected.cyan(), found.red());
137            }
138            Self::InvalidReference(ref_var) => {
139                eprintln!("{} {}", "error:".red().bold(), "invalid reference".red().bold());
140                eprintln!("  {}", ref_var.red());
141                eprintln!("\n{} {}", "Hint:".yellow().bold(), "Check if the referenced key exists and is accessible".cyan());
142            }
143            Self::CyclicReference(path) => {
144                eprintln!("{} {}", "error:".red().bold(), "cyclic reference".red().bold());
145                eprintln!("  {}", path.red());
146            }
147            Self::KeyConflict(key) => {
148                eprintln!("{} {}", "error:".red().bold(), "key conflict".red().bold());
149                eprintln!("  Duplicate key '{}' in nested structure", key.red());
150            }
151            _ => eprintln!("{}", self.to_string().red()),
152        }
153    }
154}
155
156/// Parses a .hk file from a string input.
157pub fn parse_hk(input: &str) -> Result<HkConfig, HkError> {
158    let lines: Vec<&str> = input.lines().collect();
159    let mut config = IndexMap::new();
160    let mut i = 0;
161
162    while i < lines.len() {
163        let line = lines[i].trim_start();
164        if line.is_empty() || line.starts_with('!') {
165            i += 1;
166            continue;
167        }
168
169        if line.starts_with('[') {
170            let close = line.find(']').ok_or_else(|| HkError::Parse {
171                line: (i + 1) as u32,
172                column: line.find('[').unwrap() + 1,
173                message: "Unclosed section header".to_string(),
174            })?;
175            let section_name = line[1..close].trim();
176            if section_name.is_empty() {
177                return Err(HkError::Parse {
178                    line: (i + 1) as u32,
179                    column: close + 1,
180                    message: "Empty section name".to_string(),
181                });
182            }
183
184            // Find the end of this section (next section or EOF)
185            let mut end = i + 1;
186            while end < lines.len() {
187                let next_line = lines[end].trim_start();
188                if next_line.starts_with('[') {
189                    break;
190                }
191                end += 1;
192            }
193
194            let section_lines = &lines[i + 1..end];
195            let map = parse_map(1, section_lines, i + 1)?;
196            config.insert(section_name.to_string(), HkValue::Map(map));
197            i = end;
198        } else {
199            return Err(HkError::Parse {
200                line: (i + 1) as u32,
201                column: 1,
202                message: "Expected section header".to_string(),
203            });
204        }
205    }
206
207    Ok(config)
208}
209
210/// Parse a map from a slice of lines, starting with a given indentation level (number of dashes).
211/// level: the number of dashes expected for the current depth (e.g., 1 for "->", 2 for "-->")
212/// Returns the map and the index of the next line to process.
213fn parse_map(level: usize, lines: &[&str], start_line: usize) -> Result<IndexMap<String, HkValue>, HkError> {
214    let mut map = IndexMap::new();
215    let mut i = 0;
216
217    while i < lines.len() {
218        let line = lines[i];
219        let trimmed = line.trim_start();
220        if trimmed.is_empty() || trimmed.starts_with('!') {
221            i += 1;
222            continue;
223        }
224
225        // Count leading dashes
226        let dash_count = trimmed.chars().take_while(|c| *c == '-').count();
227        if dash_count == 0 {
228            return Err(HkError::Parse {
229                line: (start_line + i) as u32,
230                column: 1,
231                message: "Expected key or map header".to_string(),
232            });
233        }
234        if dash_count != level {
235            // Different level – return to caller
236            break;
237        }
238
239        // After dashes, skip any spaces, expect '>', then skip spaces
240        let after_dashes = &trimmed[dash_count..];
241        let rest = after_dashes.trim_start();
242        if !rest.starts_with('>') {
243            return Err(HkError::Parse {
244                line: (start_line + i) as u32,
245                column: dash_count + 1,
246                message: "Expected '>' after dashes".to_string(),
247            });
248        }
249        let after_gt = &rest[1..].trim_start();
250        if after_gt.is_empty() {
251            return Err(HkError::Parse {
252                line: (start_line + i) as u32,
253                column: dash_count + 1,
254                message: "Missing key after '>'".to_string(),
255            });
256        }
257
258        // Check if it's a key-value line (contains "=>")
259        if let Some(arrow_pos) = after_gt.find("=>") {
260            let key = after_gt[..arrow_pos].trim();
261            let value_part = after_gt[arrow_pos + 2..].trim();
262            let key = unquote_key(key);
263            if key.is_empty() {
264                return Err(HkError::Parse {
265                    line: (start_line + i) as u32,
266                    column: dash_count + 1,
267                    message: "Empty key".to_string(),
268                });
269            }
270            let value = parse_value(value_part, start_line + i, arrow_pos + dash_count + 2)?;
271            insert_key(&mut map, &key, value)?;
272            i += 1;
273        } else {
274            // It's a map header: "- > key" without "=>"
275            let key = after_gt.trim();
276            let key = unquote_key(key);
277            if key.is_empty() {
278                return Err(HkError::Parse {
279                    line: (start_line + i) as u32,
280                    column: dash_count + 1,
281                    message: "Empty map key".to_string(),
282                });
283            }
284
285            // Find the sub-lines that belong to this map (higher level)
286            let next_level = level + 1;
287            let mut j = i + 1;
288            while j < lines.len() {
289                let sub_line = lines[j];
290                let sub_trimmed = sub_line.trim_start();
291                if sub_trimmed.is_empty() || sub_trimmed.starts_with('!') {
292                    j += 1;
293                    continue;
294                }
295                let sub_dash_count = sub_trimmed.chars().take_while(|c| *c == '-').count();
296                if sub_dash_count < next_level {
297                    break;
298                }
299                j += 1;
300            }
301
302            let sub_lines = &lines[i + 1..j];
303            let sub_map = parse_map(next_level, sub_lines, start_line + i + 1)?;
304            insert_key(&mut map, &key, HkValue::Map(sub_map))?;
305            i = j;
306        }
307    }
308
309    Ok(map)
310}
311
312/// Insert a key (which may contain dots for nesting) into the map.
313/// Keys that start or end with a dot are treated as literal keys (no nesting).
314fn insert_key(map: &mut IndexMap<String, HkValue>, key: &str, value: HkValue) -> Result<(), HkError> {
315    // If the key contains dots but not at the start or end, split and nest.
316    if key.contains('.') && !key.starts_with('.') && !key.ends_with('.') {
317        let parts: Vec<&str> = key.split('.').collect();
318        insert_nested(map, parts, value)
319    } else {
320        // Otherwise, treat as a single key.
321        if map.contains_key(key) {
322            return Err(HkError::KeyConflict(key.to_string()));
323        }
324        map.insert(key.to_string(), value);
325        Ok(())
326    }
327}
328
329/// Insert a nested key using the split parts.
330fn insert_nested(map: &mut IndexMap<String, HkValue>, keys: Vec<&str>, value: HkValue) -> Result<(), HkError> {
331    let mut current = map;
332    for key in &keys[0..keys.len() - 1] {
333        let entry = current
334            .entry(key.to_string())
335            .or_insert(HkValue::Map(IndexMap::new()));
336        if let HkValue::Map(submap) = entry {
337            current = submap;
338        } else {
339            return Err(HkError::KeyConflict(key.to_string()));
340        }
341    }
342    if let Some(last_key) = keys.last() {
343        current.insert(last_key.to_string(), value);
344    }
345    Ok(())
346}
347
348/// Remove surrounding quotes from a key (if present) and unescape inner quotes.
349fn unquote_key(s: &str) -> String {
350    let s = s.trim();
351    if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
352        let inner = &s[1..s.len() - 1];
353        inner.replace("\\\"", "\"")
354    } else {
355        s.to_string()
356    }
357}
358
359fn parse_value(s: &str, line: usize, column: usize) -> Result<HkValue, HkError> {
360    let s = s.trim();
361    if s.is_empty() {
362        return Err(HkError::Parse {
363            line: line as u32,
364            column,
365            message: "Empty value".to_string(),
366        });
367    }
368
369    // Array
370    if s.starts_with('[') && s.ends_with(']') {
371        let inner = &s[1..s.len() - 1];
372        let mut items = Vec::new();
373        let mut current = String::new();
374        let mut in_quotes = false;
375        let mut escape = false;
376        for c in inner.chars() {
377            if escape {
378                current.push(c);
379                escape = false;
380                continue;
381            }
382            match c {
383                '\\' => escape = true,
384                '"' => in_quotes = !in_quotes,
385                ',' if !in_quotes => {
386                    if !current.trim().is_empty() {
387                        let item = parse_simple_value(current.trim(), line, column)?;
388                        items.push(item);
389                        current.clear();
390                    }
391                }
392                _ => current.push(c),
393            }
394        }
395        if !current.trim().is_empty() {
396            let item = parse_simple_value(current.trim(), line, column)?;
397            items.push(item);
398        }
399        Ok(HkValue::Array(items))
400    } else {
401        parse_simple_value(s, line, column)
402    }
403}
404
405fn parse_simple_value(s: &str, line: usize, column: usize) -> Result<HkValue, HkError> {
406    let s = s.trim();
407    if s.is_empty() {
408        return Err(HkError::Parse {
409            line: line as u32,
410            column,
411            message: "Empty value".to_string(),
412        });
413    }
414
415    // Boolean
416    if s.eq_ignore_ascii_case("true") {
417        return Ok(HkValue::Bool(true));
418    }
419    if s.eq_ignore_ascii_case("false") {
420        return Ok(HkValue::Bool(false));
421    }
422
423    // Number
424    if let Ok(n) = f64::from_str(s) {
425        return Ok(HkValue::Number(n));
426    }
427
428    // Quoted string
429    if s.starts_with('"') && s.ends_with('"') {
430        let inner = &s[1..s.len() - 1];
431        let mut result = String::new();
432        let mut chars = inner.chars();
433        while let Some(c) = chars.next() {
434            if c == '\\' {
435                if let Some(next) = chars.next() {
436                    match next {
437                        'n' => result.push('\n'),
438                        'r' => result.push('\r'),
439                        't' => result.push('\t'),
440                        '"' => result.push('"'),
441                        '\\' => result.push('\\'),
442                        _ => result.push(next),
443                    }
444                }
445            } else {
446                result.push(c);
447            }
448        }
449        Ok(HkValue::String(result))
450    } else {
451        // Plain string
452        Ok(HkValue::String(s.to_string()))
453    }
454}
455
456/// Loads and parses a .hk file from the given path.
457pub fn load_hk_file<P: AsRef<Path>>(path: P) -> Result<HkConfig, HkError> {
458    let file = File::open(path)?;
459    let reader = BufReader::new(file);
460    let mut contents = String::new();
461    for line in reader.lines() {
462        let line = line?;
463        contents.push_str(&line);
464        contents.push('\n');
465    }
466    parse_hk(&contents)
467}
468
469/// Resolves interpolations in the config, including env vars and references.
470pub fn resolve_interpolations(config: &mut HkConfig) -> Result<(), HkError> {
471    let context = config.clone();
472    let mut resolved = HashSet::new();
473    let mut resolving = Vec::new();
474    for (section, value) in config.iter_mut() {
475        if let HkValue::Map(map) = value {
476            resolve_map(map, &context, &mut resolved, &mut resolving, &format!("{}", section))?;
477        }
478    }
479    Ok(())
480}
481
482fn resolve_map(
483    map: &mut IndexMap<String, HkValue>,
484    top: &HkConfig,
485    resolved: &mut HashSet<String>,
486    resolving: &mut Vec<String>,
487    path: &str,
488) -> Result<(), HkError> {
489    for (key, v) in map.iter_mut() {
490        let new_path = format!("{}.{}", path, key);
491        if resolved.contains(&new_path) {
492            continue;
493        }
494        resolving.push(new_path.clone());
495        resolve_value(v, top, resolved, resolving, &new_path)?;
496        resolving.pop();
497        resolved.insert(new_path);
498    }
499    Ok(())
500}
501
502fn resolve_value(
503    v: &mut HkValue,
504    top: &HkConfig,
505    resolved: &mut HashSet<String>,
506    resolving: &mut Vec<String>,
507    path: &str,
508) -> Result<(), HkError> {
509    match v {
510        HkValue::String(s) => {
511            let mut new_s = String::new();
512            let mut last = 0;
513            for cap in INTERPOL_RE.captures_iter(s) {
514                let m = cap.get(0).unwrap();
515                new_s.push_str(&s[last..m.start()]);
516                let var = &cap[1];
517                let repl = if var.starts_with("env:") {
518                    env::var(&var[4..]).unwrap_or_default()
519                } else {
520                    // Resolve the reference recursively, detecting cycles
521                    if resolving.contains(&var.to_string()) {
522                        return Err(HkError::CyclicReference(var.to_string()));
523                    }
524                    resolve_reference(var, top, resolved, resolving)?
525                };
526                new_s.push_str(&repl);
527                last = m.end();
528            }
529            new_s.push_str(&s[last..]);
530            *s = new_s;
531        }
532        HkValue::Array(a) => {
533            for (i, item) in a.iter_mut().enumerate() {
534                resolve_value(item, top, resolved, resolving, &format!("{}[{}]", path, i))?;
535            }
536        }
537        HkValue::Map(m) => {
538            resolve_map(m, top, resolved, resolving, path)?;
539        }
540        _ => {}
541    }
542    Ok(())
543}
544
545fn resolve_reference(
546    path: &str,
547    top: &HkConfig,
548    resolved: &mut HashSet<String>,
549    resolving: &mut Vec<String>,
550) -> Result<String, HkError> {
551    // Check if the reference is already in the resolving stack (cycle)
552    if resolving.contains(&path.to_string()) {
553        return Err(HkError::CyclicReference(path.to_string()));
554    }
555
556    // Get the raw value from the config
557    let raw_value = get_value_by_path(path, top).ok_or_else(|| HkError::InvalidReference(path.to_string()))?;
558    // Clone the value so we can resolve it without affecting the original
559    let mut cloned_value = raw_value.clone();
560
561    // Push the path onto the resolving stack
562    resolving.push(path.to_string());
563
564    // Resolve the cloned value recursively
565    resolve_value(&mut cloned_value, top, resolved, resolving, path)?;
566
567    // Pop the path from the stack
568    resolving.pop();
569
570    // Convert the resolved value to a string
571    cloned_value.as_string()
572}
573
574fn get_value_by_path<'a>(path: &str, config: &'a HkConfig) -> Option<&'a HkValue> {
575    let bracket_re = Regex::new(r"([^\[\].]+)(?:\[(\d+)\])?").unwrap();
576    let mut parts = Vec::new();
577    for cap in bracket_re.captures_iter(path) {
578        let key = cap.get(1).map(|m| m.as_str()).unwrap();
579        let idx = cap.get(2).map(|m| m.as_str().parse::<usize>().ok());
580        parts.push((key, idx.flatten()));
581    }
582
583    if parts.is_empty() {
584        return None;
585    }
586
587    let (first_key, _) = parts[0];
588    let mut current_value: Option<&'a HkValue> = config.get(first_key);
589    for (key, idx) in parts.iter().skip(1) {
590        match current_value {
591            Some(HkValue::Map(map)) => {
592                current_value = map.get(*key);
593            }
594            Some(HkValue::Array(arr)) if idx.is_some() => {
595                if let Some(i) = idx {
596                    if *i < arr.len() {
597                        current_value = Some(&arr[*i]);
598                        continue;
599                    } else {
600                        return None;
601                    }
602                } else {
603                    return None;
604                }
605            }
606            _ => return None,
607        }
608        if let Some(idx) = idx {
609            if let Some(HkValue::Array(arr)) = current_value {
610                if *idx < arr.len() {
611                    current_value = Some(&arr[*idx]);
612                } else {
613                    return None;
614                }
615            } else {
616                return None;
617            }
618        }
619    }
620    current_value
621}
622
623/// Serializes a HkConfig back to a .hk string, preserving key order.
624pub fn serialize_hk(config: &HkConfig) -> String {
625    let mut output = String::new();
626    for (section, value) in config.iter() {
627        output.push_str(&format!("[{}]\n", section));
628        if let HkValue::Map(map) = value {
629            serialize_map(map, 1, &mut output);
630        }
631        output.push('\n');
632    }
633    output.trim_end().to_string()
634}
635
636fn serialize_map(map: &IndexMap<String, HkValue>, level: usize, output: &mut String) {
637    let prefix = "-".repeat(level) + " > ";
638    for (key, value) in map.iter() {
639        match value {
640            HkValue::Map(submap) => {
641                output.push_str(&format!("{}{}\n", prefix, key));
642                serialize_map(submap, level + 1, output);
643            }
644            _ => {
645                let val = serialize_value(value);
646                output.push_str(&format!("{}{} => {}\n", prefix, key, val));
647            }
648        }
649    }
650}
651
652fn serialize_value(value: &HkValue) -> String {
653    match value {
654        HkValue::String(s) => {
655            if s.contains(',') || s.contains(' ') || s.contains(']') || s.contains('"') || s.contains('\n') {
656                format!("\"{}\"", s.replace("\"", "\\\""))
657            } else {
658                s.clone()
659            }
660        }
661        HkValue::Number(n) => n.to_string(),
662        HkValue::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
663        HkValue::Array(a) => format!(
664            "[{}]",
665            a.iter()
666                .map(serialize_value)
667                .collect::<Vec<_>>()
668                .join(", ")
669        ),
670        HkValue::Map(_) => "<map>".to_string(),
671    }
672}
673
674pub fn write_hk_file<P: AsRef<Path>>(path: P, config: &HkConfig) -> io::Result<()> {
675    let mut file = File::create(path)?;
676    file.write_all(serialize_hk(config).as_bytes())
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use pretty_assertions::assert_eq;
683
684    #[test]
685    fn test_parse_libraries_repo() {
686        let input = r#"
687! Repozytorium bibliotek dla Hacker Lang
688
689[libraries]
690-> obsidian
691--> version => 0.2
692--> description => Biblioteka inspirowana zenity.
693--> authors => ["HackerOS Team <hackeros068@gmail.com>"]
694--> so-download => https://github.com/Bytes-Repository/obsidian-lib/releases/download/v0.2/libobsidian_lib.so
695--> .hl-download => https://github.com/Bytes-Repository/obsidian-lib/blob/main/obsidian.hl
696
697-> yuy
698--> version => 0.2
699--> description => Twórz ładne interfejsy cli
700"#;
701        let result = parse_hk(input).expect("Failed to parse libraries file");
702        assert!(result.contains_key("libraries"));
703        let libraries = result["libraries"].as_map().unwrap();
704        assert!(libraries.contains_key("obsidian"));
705        let obsidian = libraries["obsidian"].as_map().unwrap();
706        assert_eq!(obsidian["version"].as_number().unwrap(), 0.2);
707        assert_eq!(obsidian["description"].as_string().unwrap(), "Biblioteka inspirowana zenity.");
708        assert!(obsidian.contains_key("so-download"));
709        assert!(obsidian.contains_key(".hl-download"));
710        assert_eq!(
711            obsidian[".hl-download"].as_string().unwrap(),
712            "https://github.com/Bytes-Repository/obsidian-lib/blob/main/obsidian.hl"
713        );
714
715        assert!(libraries.contains_key("yuy"));
716        let yuy = libraries["yuy"].as_map().unwrap();
717        assert_eq!(yuy["version"].as_number().unwrap(), 0.2);
718    }
719
720    #[test]
721    fn test_parse_hk_with_comments_and_types() {
722        let input = r#"
723        ! Globalne informacje o projekcie
724        [metadata]
725        -> name => Hacker Lang
726        -> version => 1.5
727        -> list => [1, 2.5, true, "four"]
728        "#;
729        let result = parse_hk(input).unwrap();
730        assert!(result.contains_key("metadata"));
731        let metadata = result["metadata"].as_map().unwrap();
732        assert_eq!(metadata["name"].as_string().unwrap(), "Hacker Lang");
733        assert_eq!(metadata["version"].as_number().unwrap(), 1.5);
734        let list = metadata["list"].as_array().unwrap();
735        assert_eq!(list.len(), 4);
736    }
737
738    #[test]
739    fn test_edge_cases() {
740        // Empty section
741        let input = "[empty]\n";
742        let config = parse_hk(input).unwrap();
743        assert!(config.contains_key("empty"));
744        assert_eq!(config["empty"].as_map().unwrap().len(), 0);
745
746        // Section with only comments
747        let input = "[comments]\n! comment\n! another\n";
748        let config = parse_hk(input).unwrap();
749        assert!(config.contains_key("comments"));
750        assert_eq!(config["comments"].as_map().unwrap().len(), 0);
751
752        // Nested map with dots in keys
753        let input = r#"
754[config]
755-> a.b.c => 42
756"#;
757        let config = parse_hk(input).unwrap();
758        let a = config["config"].as_map().unwrap().get("a").unwrap().as_map().unwrap();
759        let b = a.get("b").unwrap().as_map().unwrap();
760        let c = b.get("c").unwrap().as_number().unwrap();
761        assert_eq!(c, 42.0);
762    }
763
764    #[test]
765    fn test_array_reference() {
766        let input = r#"
767[data]
768-> numbers => [10, 20, 30]
769-> first => ${data.numbers[0]}
770"#;
771        let mut config = parse_hk(input).unwrap();
772        resolve_interpolations(&mut config).unwrap();
773        let first = config["data"].as_map().unwrap()["first"].as_string().unwrap();
774        assert_eq!(first, "10");
775    }
776
777    #[test]
778    fn test_cyclic_reference_detection() {
779        let input = r#"
780[a]
781-> b => ${a.c}
782-> c => ${a.b}
783"#;
784        let mut config = parse_hk(input).unwrap();
785        let err = resolve_interpolations(&mut config).unwrap_err();
786        match err {
787            HkError::CyclicReference(path) => {
788                assert!(path.contains("a.b") || path.contains("a.c"));
789            }
790            _ => panic!("Expected cyclic reference error, got {:?}", err),
791        }
792    }
793
794    #[test]
795    fn test_key_conflict() {
796        let input = r#"
797[conflict]
798-> a => 1
799-> a.b => 2
800"#;
801        let result = parse_hk(input);
802        assert!(result.is_err());
803    }
804
805    #[test]
806    fn test_invalid_reference() {
807        let input = r#"
808[a]
809-> b => ${a.missing}
810"#;
811        let mut config = parse_hk(input).unwrap();
812        let err = resolve_interpolations(&mut config).unwrap_err();
813        match err {
814            HkError::InvalidReference(var) => {
815                assert_eq!(var, "a.missing");
816            }
817            _ => panic!("Expected invalid reference error"),
818        }
819    }
820
821    #[test]
822    fn test_serialize_roundtrip() {
823        let input = r#"
824[test]
825-> key => value
826-> array => [1, "two", true]
827-> nested
828--> sub => 42
829"#;
830        let config = parse_hk(input).unwrap();
831        let serialized = serialize_hk(&config);
832        let parsed_again = parse_hk(&serialized).unwrap();
833        assert_eq!(config, parsed_again);
834    }
835}