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> {
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#[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
130pub 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 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
154fn 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
190pub 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
203pub 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
262pub 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
317fn 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 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 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 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 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 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
495pub 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 if let Some(HkValue::Map(obsidian)) = libraries.get("obsidian") {
562 assert_eq!(obsidian.get("version"), Some(&HkValue::Number(0.2)));
564 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 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}