Skip to main content

hk_parser/
lib.rs

1// src/lib.rs
2//! Hacker Lang Configuration Parser
3//!
4//! This crate provides a robust parser and serializer for .hk files used in Hacker Lang.
5//! It supports nested structures, comments, and error handling.
6
7use 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
31/// Represents the structure of a .hk file.
32/// Sections are top-level keys in the outer IndexMap to preserve order.
33pub type HkConfig = IndexMap<String, HkValue>;
34
35lazy_static! {
36    static ref INTERPOL_RE: Regex = Regex::new(r"\$\{([^}]+)\}").unwrap();
37}
38
39/// Enum for values in the .hk config: supports strings, numbers, booleans, arrays, and maps.
40#[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/// Custom error type for parsing .hk files.
107#[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
125/// Parses a .hk file from a string input.
126pub 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        // Try parsing a comment. If successful, skip to next iteration.
138        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
150/// Helper to map nom error to HkError.
151fn 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    // Use the first error location for line/column
158    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    // Convert VerboseError<LocatedSpan<&str>> to VerboseError<&str>
165    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
182/// Loads and parses a .hk file from the given path.
183pub 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
195/// Resolves interpolations in the config, including env vars and references.
196pub fn resolve_interpolations(config: &mut HkConfig) -> Result<(), HkError> {
197    // Clone the config to use as a read-only context while we mutate the original
198    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
255/// Serializes a HkConfig back to a .hk string, preserving key order.
256pub 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(), // Maps are serialized as nested, not inline
302    }
303}
304
305/// Writes a HkConfig to a file.
306pub 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
311// Parser combinators
312fn 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
348/// Inserts a value into a nested map using dot-separated keys.
349fn 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        // Serializer adds quotes to strings containing spaces
591        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            // Relaxed check as error message might vary depending on nom version and verbosity
605            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}