You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
7.8 KiB
Rust

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<TimeSpan> {
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)) = &current_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);
}
}