Initial commit (config parsing)
commit
23260fd60a
@ -0,0 +1,9 @@
|
|||||||
|
name: default
|
||||||
|
type: docker
|
||||||
|
kind: pipeline
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: rust
|
||||||
|
commands:
|
||||||
|
- cargo test
|
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
**/*.rs.bk
|
@ -0,0 +1,6 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
[[package]]
|
||||||
|
name = "systemf"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "systemf"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["eater <=@eater.me>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
@ -0,0 +1,216 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::config::parser::parse_time_span;
|
||||||
|
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Config {
|
||||||
|
name: String,
|
||||||
|
file_pointer: usize,
|
||||||
|
files: HashMap<usize, String>,
|
||||||
|
sections: HashMap<String, Section>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub(crate) fn add_file(&mut self, file_name: String) -> usize {
|
||||||
|
let id = self.file_pointer;
|
||||||
|
self.file_pointer += 1;
|
||||||
|
self.files.insert(id, file_name);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Section {
|
||||||
|
name: String,
|
||||||
|
entries: HashMap<String, Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Section {
|
||||||
|
pub fn new(name: String) -> Section {
|
||||||
|
Section {
|
||||||
|
name,
|
||||||
|
entries: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entry(&self, name: &str) -> Option<&Entry> {
|
||||||
|
self.entries.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_string(&self, name: &str) -> Option<&str> {
|
||||||
|
self.entries.get(name).map(|x| x.get_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_boolean(&self, name: &str) -> Option<bool> {
|
||||||
|
self.entries.get(name).and_then(|x| x.get_boolean())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_list(&self, name: &str) -> Option<Vec<&str>> {
|
||||||
|
self.entries.get(name).map(|x| x.get_list())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_time_span(&self, name: &str) -> Option<TimeSpan> {
|
||||||
|
self.entries.get(name).and_then(|x| x.get_time_span())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct Entry {
|
||||||
|
name: String,
|
||||||
|
values: Vec<EntryValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn new(name: String) -> Entry {
|
||||||
|
Entry {
|
||||||
|
name,
|
||||||
|
values: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, value: &str, file_id: usize) {
|
||||||
|
self.values.push(EntryValue { value: value.to_string(), file_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_string(&self) -> &str {
|
||||||
|
self.last_value().unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_list(&self) -> Vec<&str> {
|
||||||
|
self.values.iter()
|
||||||
|
.map(|x| x.value.as_str())
|
||||||
|
.fold(Vec::new(), |mut acc, val| {
|
||||||
|
if val == "" {
|
||||||
|
acc.clear()
|
||||||
|
} else {
|
||||||
|
acc.push(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_value(&self) -> Option<&str> {
|
||||||
|
self.values.last().map(|x| x.value.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_boolean(&self) -> Option<bool> {
|
||||||
|
self.last_value().and_then(|value| {
|
||||||
|
match value {
|
||||||
|
"yes" | "true" | "1" | "on" => {
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
"no" | "false" | "0" | "off" => {
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_time_span(&self) -> Option<TimeSpan> {
|
||||||
|
self.last_value().and_then(parse_time_span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||||
|
pub struct TimeSpan {
|
||||||
|
pub years: usize,
|
||||||
|
pub months: usize,
|
||||||
|
pub weeks: usize,
|
||||||
|
pub days: usize,
|
||||||
|
pub hours: usize,
|
||||||
|
pub minutes: usize,
|
||||||
|
pub seconds: usize,
|
||||||
|
pub milliseconds: usize,
|
||||||
|
pub microseconds: usize,
|
||||||
|
pub nanoseconds: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeSpan {
|
||||||
|
pub fn with_years(&self, years: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.years = years;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_months(&self, months: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.months = months;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weeks(&self, weeks: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.weeks = weeks;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_days(&self, days: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.days = days;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_hours(&self, hours: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.hours = hours;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_minutes(&self, minutes: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.minutes = minutes;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_seconds(&self, seconds: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.seconds = seconds;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_milliseconds(&self, milliseconds: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.milliseconds = milliseconds;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_microseconds(&self, microseconds: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.microseconds = microseconds;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_nanoseconds(&self, nanoseconds: usize) -> TimeSpan {
|
||||||
|
let mut new = self.clone();
|
||||||
|
new.nanoseconds = nanoseconds;
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> TimeSpan {
|
||||||
|
TimeSpan {
|
||||||
|
years: 0,
|
||||||
|
months: 0,
|
||||||
|
weeks: 0,
|
||||||
|
days: 0,
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
microseconds: 0,
|
||||||
|
nanoseconds: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct EntryValue {
|
||||||
|
value: String,
|
||||||
|
file_id: usize,
|
||||||
|
}
|
@ -0,0 +1,271 @@
|
|||||||
|
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)) = ¤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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
pub mod config;
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
Loading…
Reference in New Issue