use crate::config::{Config, Section, Entry}; use std::error::Error; use std::fmt::{Display, Formatter}; #[derive(Debug)] pub struct ParserError { pub filename: String, pub line_no: usize, pub description: String, } impl Error for ParserError {} impl Display for ParserError { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "Failed to parse Unit config: {} in file {} at line {}", self.description, self.filename, self.line_no) } } pub(crate) fn parse_config(config: &mut Config, filename: String, contents: String) -> Result<(), ParserError> { let file_id = config.add_file(filename.clone()); let mut current_entry: Option<(&str, String)> = None; let mut current_section: Option<&mut Section> = None; let mut line_no = 0; for line in contents.lines() { line_no += 1; let line_trimmed = line.trim(); // We can ignore comments or empty lines if line_trimmed == "" || line_trimmed.starts_with("#") || line_trimmed.starts_with(";") { continue; } // Start of group if line_trimmed.starts_with('[') { if let Some(x) = line_trimmed.rfind(']') { let name = &line_trimmed[1..x]; current_section = Some( config .sections .entry(name.to_string()) .or_insert_with(|| Section::new(name.to_string())) ); } continue; } if let Some(section) = &mut current_section { let (key, value) = if let Some((key, value)) = ¤t_entry { (*key, value.clone() + line_trimmed) } else { let split: Vec<&str> = line_trimmed.splitn(2, "=").collect(); if split.len() < 2 { return Err(ParserError { filename, line_no, description: "Line didn't have Key=Value syntax".to_string(), }); } (split.get(0).unwrap().trim(), split.get(1).unwrap().trim().to_string()) }; if value.ends_with("\\") { let value = value[..value.len() - 1].to_string(); current_entry = Some((key, value + " ")) } else { section .entries .entry(key.to_string()) .or_insert_with(|| Entry::new(key.to_string())) .add(&value, file_id) } } else { return Err(ParserError { filename, line_no, description: "Config should start with section header".to_string(), }); } } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::time::FiniteTimeSpan; use crate::time::TimeSpan::{Finite, Infinite}; #[test] fn test_parse() { let mut config = Config::default(); let res = parse_config(&mut config, "test.unit".to_string(), r" [Unit] Hello=no Hello= Hello=Hell Hello=World Jump=Oh no\ 1336".to_string()); assert!(res.is_ok()); assert!(config.sections.get("Unit").is_some()); let section = config.sections.get("Unit").unwrap(); let hello_entry = section.entry("Hello").unwrap(); assert_eq!(hello_entry.get_string(), "World"); assert_eq!(hello_entry.get_list(), vec!["Hell", "World"]); assert_eq!(section.entry("Jump").unwrap().get_string(), "Oh no 1336") } #[test] fn test_config_without_section() { let mut config = Config::default(); let err = parse_config(&mut config, "test.unit".to_string(), r"Hello=World".to_string()); assert!(err.is_err()); let err = err.unwrap_err(); assert_eq!(err.line_no, 1); assert_eq!(err.description, "Config should start with section header") } #[test] fn test_booleans() { let mut config = Config::default(); let res = parse_config(&mut config, "test.unit".to_string(), r" [Unit] 1=no 2=off 3=0 4=false 5=yes 6=on 7=1 8=true 9=x".to_string()); assert!(res.is_ok()); assert!(config.sections.get("Unit").is_some()); let unit = config.sections.get("Unit").unwrap(); assert_eq!(unit.get_boolean("1"), Some(false)); assert_eq!(unit.get_boolean("2"), Some(false)); assert_eq!(unit.get_boolean("3"), Some(false)); assert_eq!(unit.get_boolean("4"), Some(false)); assert_eq!(unit.get_boolean("5"), Some(true)); assert_eq!(unit.get_boolean("6"), Some(true)); assert_eq!(unit.get_boolean("7"), Some(true)); assert_eq!(unit.get_boolean("8"), Some(true)); assert_eq!(unit.get_boolean("9"), None); } #[test] fn test_time_span() { let mut config = Config::default(); let res = parse_config(&mut config, "test.unit".to_string(), r" [Unit] 1=3 hours 2=1h 3=3 M 4=3m 5=1M1m 6=blaat 7=infinite 8=infinity ".to_string()); assert!(res.is_ok()); assert!(config.sections.get("Unit").is_some()); let unit = config.sections.get("Unit").unwrap(); assert_eq!(unit.get_time_span("1"), Some(Finite(FiniteTimeSpan::new().with_hours(3)))); assert_eq!(unit.get_time_span("2"), Some(Finite(FiniteTimeSpan::new().with_hours(1)))); assert_eq!(unit.get_time_span("3"), Some(Finite(FiniteTimeSpan::new().with_months(3)))); assert_eq!(unit.get_time_span("4"), Some(Finite(FiniteTimeSpan::new().with_minutes(3)))); assert_eq!(unit.get_time_span("5"), Some(Finite(FiniteTimeSpan::new().with_months(1).with_minutes(1)))); assert_eq!(unit.get_time_span("6"), None); assert_eq!(unit.get_finite_time_span("8"), None); assert_eq!(unit.get_finite_time_span("9"), None); assert_eq!(unit.get_time_span("8"), Some(Infinite)); assert_eq!(unit.get_time_span("9"), Some(Infinite)); } }