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    /// Returns the value as a String.
51    /// 
52    /// FIX: Automatically converts Numbers and Bools to their string representation
53    /// instead of returning a TypeMismatch error. This handles cases like 'version => 0.2'.
54    pub fn as_string(&self) -> Result<String, HkError> {
55        match self {
56            Self::String(s) => Ok(s.clone()),
57            Self::Number(n) => Ok(n.to_string()),
58            Self::Bool(b) => Ok(b.to_string()),
59            _ => Err(HkError::TypeMismatch {
60                expected: "string".to_string(),
61                found: format!("{:?}", self),
62            }),
63        }
64    }
65
66    pub fn as_number(&self) -> Result<f64, HkError> {
67        if let Self::Number(n) = self {
68            Ok(*n)
69        } else {
70            Err(HkError::TypeMismatch {
71                expected: "number".to_string(),
72                found: format!("{:?}", self),
73            })
74        }
75    }
76
77    pub fn as_bool(&self) -> Result<bool, HkError> {
78        if let Self::Bool(b) = self {
79            Ok(*b)
80        } else {
81            Err(HkError::TypeMismatch {
82                expected: "bool".to_string(),
83                found: format!("{:?}", self),
84            })
85        }
86    }
87
88    pub fn as_array(&self) -> Result<&Vec<HkValue>, HkError> {
89        if let Self::Array(a) = self {
90            Ok(a)
91        } else {
92            Err(HkError::TypeMismatch {
93                expected: "array".to_string(),
94                found: format!("{:?}", self),
95            })
96        }
97    }
98
99    pub fn as_map(&self) -> Result<&IndexMap<String, HkValue>, HkError> {
100        if let Self::Map(m) = self {
101            Ok(m)
102        } else {
103            Err(HkError::TypeMismatch {
104                expected: "map".to_string(),
105                found: format!("{:?}", self),
106            })
107        }
108    }
109}
110
111/// Custom error type for parsing .hk files.
112#[derive(Error, Debug)]
113pub enum HkError {
114    #[error("IO error: {0}")]
115    Io(#[from] io::Error),
116    #[error("Parse error at line {line}, column {column}: {message}")]
117    Parse {
118        line: u32,
119        column: usize,
120        message: String,
121    },
122    #[error("Type mismatch: expected {expected}, found {found}")]
123    TypeMismatch { expected: String, found: String },
124    #[error("Missing field: {0}")]
125    MissingField(String),
126    #[error("Invalid reference: {0}")]
127    InvalidReference(String),
128}
129
130/// Parses a .hk file from a string input.
131pub fn parse_hk(input: &str) -> Result<HkConfig, HkError> {
132    let input_span = LocatedSpan::new(input);
133    let mut remaining = input_span;
134    let mut config = IndexMap::new();
135
136    while !remaining.fragment().is_empty() {
137        // Czyszczenie białych znaków i komentarzy przed parsowaniem sekcji
138        let (rest, _) = many0(alt((
139            multispace1,
140            map(comment, |_| Span::new("")) 
141        )))(remaining).map_err(|e| map_nom_error(input, remaining, e))?;
142        
143        remaining = rest;
144        if remaining.fragment().is_empty() { break; }
145
146        let (rest, (name, values)) = section(remaining).map_err(|e| map_nom_error(input, remaining, e))?;
147        config.insert(name, HkValue::Map(values));
148        remaining = rest;
149    }
150
151    Ok(config)
152}
153
154/// Helper to map nom error to HkError.
155fn map_nom_error(input: &str, span: Span, err: nom::Err<VerboseError<Span>>) -> HkError {
156    let verbose_err = match err {
157        nom::Err::Error(e) | nom::Err::Failure(e) => e,
158        nom::Err::Incomplete(_) => VerboseError { errors: vec![] },
159    };
160   
161    let (line, column) = if let Some((s, _)) = verbose_err.errors.first() {
162        (s.location_line(), s.get_column())
163    } else {
164        (span.location_line(), span.get_column())
165    };
166
167    let errors_str: Vec<(&str, VerboseErrorKind)> = verbose_err
168        .errors
169        .iter()
170        .map(|(s, k)| (*s.fragment(), k.clone()))
171        .collect();
172    let verbose_err_str = VerboseError { errors: errors_str };
173    let mut message = nom::error::convert_error(input, verbose_err_str);
174
175    if message.contains("tag \"=>\"") {
176        message.push_str("\nHint: Upewnij się, że po kluczu znajduje się '=>' przed wartością.");
177    } else if message.contains("tag \"[\"") {
178        message.push_str("\nHint: Sprawdź, czy sekcje zaczynają się od '[' i kończą ']'.");
179    } else if message.contains("take_while1") {
180        message.push_str("\nHint: Klucze mogą zawierać tylko litery, cyfry, '_', '-' i '.'.");
181    }
182
183    HkError::Parse {
184        line,
185        column,
186        message,
187    }
188}
189
190/// Loads and parses a .hk file from the given path.
191pub fn load_hk_file<P: AsRef<Path>>(path: P) -> Result<HkConfig, HkError> {
192    let file = File::open(path)?;
193    let reader = BufReader::new(file);
194    let mut contents = String::new();
195    for line in reader.lines() {
196        let line = line?;
197        contents.push_str(&line);
198        contents.push('\n');
199    }
200    parse_hk(&contents)
201}
202
203/// Resolves interpolations in the config, including env vars and references.
204pub fn resolve_interpolations(config: &mut HkConfig) -> Result<(), HkError> {
205    let context = config.clone();
206   
207    for (_, value) in config.iter_mut() {
208        if let HkValue::Map(map) = value {
209            resolve_map(map, &context)?;
210        }
211    }
212    Ok(())
213}
214
215fn resolve_map(map: &mut IndexMap<String, HkValue>, top: &HkConfig) -> Result<(), HkError> {
216    for (_, v) in map.iter_mut() {
217        resolve_value(v, top)?;
218    }
219    Ok(())
220}
221
222fn resolve_value(v: &mut HkValue, top: &HkConfig) -> Result<(), HkError> {
223    match v {
224        HkValue::String(s) => {
225            let mut new_s = String::new();
226            let mut last = 0;
227            for cap in INTERPOL_RE.captures_iter(s) {
228                let m = cap.get(0).unwrap();
229                new_s.push_str(&s[last..m.start()]);
230                let var = &cap[1];
231                let repl = if var.starts_with("env:") {
232                    env::var(&var[4..]).unwrap_or_default()
233                } else {
234                    resolve_path(var, top).ok_or(HkError::InvalidReference(var.to_string()))?
235                };
236                new_s.push_str(&repl);
237                last = m.end();
238            }
239            new_s.push_str(&s[last..]);
240            *s = new_s;
241        }
242        HkValue::Array(a) => {
243            for item in a.iter_mut() {
244                resolve_value(item, top)?;
245            }
246        }
247        HkValue::Map(m) => resolve_map(m, top)?,
248        _ => {}
249    }
250    Ok(())
251}
252
253fn resolve_path(path: &str, config: &HkConfig) -> Option<String> {
254    let parts: Vec<&str> = path.split('.').collect();
255    let mut current: Option<&HkValue> = config.get(parts[0]);
256    for &p in &parts[1..] {
257        current = current.and_then(|v| v.as_map().ok()).and_then(|m| m.get(p));
258    }
259    current.and_then(|v| v.as_string().ok())
260}
261
262/// Serializes a HkConfig back to a .hk string, preserving key order.
263pub fn serialize_hk(config: &HkConfig) -> String {
264    let mut output = String::new();
265    for (section, value) in config.iter() {
266        output.push_str(&format!("[{}]\n", section));
267        if let HkValue::Map(map) = value {
268            serialize_map(map, 0, &mut output);
269        }
270        output.push('\n');
271    }
272    output.trim_end().to_string()
273}
274
275fn serialize_map(map: &IndexMap<String, HkValue>, indent: usize, output: &mut String) {
276    let spaces = " ".repeat(indent);
277    for (key, value) in map.iter() {
278        match value {
279            HkValue::Map(submap) => {
280                output.push_str(&format!("{}-> {}\n", spaces, key));
281                serialize_map(submap, indent + 1, output);
282            }
283            _ => {
284                output.push_str(&format!("{}-> {} => {}\n", spaces, key, serialize_value(value)));
285            }
286        }
287    }
288}
289
290fn serialize_value(value: &HkValue) -> String {
291    match value {
292        HkValue::String(s) => {
293            if s.contains(',') || s.contains(' ') || s.contains(']') || s.contains('"') {
294                format!("\"{}\"", s.replace("\"", "\\\""))
295            } else {
296                s.clone()
297            }
298        }
299        HkValue::Number(n) => n.to_string(),
300        HkValue::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
301        HkValue::Array(a) => format!(
302            "[{}]",
303            a.iter()
304                .map(serialize_value)
305                .collect::<Vec<_>>()
306                .join(", ")
307        ),
308        HkValue::Map(_) => "<map>".to_string(), 
309    }
310}
311
312pub fn write_hk_file<P: AsRef<Path>>(path: P, config: &HkConfig) -> io::Result<()> {
313    let mut file = File::create(path)?;
314    file.write_all(serialize_hk(config).as_bytes())
315}
316
317// --- Parser Combinators ---
318
319// Helper to define allowed characters in keys: alphanumeric, _, -, .
320fn is_key_char(c: char) -> bool {
321    c.is_alphanumeric() || c == '_' || c == '-' || c == '.'
322}
323
324fn comment(input: Span) -> ParseResult<Span> {
325    context(
326        "comment",
327        delimited(tag("!"), take_while(|c| c != '\r' && c != '\n'), opt(tag("\n"))),
328    )(input)
329}
330
331fn section(input: Span) -> ParseResult<(String, IndexMap<String, HkValue>)> {
332    context(
333        "section",
334        map(
335            tuple((
336                delimited(tag("["), take_until("]"), tag("]")),
337                multispace0,
338                terminated(
339                    many0(alt((
340                        map(comment, |_| None),
341                        map(key_value, Some),
342                        map(nested_key_value, Some),
343                    ))),
344                    // Using multispace0 ensures we consume trailing newlines before EOF or next section
345                    tuple((multispace0, peek(alt((tag("["), map(eof, |_| Span::new(""))))))),
346                ),
347            )),
348            |(name, _, opt_pairs)| {
349                let mut map = IndexMap::new();
350                for pair_opt in opt_pairs {
351                    if let Some((key, value)) = pair_opt {
352                        insert_nested(&mut map, key.split('.').collect::<Vec<_>>(), value);
353                    }
354                }
355                (name.fragment().trim().to_string(), map)
356            },
357        ),
358    )(input)
359}
360
361fn insert_nested(map: &mut IndexMap<String, HkValue>, keys: Vec<&str>, value: HkValue) {
362    let mut current = map;
363    for key in &keys[0..keys.len() - 1] {
364        let entry = current
365            .entry(key.to_string())
366            .or_insert(HkValue::Map(IndexMap::new()));
367        if let HkValue::Map(submap) = entry {
368            current = submap;
369        } else {
370            // In a robust system, this might return an error rather than panic
371            panic!("Invalid nesting: key conflict"); 
372        }
373    }
374    if let Some(last_key) = keys.last() {
375        current.insert(last_key.to_string(), value);
376    }
377}
378
379fn key_value(input: Span) -> ParseResult<(String, HkValue)> {
380    context(
381        "key_value",
382        map(
383            tuple((
384                preceded(
385                    tuple((multispace0, tag("->"), multispace1)),
386                    take_while1(is_key_char),
387                ),
388                multispace0,
389                tag("=>"),
390                line_value,
391            )),
392            |(key, _, _, value)| (key.fragment().trim().to_string(), value),
393        ),
394    )(input)
395}
396
397fn nested_key_value(input: Span) -> ParseResult<(String, HkValue)> {
398    context(
399        "nested_key_value",
400        map(
401            tuple((
402                preceded(
403                    // multispace0 here allows for "compressed" lists or standard spacing
404                    tuple((multispace0, tag("->"), multispace1)),
405                    take_while1(is_key_char),
406                ),
407                many1(sub_key_value),
408            )),
409            |(key, sub_pairs)| {
410                let mut sub_map = IndexMap::new();
411                for (sub_key, sub_value) in sub_pairs {
412                    sub_map.insert(sub_key, sub_value);
413                }
414                (key.fragment().trim().to_string(), HkValue::Map(sub_map))
415            },
416        ),
417    )(input)
418}
419
420fn sub_key_value(input: Span) -> ParseResult<(String, HkValue)> {
421    context(
422        "sub_key_value",
423        map(
424            tuple((
425                preceded(
426                    // FIX: Changed multispace1 to multispace0. 
427                    // line_value consumes the newline. If there is no indentation, 
428                    // multispace1 fails because there is no whitespace left.
429                    tuple((multispace0, tag("-->"), multispace1)),
430                    take_while1(is_key_char),
431                ),
432                multispace0,
433                tag("=>"),
434                line_value,
435            )),
436            |(sub_key, _, _, sub_value)| (sub_key.fragment().trim().to_string(), sub_value),
437        ),
438    )(input)
439}
440
441fn line_value(input: Span) -> ParseResult<HkValue> {
442    preceded(
443        multispace0,
444        alt((
445            map(array, HkValue::Array),
446            map(
447                // Consumes until newline, and optionally consumes the newline itself
448                terminated(
449                    take_while(|c| c != '\r' && c != '\n'), 
450                    opt(tag("\n"))
451                ),
452                |s: Span| parse_simple(s.fragment()),
453            ),
454        )),
455    )(input)
456}
457
458fn parse_simple(s: &str) -> HkValue {
459    let s = s.trim();
460    if s.eq_ignore_ascii_case("true") {
461        HkValue::Bool(true)
462    } else if s.eq_ignore_ascii_case("false") {
463        HkValue::Bool(false)
464    } else if let Ok(n) = f64::from_str(s) {
465        HkValue::Number(n)
466    } else {
467        HkValue::String(s.to_string())
468    }
469}
470
471fn array(input: Span) -> ParseResult<Vec<HkValue>> {
472    delimited(
473        tag("["),
474        separated_list0(tuple((multispace0, tag(","), multispace0)), item_value),
475        tag("]"),
476    )(input)
477    .map(|(i, v)| (i, v))
478}
479
480fn item_value(input: Span) -> ParseResult<HkValue> {
481    alt((
482        map(array, HkValue::Array),
483        map(double_quoted, |s| HkValue::String(s.fragment().to_string())),
484        map(
485            take_while1(|c: char| !c.is_whitespace() && c != ',' && c != ']'),
486            |s: Span| parse_simple(s.fragment()),
487        ),
488    ))(input)
489}
490
491fn double_quoted(input: Span) -> ParseResult<Span> {
492    delimited(tag("\""), take_while(|c| c != '"'), tag("\""))(input)
493}
494
495// --- Type Conversion Traits ---
496
497pub trait FromHkValue: Sized {
498    fn from_hk_value(value: &HkValue) -> Result<Self, HkError>;
499}
500
501impl FromHkValue for String {
502    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
503        value.as_string()
504    }
505}
506
507impl FromHkValue for f64 {
508    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
509        value.as_number()
510    }
511}
512
513impl FromHkValue for bool {
514    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
515        value.as_bool()
516    }
517}
518
519impl<T: FromHkValue> FromHkValue for Vec<T> {
520    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
521        value
522            .as_array()?
523            .iter()
524            .map(|v| T::from_hk_value(v))
525            .collect()
526    }
527}
528
529impl<T: FromHkValue> FromHkValue for Option<T> {
530    fn from_hk_value(value: &HkValue) -> Result<Self, HkError> {
531        Ok(Some(T::from_hk_value(value)?))
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use pretty_assertions::assert_eq;
539
540    #[test]
541    fn test_parse_libraries_repo() {
542        let input = r#"
543! Repozytorium bibliotek dla Hacker Lang
544
545[libraries]
546-> obsidian
547--> version => 0.2
548--> description => Biblioteka inspirowana zenity.
549--> authors => ["HackerOS Team <hackeros068@gmail.com>"]
550--> so-download => https://github.com/Bytes-Repository/obsidian-lib/releases/download/v0.2/libobsidian_lib.so
551--> .hl-download => https://github.com/Bytes-Repository/obsidian-lib/blob/main/obsidian.hl
552
553-> yuy
554--> version => 0.2
555--> description => Twórz ładne interfejsy cli
556"#;
557        let result = parse_hk(input).expect("Failed to parse libraries file");
558        
559        if let Some(HkValue::Map(libraries)) = result.get("libraries") {
560            // Check obsidian
561            if let Some(HkValue::Map(obsidian)) = libraries.get("obsidian") {
562                // Internal representation is Number
563                assert_eq!(obsidian.get("version"), Some(&HkValue::Number(0.2)));
564                // But as_string() should convert it gracefully now
565                assert_eq!(obsidian.get("version").unwrap().as_string().unwrap(), "0.2");
566                
567                assert_eq!(obsidian.get("description").unwrap().as_string().unwrap(), "Biblioteka inspirowana zenity.");
568                assert!(obsidian.contains_key("so-download"));
569                assert!(obsidian.contains_key(".hl-download"));
570            } else {
571                panic!("Missing obsidian key");
572            }
573
574            // Check yuy
575             if let Some(HkValue::Map(yuy)) = libraries.get("yuy") {
576                assert_eq!(yuy.get("version"), Some(&HkValue::Number(0.2)));
577            } else {
578                panic!("Missing yuy key");
579            }
580        } else {
581            panic!("Missing libraries section");
582        }
583    }
584
585    #[test]
586    fn test_parse_hk_with_comments_and_types() {
587        let input = r#"
588        ! Globalne informacje o projekcie
589        [metadata]
590        -> name => Hacker Lang
591        -> version => 1.5
592        -> list => [1, 2.5, true, "four"]
593        "#;
594        let result = parse_hk(input).unwrap();
595        assert!(result.contains_key("metadata"));
596    }
597}