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