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
13pub type HkConfig = IndexMap<String, HkValue>;
16
17lazy_static! {
18 static ref INTERPOL_RE: Regex = Regex::new(r"\$\{([^}]+)\}").unwrap();
19}
20
21#[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#[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
156pub 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 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
210fn 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 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 break;
237 }
238
239 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 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 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 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
312fn insert_key(map: &mut IndexMap<String, HkValue>, key: &str, value: HkValue) -> Result<(), HkError> {
315 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 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
329fn 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
348fn 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 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 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 if let Ok(n) = f64::from_str(s) {
425 return Ok(HkValue::Number(n));
426 }
427
428 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 Ok(HkValue::String(s.to_string()))
453 }
454}
455
456pub 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
469pub 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 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 if resolving.contains(&path.to_string()) {
553 return Err(HkError::CyclicReference(path.to_string()));
554 }
555
556 let raw_value = get_value_by_path(path, top).ok_or_else(|| HkError::InvalidReference(path.to_string()))?;
558 let mut cloned_value = raw_value.clone();
560
561 resolving.push(path.to_string());
563
564 resolve_value(&mut cloned_value, top, resolved, resolving, path)?;
566
567 resolving.pop();
569
570 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
623pub 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 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 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 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}