Files
asusctl/config-traits/src/lib.rs
2025-01-15 22:19:46 +13:00

329 lines
11 KiB
Rust

//! `config_traits` is a crate that broke out from the requirement to manage
//! various different config files, including parsing from different formats and
//! updating them from previous versions where fields or names are changed in
//! some way.
//!
//! The end canonical file format is `.ron` as this supports rust types well
use std::fs::{self, create_dir, File, OpenOptions};
use std::io::{Read, Write};
use std::path::PathBuf;
use log::{error, warn};
pub use ron;
use ron::ser::PrettyConfig;
use serde::de::DeserializeOwned;
use serde::Serialize;
/// Config file helper traits. Only `new()` and `file_name()` are required to be
/// implemented, the rest are intended to be free methods.
pub trait StdConfig
where
Self: Serialize + DeserializeOwned
{
/// Taking over the standard `new()` to ensure things can be generic
fn new() -> Self;
/// Return the config files names, such as `wibble.cfg`
fn file_name(&self) -> String;
/// Return the full path to the directory the config file resides in
fn config_dir() -> PathBuf;
/// Return the full path to the config file
fn file_path(&self) -> PathBuf {
let mut config = Self::config_dir();
if !config.exists() {
create_dir(config.as_path())
.unwrap_or_else(|e| panic!("Could not create {:?} {e}", Self::config_dir()));
}
config.push(self.file_name());
let mut do_rename = !config.exists();
let mut cfg_old = config.clone();
// Migrating all configs to .ron format, so we do need to check for older ones
if do_rename {
warn!("Config {cfg_old:?} does not exist, looking for .cfg next");
cfg_old.pop();
let tmp = self.file_name();
let parts: Vec<_> = tmp.split('.').collect();
cfg_old.push(format!("{}.cfg", parts[0]));
}
if do_rename && cfg_old.exists() {
// Now we gotta rename it
warn!("Renaming {cfg_old:?} to {config:?}");
std::fs::rename(&cfg_old, &config).unwrap_or_else(|err| {
error!(
"Could not rename. Please remove {} then restart service: Error {}",
self.file_name(),
err
);
});
do_rename = false;
}
if do_rename && !cfg_old.exists() {
warn!("Config {cfg_old:?} does not exist, looking for .conf next");
cfg_old.pop();
let tmp = self.file_name();
let parts: Vec<_> = tmp.split('.').collect();
cfg_old.push(format!("{}.conf", parts[0]));
}
if do_rename && cfg_old.exists() {
// Now we gotta rename it
warn!("Renaming {cfg_old:?} to {config:?}");
std::fs::rename(&cfg_old, &config).unwrap_or_else(|err| {
error!(
"Could not rename. Please remove {} then restart service: Error {}",
self.file_name(),
err
);
});
}
config
}
/// Directly open the config file for read and write. If the config file
/// does not exist it is created, including the directories the file
/// resides in.
fn file_open(&self) -> File {
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(self.file_path())
.unwrap_or_else(|e| panic!("Could not open {:?} {e}", self.file_path()))
}
/// Open and parse the config file to self from ron format
fn read(&mut self) {
if let Ok(data) = fs::read_to_string(self.file_path()) {
if data.is_empty() {
warn!("File is empty {:?}", self.file_path());
} else if let Ok(data) = ron::from_str(&data) {
*self = data;
} else {
warn!("Could not deserialise {:?}", self.file_path());
}
}
}
/// Open and parse the config file to self from ron format
fn read_new(&self) -> Option<Self> {
if let Ok(data) = fs::read_to_string(self.file_path()) {
if data.is_empty() {
warn!("File is empty {:?}", self.file_path());
} else if let Ok(data) = ron::from_str(&data) {
return Some(data);
} else {
warn!("Could not deserialise {:?}", self.file_path());
}
}
None
}
/// Write the config file data to pretty ron format
fn write(&self) {
let mut file = match File::create(self.file_path()) {
Ok(data) => data,
Err(e) => {
error!(
"Couldn't overwrite config {:?}, error: {e}",
self.file_path()
);
return;
}
};
let ron = match ron::ser::to_string_pretty(&self, PrettyConfig::new().depth_limit(4)) {
Ok(data) => data,
Err(e) => {
error!("Parse {:?} to RON failed, error: {e}", self.file_path());
return;
}
};
file.write_all(ron.as_bytes())
.unwrap_or_else(|err| error!("Could not write config: {}", err));
}
/// Renames the existing file to `<file>-old`
fn rename_file_old(&self) {
warn!("Renaming {} to {}-old", self.file_name(), self.file_name());
let mut cfg_old = self.file_path().to_string_lossy().to_string();
cfg_old.push_str("-old");
std::fs::rename(self.file_path(), cfg_old).unwrap_or_else(|err| {
error!(
"Could not rename. Please remove {} then restart service: Error {}",
self.file_name(),
err
);
});
}
}
#[macro_export]
macro_rules! std_config_load {
($trait_name:ident: $($generic:ident),*) => {
/// Base trait for loading/parsing. This is intended to be used to help update
/// configs to new versions
///
/// # Example
/// ```rust
/// use std::path::PathBuf;
/// use serde::{Deserialize, Serialize};
/// use config_traits::{StdConfig, StdConfigLoad2};
///
/// #[derive(Deserialize, Serialize, Debug)]
/// struct FanCurveConfigOld {}
///
/// #[derive(Deserialize, Serialize, Debug)]
/// struct FanCurveConfigOlder {}
///
/// #[derive(Deserialize, Serialize, Debug)]
/// struct FanCurveConfig {}
///
/// impl From<FanCurveConfigOld> for FanCurveConfig {
/// fn from(_: FanCurveConfigOld) -> Self { Self {} }
/// }
///
/// impl From<FanCurveConfigOlder> for FanCurveConfig {
/// fn from(_: FanCurveConfigOlder) -> Self { Self {} }
/// }
///
/// impl StdConfig for FanCurveConfig {
/// fn new() -> Self { Self {} }
///
/// fn file_name(&self) -> std::string::String { "test_name.conf".to_owned() }
///
/// fn config_dir() -> PathBuf { PathBuf::from("/tmp") }
/// }
///
/// impl StdConfigLoad2<FanCurveConfigOld, FanCurveConfigOlder> for FanCurveConfig {}
/// ```
///
/// If all of the generics fails to parse, then the old config is renamed and a
/// new one created
pub trait $trait_name<$($generic),*>
where
Self: $crate::StdConfig + DeserializeOwned + Serialize,
$($generic: DeserializeOwned + Into<Self>),*
{
fn load(mut self) -> Self {
let mut file = self.file_open();
let mut buf = String::new();
if let Ok(read_len) = file.read_to_string(&mut buf) {
if read_len != 0 {
if let Ok(data) = ron::from_str(&buf) {
self = data;
log::info!("Parsed RON for {:?}", std::any::type_name::<Self>());
} $(else if let Ok(data) = ron::from_str::<$generic>(&buf) {
self = data.into();
log::info!("New version failed, trying previous: Parsed RON for {:?}", std::any::type_name::<$generic>());
})* else {
self.rename_file_old();
self = Self::new();
}
} else {
error!("Config file {} zero read length", self.file_name());
}
}
self.write();
self
}
}
};
}
std_config_load!(StdConfigLoad:);
std_config_load!(StdConfigLoad1: T1);
std_config_load!(StdConfigLoad2: T1, T2);
std_config_load!(StdConfigLoad3: T1, T2, T3);
std_config_load!(StdConfigLoad4: T1, T2, T3, T4);
#[cfg(test)]
mod tests {
use std::path::PathBuf;
#[test]
fn check_macro_from_1() {
#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Test {}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Old1 {}
impl crate::StdConfig for Test {
fn new() -> Self {
Self {}
}
fn file_name(&self) -> String {
String::new()
}
fn config_dir() -> PathBuf {
PathBuf::new()
}
}
impl From<Old1> for Test {
fn from(_: Old1) -> Self {
Self {}
}
}
let _ = Test {};
impl crate::StdConfigLoad1<Old1> for Test {}
}
#[test]
fn check_macro_from_3() {
#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Test {}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Old1 {}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Old2 {}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
struct Old3 {}
impl crate::StdConfig for Test {
fn new() -> Self {
Self {}
}
fn file_name(&self) -> String {
String::new()
}
fn config_dir() -> PathBuf {
PathBuf::new()
}
}
impl From<Old1> for Test {
fn from(_: Old1) -> Self {
Self {}
}
}
impl From<Old2> for Test {
fn from(_: Old2) -> Self {
Self {}
}
}
impl From<Old3> for Test {
fn from(_: Old3) -> Self {
Self {}
}
}
let _ = Test {};
impl crate::StdConfigLoad3<Old1, Old2, Old3> for Test {}
}
}