use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "Versions", into = "Versions")]
pub struct Config {
allowed_kinds: Vec<String>,
digits: usize,
pub allow_unrecognised: bool,
pub allow_invalid: bool,
pub subfolders_are_namespaces: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
allowed_kinds: Vec::new(),
digits: default_digits(),
allow_unrecognised: false,
allow_invalid: false,
subfolders_are_namespaces: false,
}
}
}
impl Config {
pub fn load(path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read config file: {e}"))?;
toml::from_str(&content).map_err(|e| format!("Failed to parse config file: {e}"))
}
pub fn save(&self, path: &Path) -> Result<(), String> {
let content =
toml::to_string_pretty(self).map_err(|e| format!("Failed to serialize config: {e}"))?;
std::fs::write(path, content).map_err(|e| format!("Failed to write config file: {e}"))
}
#[must_use]
pub const fn digits(&self) -> usize {
self.digits
}
#[must_use]
pub fn allowed_kinds(&self) -> &[String] {
&self.allowed_kinds
}
pub const fn set_subfolders_are_namespaces(&mut self, value: bool) {
self.subfolders_are_namespaces = value;
}
}
const fn default_digits() -> usize {
3
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "_version")]
enum Versions {
#[serde(rename = "1")]
V1 {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
allowed_kinds: Vec<String>,
#[serde(default = "default_digits")]
digits: usize,
#[serde(default)]
allow_unrecognised: bool,
#[serde(default)]
allow_invalid: bool,
#[serde(default)]
subfolders_are_namespaces: bool,
},
}
impl From<Versions> for super::Config {
fn from(versions: Versions) -> Self {
match versions {
Versions::V1 {
allowed_kinds,
digits,
allow_unrecognised,
allow_invalid,
subfolders_are_namespaces,
} => Self {
allowed_kinds,
digits,
allow_unrecognised,
allow_invalid,
subfolders_are_namespaces,
},
}
}
}
impl From<super::Config> for Versions {
fn from(config: super::Config) -> Self {
Self::V1 {
allowed_kinds: config.allowed_kinds,
digits: config.digits,
allow_unrecognised: config.allow_unrecognised,
allow_invalid: config.allow_invalid,
subfolders_are_namespaces: config.subfolders_are_namespaces,
}
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
#[test]
fn load_reads_valid_file() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(
b"_version = \"1\"\nallowed_kinds = [\"USR\", \"SYS\"]\ndigits = 4\nallow_unrecognised = true\nallow_invalid = true\nsubfolders_are_namespaces = true\n",
)
.unwrap();
let config = Config::load(file.path()).unwrap();
assert_eq!(
config.allowed_kinds(),
&["USR".to_string(), "SYS".to_string()]
);
assert_eq!(config.digits(), 4);
assert!(config.allow_unrecognised);
assert!(config.allow_invalid);
assert!(config.subfolders_are_namespaces);
}
#[test]
fn load_missing_file_returns_error() {
let tmp = tempfile::tempdir().unwrap();
let missing = tmp.path().join("missing.toml");
let error = Config::load(&missing).unwrap_err();
assert!(error.starts_with("Failed to read config file:"));
}
#[test]
fn load_invalid_toml_returns_error() {
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(b"_version = \"1\"\ndigits = \"three\"\n")
.unwrap();
let error = Config::load(file.path()).unwrap_err();
assert!(error.starts_with("Failed to parse config file:"));
}
#[test]
fn empty_file_returns_default() {
let expected = Config::default();
let actual: Config = toml::from_str(r#"_version = "1""#).unwrap();
assert_eq!(actual, expected);
}
}