1use indexmap::IndexMap;
8use lazy_static::lazy_static;
9use nom::{
10 branch::alt,
11 bytes::complete::{tag, take_until, take_while, take_while1},
12 character::complete::{multispace0, multispace1},
13 combinator::{eof, map, opt, peek},
14 error::{context, VerboseError, VerboseErrorKind},
15 multi::{many0, many1, separated_list0},
16 sequence::{delimited, preceded, terminated, tuple},
17 IResult,
18};
19use nom_locate::LocatedSpan;
20use regex::Regex;
21use std::env;
22use std::fs::File;
23use std::io::{self, BufRead, BufReader, Write};
24use std::path::Path;
25use std::str::FromStr;
26use thiserror::Error;
27
28type Span<'a> = LocatedSpan<&'a str>;
29type ParseResult<'a, T> = IResult<Span<'a>, T, VerboseError<Span<'a>>>;
30
31pub type HkConfig = IndexMap<String, HkValue>;
34
35lazy_static! {
36 static ref INTERPOL_RE: Regex = Regex::new(r"\$\{([^}]+)\}").unwrap();
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum HkValue {
42 String(String),
43 Number(f64),
44 Bool(bool),
45 Array(Vec<HkValue>),
46 Map(IndexMap<String, HkValue>),
47}
48
49impl HkValue {
50 pub fn as_string(&self) -> Result<String, HkError> {
51 if let Self::String(s) = self {
52 Ok(s.clone())
53 } else {
54 Err(HkError::TypeMismatch {
55 expected: "string".to_string(),
56 found: format!("{:?}", self),
57 })
58 }
59 }
60
61 pub fn as_number(&self) -> Result<f64, HkError> {
62 if let Self::Number(n) = self {
63 Ok(*n)
64 } else {
65 Err(HkError::TypeMismatch {
66 expected: "number".to_string(),
67 found: format!("{:?}", self),
68 })
69 }
70 }
71
72 pub fn as_bool(&self) -> Result<bool, HkError> {
73 if let Self::Bool(b) = self {
74 Ok(*b)
75 } else {
76 Err(HkError::TypeMismatch {
77 expected: "bool".to_string(),
78 found: format!("{:?}", self),
79 })
80 }
81 }
82
83 pub fn as_array(&self) -> Result<&Vec<HkValue>, HkError> {
84 if let Self::Array(a) = self {
85 Ok(a)
86 } else {
87 Err(HkError::TypeMismatch {
88 expected: "array".to_string(),
89 found: format!("{:?}", self),
90 })
91 }
92 }
93
94 pub fn as_map(&self) -> Result<&IndexMap<String, HkValue>, HkError> {
95 if let Self::Map(m) = self {
96 Ok(m)
97 } else {
98 Err(HkError::TypeMismatch {
99 expected: "map".to_string(),
100 found: format!("{:?}", self),
101 })
102 }
103 }
104}
105
106#[derive(Error, Debug)]
108pub enum HkError {
109 #[error("IO error: {0}")]
110 Io(#[from] io::Error),
111 #[error("Parse error at line {line}, column {column}: {message}")]
112 Parse {
113 line: u32,
114 column: usize,
115 message: String,
116 },
117 #[error("Type mismatch: expected {expected}, found {found}")]
118 TypeMismatch { expected: String, found: String },
119 #[error("Missing field: {0}")]
120 MissingField(String),
121 #[error("Invalid reference: {0}")]
122 InvalidReference(String),
123}
124
125pub fn parse_hk(input: &str) -> Result<HkConfig, HkError> {
127 let input_span = LocatedSpan::new(input);
128 let mut remaining = input_span;
129 let mut config = IndexMap::new();
130 while !remaining.fragment().is_empty() {
131 let (rest, _) = multispace0(remaining).map_err(|e| map_nom_error(input, remaining, e))?;
132 remaining = rest;
133 if remaining.fragment().is_empty() {
134 break;
135 }
136
137 if let Ok((rest, _)) = comment(remaining) {
139 remaining = rest;
140 continue;
141 }
142
143 let (rest, (name, values)) = section(remaining).map_err(|e| map_nom_error(input, remaining, e))?;
144 config.insert(name, HkValue::Map(values));
145 remaining = rest;
146 }
147 Ok(config)
148}
149
150fn map_nom_error(input: &str, span: Span, err: nom::Err<VerboseError<Span>>) -> HkError {
152 let verbose_err = match err {
153 nom::Err::Error(e) | nom::Err::Failure(e) => e,
154 nom::Err::Incomplete(_) => VerboseError { errors: vec![] },
155 };
156
157 let (line, column) = if let Some((s, _)) = verbose_err.errors.first() {
159 (s.location_line(), s.get_column())
160 } else {
161 (span.location_line(), span.get_column())
162 };
163
164 let errors_str: Vec<(&str, VerboseErrorKind)> = verbose_err
166 .errors
167 .iter()
168 .map(|(s, k)| (*s.fragment(), k.clone()))
169 .collect();
170
171 let verbose_err_str = VerboseError { errors: errors_str };
172
173 let message = nom::error::convert_error(input, verbose_err_str);
174
175 HkError::Parse {
176 line,
177 column,
178 message,
179 }
180}
181
182pub fn load_hk_file<P: AsRef<Path>>(path: P) -> Result<HkConfig, HkError> {
184 let file = File::open(path)?;
185 let reader = BufReader::new(file);
186 let mut contents = String::new();
187 for line in reader.lines() {
188 let line = line?;
189 contents.push_str(&line);
190 contents.push('\n');
191 }
192 parse_hk(&contents)
193}
194
195pub fn resolve_interpolations(config: &mut HkConfig) -> Result<(), HkError> {
197 let context = config.clone();
199
200 for (_, value) in config.iter_mut() {
201 if let HkValue::Map(map) = value {
202 resolve_map(map, &context)?;
203 }
204 }
205 Ok(())
206}
207
208fn resolve_map(map: &mut IndexMap<String, HkValue>, top: &HkConfig) -> Result<(), HkError> {
209 for (_, v) in map.iter_mut() {
210 resolve_value(v, top)?;
211 }
212 Ok(())
213}
214
215fn resolve_value(v: &mut HkValue, top: &HkConfig) -> Result<(), HkError> {
216 match v {
217 HkValue::String(s) => {
218 let mut new_s = String::new();
219 let mut last = 0;
220 for cap in INTERPOL_RE.captures_iter(s) {
221 let m = cap.get(0).unwrap();
222 new_s.push_str(&s[last..m.start()]);
223 let var = &cap[1];
224 let repl = if var.starts_with("env:") {
225 env::var(&var[4..]).unwrap_or_default()
226 } else {
227 resolve_path(var, top).ok_or(HkError::InvalidReference(var.to_string()))?
228 };
229 new_s.push_str(&repl);
230 last = m.end();
231 }
232 new_s.push_str(&s[last..]);
233 *s = new_s;
234 }
235 HkValue::Array(a) => {
236 for item in a.iter_mut() {
237 resolve_value(item, top)?;
238 }
239 }
240 HkValue::Map(m) => resolve_map(m, top)?,
241 _ => {}
242 }
243 Ok(())
244}
245
246fn resolve_path(path: &str, config: &HkConfig) -> Option<String> {
247 let parts: Vec<&str> = path.split('.').collect();
248 let mut current: Option<&HkValue> = config.get(parts[0]);
249 for &p in &parts[1..] {
250 current = current.and_then(|v| v.as_map().ok()).and_then(|m| m.get(p));
251 }
252 current.and_then(|v| v.as_string().ok())
253}
254
255pub fn serialize_hk(config: &HkConfig) -> String {
257 let mut output = String::new();
258 for (section, value) in config.iter() {
259 output.push_str(&format!("[{}]\n", section));
260 if let HkValue::Map(map) = value {
261 serialize_map(map, 0, &mut output);
262 }
263 output.push('\n');
264 }
265 output.trim_end().to_string()
266}
267
268fn serialize_map(map: &IndexMap<String, HkValue>, indent: usize, output: &mut String) {
269 let spaces = " ".repeat(indent);
270 for (key, value) in map.iter() {
271 match value {
272 HkValue::Map(submap) => {
273 output.push_str(&format!("{}-> {}\n", spaces, key));
274 serialize_map(submap, indent + 1, output);
275 }
276 _ => {
277 output.push_str(&format!("{}-> {} => {}\n", spaces, key, serialize_value(value)));
278 }
279 }
280 }
281}
282
283fn serialize_value(value: &HkValue) -> String {
284 match value {
285 HkValue::String(s) => {
286 if s.contains(',') || s.contains(' ') || s.contains(']') || s.contains('"') {
287 format!("\"{}\"", s.replace("\"", "\\\""))
288 } else {
289 s.clone()
290 }
291 }
292 HkValue::Number(n) => n.to_string(),
293 HkValue::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
294 HkValue::Array(a) => format!(
295 "[{}]",
296 a.iter()
297 .map(serialize_value)
298 .collect::<Vec<_>>()
299 .join(", ")
300 ),
301 HkValue::Map(_) => "<map>".to_string(), }
303}
304
305pub fn write_hk_file<P: AsRef<Path>>(path: P, config: &HkConfig) -> io::Result<()> {
307 let mut file = File::create(path)?;
308 file.write_all(serialize_hk(config).as_bytes())
309}
310
311fn comment(input: Span) -> ParseResult<Span> {
313 context(
314 "comment",
315 delimited(tag("!"), take_while(|c| c != '\r' && c != '\n'), opt(tag("\n"))),
316 )(input)
317}
318
319fn section(input: Span) -> ParseResult<(String, IndexMap<String, HkValue>)> {
320 context(
321 "section",
322 map(
323 tuple((
324 delimited(tag("["), take_until("]"), tag("]")),
325 multispace0,
326 terminated(
327 many0(alt((
328 map(comment, |_| None),
329 map(key_value, Some),
330 map(nested_key_value, Some),
331 ))),
332 tuple((multispace0, peek(alt((tag("["), map(eof, |_| Span::new(""))))))),
333 ),
334 )),
335 |(name, _, opt_pairs)| {
336 let mut map = IndexMap::new();
337 for pair_opt in opt_pairs {
338 if let Some((key, value)) = pair_opt {
339 insert_nested(&mut map, key.split('.').collect::<Vec<_>>(), value);
340 }
341 }
342 (name.fragment().trim().to_string(), map)
343 },
344 ),
345 )(input)
346}
347
348fn insert_nested(map: &mut IndexMap<String, HkValue>, keys: Vec<&str>, value: HkValue) {
350 let mut current = map;
351 for key in &keys[0..keys.len() - 1] {
352 let entry = current
353 .entry(key.to_string())
354 .or_insert(HkValue::Map(IndexMap::new()));
355 if let HkValue::Map(submap) = entry {
356 current = submap;
357 } else {
358 panic!("Invalid nesting");
359 }
360 }
361 if let Some(last_key) = keys.last() {
362 current.insert(last_key.to_string(), value);
363 }
364}
365
366fn key_value(input: Span) -> ParseResult<(String, HkValue)> {
367 context(
368 "key_value",
369 map(
370 tuple((
371 preceded(
372 tuple((multispace0, tag("->"), multispace1)),
373 take_while1(|c: char| c.is_alphanumeric() || c == '_'),
374 ),
375 multispace0,
376 tag("=>"),
377 line_value,
378 )),
379 |(key, _, _, value)| (key.fragment().trim().to_string(), value),
380 ),
381 )(input)
382}
383
384fn nested_key_value(input: Span) -> ParseResult<(String, HkValue)> {
385 context(
386 "nested_key_value",
387 map(
388 tuple((
389 preceded(
390 tuple((multispace0, tag("->"), multispace1)),
391 take_while1(|c: char| c.is_alphanumeric() || c == '_'),
392 ),
393 many1(sub_key_value),
394 )),
395 |(key, sub_pairs)| {
396 let mut sub_map = IndexMap::new();
397 for (sub_key, sub_value) in sub_pairs {
398 sub_map.insert(sub_key, sub_value);
399 }
400 (key.fragment().trim().to_string(), HkValue::Map(sub_map))
401 },
402 ),
403 )(input)
404}
405
406fn sub_key_value(input: Span) -> ParseResult<(String, HkValue)> {
407 context(
408 "sub_key_value",
409 map(
410 tuple((
411 preceded(
412 tuple((multispace1, tag("-->"), multispace1)),
413 take_while1(|c: char| c.is_alphanumeric() || c == '_'),
414 ),
415 multispace0,
416 tag("=>"),
417 line_value,
418 )),
419 |(sub_key, _, _, sub_value)| (sub_key.fragment().trim().to_string(), sub_value),
420 ),
421 )(input)
422}
423
424fn line_value(input: Span) -> ParseResult<HkValue> {
425 preceded(
426 multispace0,
427 alt((
428 map(array, HkValue::Array),
429 map(
430 terminated(take_while(|c| c != '\r' && c != '\n'), opt(tag("\n"))),
431 |s: Span| parse_simple(s.fragment()),
432 ),
433 )),
434 )(input)
435}
436
437fn parse_simple(s: &str) -> HkValue {
438 let s = s.trim();
439 if s.eq_ignore_ascii_case("true") {
440 HkValue::Bool(true)
441 } else if s.eq_ignore_ascii_case("false") {
442 HkValue::Bool(false)
443 } else if let Ok(n) = f64::from_str(s) {
444 HkValue::Number(n)
445 } else {
446 HkValue::String(s.to_string())
447 }
448}
449
450fn array(input: Span) -> ParseResult<Vec<HkValue>> {
451 delimited(
452 tag("["),
453 separated_list0(tuple((multispace0, tag(","), multispace0)), item_value),
454 tag("]"),
455 )(input)
456 .map(|(i, v)| (i, v))
457}
458
459fn item_value(input: Span) -> ParseResult<HkValue> {
460 alt((
461 map(array, HkValue::Array),
462 map(double_quoted, |s| HkValue::String(s.fragment().to_string())),
463 map(
464 take_while1(|c: char| !c.is_whitespace() && c != ',' && c != ']'),
465 |s: Span| parse_simple(s.fragment()),
466 ),
467 ))(input)
468}
469
470fn double_quoted(input: Span) -> ParseResult<Span> {
471 delimited(tag("\""), take_while(|c| c != '"'), tag("\""))(input)
472}
473
474pub trait FromHkValue: Sized {
475 fn from_hk_value(value: &HkValue) -> Result<Self, HkError>;
476}
477
478impl FromHkValue for String {
479 fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
480 value.as_string()
481 }
482}
483
484impl FromHkValue for f64 {
485 fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
486 value.as_number()
487 }
488}
489
490impl FromHkValue for bool {
491 fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
492 value.as_bool()
493 }
494}
495
496impl<T: FromHkValue> FromHkValue for Vec<T> {
497 fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
498 value
499 .as_array()?
500 .iter()
501 .map(|v| T::from_hk_value(v))
502 .collect()
503 }
504}
505
506impl<T: FromHkValue> FromHkValue for Option<T> {
507 fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
508 Ok(Some(T::from_hk_value(value)?))
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use pretty_assertions::assert_eq;
516
517 #[test]
518 fn test_parse_hk_with_comments_and_types() {
519 let input = r#"
520 ! Globalne informacje o projekcie
521 [metadata]
522 -> name => Hacker Lang
523 -> version => 1.5
524 -> authors => HackerOS Team <hackeros068@gmail.com>
525 -> license => MIT
526 -> is_active => true
527 -> pi => 3.14
528 -> list => [1, 2.5, true, "four"]
529 [description]
530 -> summary => Programing language for HackerOS.
531 -> long => Język programowania Hacker Lang z plikami konfiguracyjnymi .hk lub .hacker lub skryptami itd. .hl.
532 [specs]
533 -> rust => >= 1.92.0
534 -> dependencies
535 --> odin => >= 2026-01
536 --> c => C23
537 --> crystal => 1.19.0
538 --> python => 3.13
539 "#;
540 let result = parse_hk(input).unwrap();
541 assert_eq!(result.len(), 3);
542
543 if let Some(HkValue::Map(metadata)) = result.get("metadata") {
544 assert_eq!(metadata.len(), 7);
545 assert_eq!(
546 metadata.get("name"),
547 Some(&HkValue::String("Hacker Lang".to_string()))
548 );
549 assert_eq!(
550 metadata.get("version"),
551 Some(&HkValue::Number(1.5))
552 );
553 assert_eq!(
554 metadata.get("authors"),
555 Some(&HkValue::String("HackerOS Team <hackeros068@gmail.com>".to_string()))
556 );
557 assert_eq!(
558 metadata.get("license"),
559 Some(&HkValue::String("MIT".to_string()))
560 );
561 assert_eq!(
562 metadata.get("is_active"),
563 Some(&HkValue::Bool(true))
564 );
565 assert_eq!(
566 metadata.get("pi"),
567 Some(&HkValue::Number(3.14))
568 );
569 assert_eq!(
570 metadata.get("list"),
571 Some(&HkValue::Array(vec![
572 HkValue::Number(1.0),
573 HkValue::Number(2.5),
574 HkValue::Bool(true),
575 HkValue::String("four".to_string()),
576 ]))
577 );
578 }
579 }
580
581 #[test]
582 fn test_serialize_hk() {
583 let mut config = IndexMap::new();
584 let mut metadata = IndexMap::new();
585 metadata.insert("name".to_string(), HkValue::String("Hacker Lang".to_string()));
586 metadata.insert("version".to_string(), HkValue::Number(1.5));
587 config.insert("metadata".to_string(), HkValue::Map(metadata));
588 let serialized = serialize_hk(&config);
589 assert!(serialized.contains("[metadata]"));
590 assert!(serialized.contains("-> name => \"Hacker Lang\""));
592 assert!(serialized.contains("-> version => 1.5"));
593 }
594
595 #[test]
596 fn test_error_handling() {
597 let invalid_input = r#"
598 [metadata]
599 -> name = Hacker Lang # Missing =>
600 "#;
601 let err = parse_hk(invalid_input).unwrap_err();
602 if let HkError::Parse { line, message, .. } = err {
603 assert_eq!(line, 3);
604 assert!(!message.is_empty());
606 } else {
607 panic!("Unexpected error type");
608 }
609 }
610
611 #[test]
612 fn test_interpolation() {
613 let mut config = IndexMap::new();
614 let mut metadata = IndexMap::new();
615 metadata.insert("name".to_string(), HkValue::String("Hacker Lang".to_string()));
616 let mut path = IndexMap::new();
617 path.insert("bin".to_string(), HkValue::String("${metadata.name}/bin".to_string()));
618 config.insert("metadata".to_string(), HkValue::Map(metadata));
619 config.insert("path".to_string(), HkValue::Map(path));
620 resolve_interpolations(&mut config).unwrap();
621 if let Some(HkValue::Map(p)) = config.get("path") {
622 if let Some(HkValue::String(s)) = p.get("bin") {
623 assert_eq!(s, "Hacker Lang/bin");
624 }
625 }
626 }
627}