use std::{
fs,
io::{self, BufRead},
path,
};
use toml::Value;
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("TOML parsing error: {0}")]
TomlParse(#[from] toml::de::Error),
#[error("Line parsing error: {0}")]
LineParse(String),
}
pub trait Configurable {
fn config(&self) -> &Value;
fn load_config(
config_file_path: impl AsRef<path::Path>,
) -> Result<Value, ConfigError> {
let content: String = fs::read_to_string(config_file_path)?;
let config: Value = toml::from_str(&content)?;
Ok(config)
}
fn load_text_file_lines(
file_path: impl AsRef<path::Path>,
) -> Result<Vec<String>, ConfigError> {
let file = fs::File::open(file_path)?;
let lines = io::BufReader::new(file)
.lines()
.map(|l| l.map_err(|e| ConfigError::LineParse(e.to_string())))
.collect::<Result<Vec<_>, _>>()?;
Ok(lines)
}
fn get_config_value(&self, key: &str) -> Option<&Value> {
let keys: Vec<&str> = key.split('.').collect();
Self::get_value_recursive(self.config(), &keys)
}
fn get_value_recursive<'a>(
config: &'a Value,
keys: &[&str],
) -> Option<&'a Value> {
if keys.is_empty() {
return None;
};
match config {
Value::Table(map) => {
let key = keys[0];
let remaining_keys = &keys[1..];
if let Some(value) = map.get(key) {
if remaining_keys.is_empty() {
Some(value)
} else {
Self::get_value_recursive(value, remaining_keys)
}
} else {
None
}
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
pub struct TestApp {
config: Value,
user_agents: Option<Vec<String>>,
}
impl Configurable for TestApp {
fn config(&self) -> &Value {
&self.config
}
}
impl TestApp {
fn from_config(config_file_path: impl AsRef<path::Path>) -> Self {
let config = Self::load_config(config_file_path);
Self {
config: config.unwrap(),
user_agents: None,
}
}
fn load_uas(&mut self, uas_file_path: impl AsRef<path::Path>) {
self.user_agents = Self::load_text_file_lines(uas_file_path).ok();
}
}
#[test]
fn test_load_config() {
let config_path = "../tests/simple_config.toml";
let app = TestApp::from_config(config_path);
assert_eq!(app.config["app"]["threads"].as_integer(), Some(4));
assert_eq!(app.config()["app"]["max_queue"].as_integer(), Some(500));
assert_eq!(app.user_agents, None);
}
#[test]
fn test_load_config_valid_toml() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("config.toml");
let mut file = File::create(&config_path).unwrap();
writeln!(file, "key = \"value\"\n[app]\nsetting = 42").unwrap();
let config = TestApp::load_config(&config_path);
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config["key"].as_str(), Some("value"));
assert_eq!(config["app"]["setting"].as_integer(), Some(42));
}
#[test]
fn test_load_config_invalid_toml() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("config.toml");
let mut file = File::create(&config_path).unwrap();
writeln!(file, "invalid = : toml").unwrap();
let config = TestApp::load_config(&config_path);
assert!(matches!(config, Err(ConfigError::TomlParse(_))));
}
#[test]
fn test_load_text_file_lines() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "line1\nline2\nline3").unwrap();
let lines = TestApp::load_text_file_lines(&file_path);
assert!(lines.is_ok());
let lines = lines.unwrap();
assert_eq!(lines, vec!["line1", "line2", "line3"]);
}
#[test]
fn test_get_config_value_empty_keys() {
let config_path = "../tests/simple_config.toml";
let app = TestApp::from_config(config_path);
assert_eq!(app.get_config_value(""), None);
}
#[test]
fn test_get_config_value() {
let config_path = "../tests/simple_config.toml";
let app = TestApp::from_config(config_path);
assert_eq!(
app.get_config_value("logging.log_to_file")
.unwrap()
.as_bool(),
Some(true)
)
}
#[test]
fn test_get_config_value_recursive() {
let toml = r#"
[app.nested]
value = 42
"#;
let config: Value = toml::from_str(toml).unwrap();
let app = TestApp {
config,
user_agents: None,
};
assert_eq!(
app.get_config_value("app.nested.value")
.and_then(|v| v.as_integer()),
Some(42)
);
assert_eq!(app.get_config_value("app.missing.value"), None);
assert_eq!(app.get_config_value("missing"), None);
}
#[test]
fn test_load_lines() {
let config_path = "../tests/simple_config.toml";
let mut app = TestApp::from_config(config_path);
let uas_file_path = {
app.get_config_value("app.user_agents_file")
.unwrap()
.as_str()
.unwrap()
.to_owned()
};
app.load_uas(&uas_file_path);
}
}