use crate::config::{Config, Section, Entry, TimeSpan}; use std::error::Error; use std::fmt::{Display, Formatter, Write}; use std::borrow::{Borrow, BorrowMut}; #[derive(Debug)] struct ParserError { filename: String, line_no: usize, description: String, } impl Error for ParserError {} impl Display for ParserError { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { f.write_str(&format!("Config didn't start with section header in file {} at line {}", self.filename, self.line_no)) } } pub fn parse_time_span(val: &str) -> Option { let parse = val.replace(" ", ""); let mut time_span = TimeSpan::default(); let mut current_value = String::new(); let mut current_num = 0usize; let finish = &mut |name: &str, amount: usize| { match name { "nsec" | "ns" => { time_span.nanoseconds += amount; } "usec" | "us" | "µs" => { time_span.microseconds += amount; } "msec" | "ms" => { time_span.milliseconds += amount; } "" | "s" | "sec" | "second" | "seconds" => { time_span.seconds += amount; } "minutes" | "minute" | "min" | "m" => { time_span.minutes += amount; } "hours" | "hour" | "hr" | "h" => { time_span.hours += amount; } "days" | "day" | "d" => { time_span.days += amount; } "weeks" | "week" | "w" => { time_span.weeks += amount; } "M" | "months" | "month" => { time_span.months += amount; } "years" | "year" | "y" => { time_span.years += amount; } _ => return false } true }; let mut last_is_nr = false; for chxr in parse.chars() { match chxr.to_ascii_lowercase() { '0'..='9' => { if !last_is_nr { if !finish(current_value.as_str(), current_num) { return None; } current_value = String::new(); current_num = 0; last_is_nr = true } current_num *= 10; current_num += chxr.to_digit(10).unwrap_or(0) as usize } 'a'..='z' => { current_value += &chxr.to_string(); last_is_nr = false; } _ => { return None; } } } if !finish(current_value.as_str(), current_num) { None } else { Some(time_span) } } 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::config::TimeSpan; #[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); } 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 ".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(TimeSpan::new().with_hours(3))); assert_eq!(unit.get_time_span("2"), Some(TimeSpan::new().with_hours(1))); assert_eq!(unit.get_time_span("3"), Some(TimeSpan::new().with_months(3))); assert_eq!(unit.get_time_span("4"), Some(TimeSpan::new().with_minutes(3))); assert_eq!(unit.get_time_span("5"), Some(TimeSpan::new().with_months(1).with_minutes(1))); assert_eq!(unit.get_time_span("6"), None); } }