diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-03-11 15:34:47 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-03-11 15:34:47 +0800 |
| commit | 8b38e22525adcafecd8b50daf041a1dc2d672d0b (patch) | |
| tree | b5c012dbbdd6975f405105b778d0ba9354a21233 /legacy_utils/cfg_file | |
| parent | e49128388e3b2f73523d82bf7f16fa1eae019c37 (diff) | |
Replace cfg_file with new config system
Diffstat (limited to 'legacy_utils/cfg_file')
| -rw-r--r-- | legacy_utils/cfg_file/Cargo.toml | 23 | ||||
| -rw-r--r-- | legacy_utils/cfg_file/cfg_file_derive/Cargo.toml | 11 | ||||
| -rw-r--r-- | legacy_utils/cfg_file/cfg_file_derive/src/lib.rs | 130 | ||||
| -rw-r--r-- | legacy_utils/cfg_file/cfg_file_test/Cargo.toml | 9 | ||||
| -rw-r--r-- | legacy_utils/cfg_file/cfg_file_test/src/lib.rs | 95 | ||||
| -rw-r--r-- | legacy_utils/cfg_file/src/config.rs | 263 | ||||
| -rw-r--r-- | legacy_utils/cfg_file/src/lib.rs | 7 |
7 files changed, 538 insertions, 0 deletions
diff --git a/legacy_utils/cfg_file/Cargo.toml b/legacy_utils/cfg_file/Cargo.toml new file mode 100644 index 0000000..0685329 --- /dev/null +++ b/legacy_utils/cfg_file/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cfg_file" +edition = "2024" +version.workspace = true + +[features] +default = ["derive"] +derive = [] + +[dependencies] +cfg_file_derive = { path = "cfg_file_derive" } + +# Async +tokio = { version = "1.48.0", features = ["full"] } +async-trait = "0.1.89" + +# Serialization +serde = { version = "1.0.228", features = ["derive"] } +serde_yaml = "0.9.34" +serde_json = "1.0.145" +ron = "0.11.0" +toml = "0.9.8" +bincode2 = "2.0.1" diff --git a/legacy_utils/cfg_file/cfg_file_derive/Cargo.toml b/legacy_utils/cfg_file/cfg_file_derive/Cargo.toml new file mode 100644 index 0000000..ce5e77f --- /dev/null +++ b/legacy_utils/cfg_file/cfg_file_derive/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cfg_file_derive" +edition = "2024" +version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full", "extra-traits"] } +quote = "1.0" diff --git a/legacy_utils/cfg_file/cfg_file_derive/src/lib.rs b/legacy_utils/cfg_file/cfg_file_derive/src/lib.rs new file mode 100644 index 0000000..e916311 --- /dev/null +++ b/legacy_utils/cfg_file/cfg_file_derive/src/lib.rs @@ -0,0 +1,130 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::ParseStream; +use syn::{Attribute, DeriveInput, Expr, parse_macro_input}; +/// # Macro - ConfigFile +/// +/// ## Usage +/// +/// Use `#[derive(ConfigFile)]` to derive the ConfigFile trait for a struct +/// +/// Specify the default storage path via `#[cfg_file(path = "...")]` +/// +/// ## About the `cfg_file` attribute macro +/// +/// Use `#[cfg_file(path = "string")]` to specify the configuration file path +/// +/// Or use `#[cfg_file(path = constant_expression)]` to specify the configuration file path +/// +/// ## Path Rules +/// +/// Paths starting with `"./"`: relative to the current working directory +/// +/// Other paths: treated as absolute paths +/// +/// When no path is specified: use the struct name + ".json" as the default filename (e.g., `my_struct.json`) +/// +/// ## Example +/// ```ignore +/// #[derive(ConfigFile)] +/// #[cfg_file(path = "./config.json")] +/// struct AppConfig; +/// ``` +#[proc_macro_derive(ConfigFile, attributes(cfg_file))] +pub fn derive_config_file(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Process 'cfg_file' + let path_expr = match find_cfg_file_path(&input.attrs) { + Some(PathExpr::StringLiteral(path)) => { + if let Some(path_str) = path.strip_prefix("./") { + quote! { + std::env::current_dir()?.join(#path_str) + } + } else { + // Using Absolute Path + quote! { + std::path::PathBuf::from(#path) + } + } + } + Some(PathExpr::PathExpression(path_expr)) => { + // For path expressions (constants), generate code that references the constant + quote! { + std::path::PathBuf::from(#path_expr) + } + } + None => { + let default_file = to_snake_case(&name.to_string()) + ".json"; + quote! { + std::env::current_dir()?.join(#default_file) + } + } + }; + + let expanded = quote! { + impl cfg_file::config::ConfigFile for #name { + type DataType = #name; + + fn default_path() -> Result<std::path::PathBuf, std::io::Error> { + Ok(#path_expr) + } + } + }; + + TokenStream::from(expanded) +} + +enum PathExpr { + StringLiteral(String), + PathExpression(syn::Expr), +} + +fn find_cfg_file_path(attrs: &[Attribute]) -> Option<PathExpr> { + for attr in attrs { + if attr.path().is_ident("cfg_file") { + let parser = |meta: ParseStream| { + let path_meta: syn::MetaNameValue = meta.parse()?; + if path_meta.path.is_ident("path") { + match &path_meta.value { + // String literal case: path = "./vault.toml" + Expr::Lit(expr_lit) if matches!(expr_lit.lit, syn::Lit::Str(_)) => { + if let syn::Lit::Str(lit_str) = &expr_lit.lit { + return Ok(PathExpr::StringLiteral(lit_str.value())); + } + } + // Path expression case: path = SERVER_FILE_VAULT or crate::constants::SERVER_FILE_VAULT + expr @ (Expr::Path(_) | Expr::Macro(_)) => { + return Ok(PathExpr::PathExpression(expr.clone())); + } + _ => {} + } + } + Err(meta.error("expected `path = \"...\"` or `path = CONSTANT`")) + }; + + if let Ok(path_expr) = attr.parse_args_with(parser) { + return Some(path_expr); + } + } + } + None +} + +fn to_snake_case(s: &str) -> String { + let mut snake = String::new(); + for (i, c) in s.chars().enumerate() { + if c.is_uppercase() { + if i != 0 { + snake.push('_'); + } + snake.push(c.to_ascii_lowercase()); + } else { + snake.push(c); + } + } + snake +} diff --git a/legacy_utils/cfg_file/cfg_file_test/Cargo.toml b/legacy_utils/cfg_file/cfg_file_test/Cargo.toml new file mode 100644 index 0000000..5db1010 --- /dev/null +++ b/legacy_utils/cfg_file/cfg_file_test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cfg_file_test" +version = "0.1.0" +edition = "2024" + +[dependencies] +cfg_file = { path = "../../cfg_file", features = ["default"] } +tokio = { version = "1.48.0", features = ["full"] } +serde = { version = "1.0.228", features = ["derive"] } diff --git a/legacy_utils/cfg_file/cfg_file_test/src/lib.rs b/legacy_utils/cfg_file/cfg_file_test/src/lib.rs new file mode 100644 index 0000000..f70d00d --- /dev/null +++ b/legacy_utils/cfg_file/cfg_file_test/src/lib.rs @@ -0,0 +1,95 @@ +#[cfg(test)] +mod test_cfg_file { + use cfg_file::ConfigFile; + use cfg_file::config::ConfigFile; + use serde::{Deserialize, Serialize}; + use std::collections::HashMap; + + #[derive(ConfigFile, Deserialize, Serialize, Default)] + #[cfg_file(path = "./.temp/example_cfg.toml")] + struct ExampleConfig { + name: String, + age: i32, + hobby: Vec<String>, + secret: HashMap<String, String>, + } + + #[derive(ConfigFile, Deserialize, Serialize, Default)] + #[cfg_file(path = "./.temp/example_bincode.bcfg")] + struct ExampleBincodeConfig { + name: String, + age: i32, + hobby: Vec<String>, + secret: HashMap<String, String>, + } + + #[tokio::test] + async fn test_config_file_serialization() { + let mut example = ExampleConfig { + name: "Weicao".to_string(), + age: 22, + hobby: ["Programming", "Painting"] + .iter() + .map(|m| m.to_string()) + .collect(), + secret: HashMap::new(), + }; + let secret_no_comments = + "Actually, I'm really too lazy to write comments, documentation, and unit tests."; + example + .secret + .entry("No comments".to_string()) + .insert_entry(secret_no_comments.to_string()); + + let secret_peek = "Of course, it's peeking at you who's reading the source code."; + example + .secret + .entry("Peek".to_string()) + .insert_entry(secret_peek.to_string()); + + ExampleConfig::write(&example).await.unwrap(); // Write to default path. + + // Read from default path. + let read_cfg = ExampleConfig::read().await.unwrap(); + assert_eq!(read_cfg.name, "Weicao"); + assert_eq!(read_cfg.age, 22); + assert_eq!(read_cfg.hobby, vec!["Programming", "Painting"]); + assert_eq!(read_cfg.secret["No comments"], secret_no_comments); + assert_eq!(read_cfg.secret["Peek"], secret_peek); + } + + #[tokio::test] + async fn test_bincode_config_file_serialization() { + let mut example = ExampleBincodeConfig { + name: "Weicao".to_string(), + age: 22, + hobby: ["Programming", "Painting"] + .iter() + .map(|m| m.to_string()) + .collect(), + secret: HashMap::new(), + }; + let secret_no_comments = + "Actually, I'm really too lazy to write comments, documentation, and unit tests."; + example + .secret + .entry("No comments".to_string()) + .insert_entry(secret_no_comments.to_string()); + + let secret_peek = "Of course, it's peeking at you who's reading the source code."; + example + .secret + .entry("Peek".to_string()) + .insert_entry(secret_peek.to_string()); + + ExampleBincodeConfig::write(&example).await.unwrap(); // Write to default path. + + // Read from default path. + let read_cfg = ExampleBincodeConfig::read().await.unwrap(); + assert_eq!(read_cfg.name, "Weicao"); + assert_eq!(read_cfg.age, 22); + assert_eq!(read_cfg.hobby, vec!["Programming", "Painting"]); + assert_eq!(read_cfg.secret["No comments"], secret_no_comments); + assert_eq!(read_cfg.secret["Peek"], secret_peek); + } +} diff --git a/legacy_utils/cfg_file/src/config.rs b/legacy_utils/cfg_file/src/config.rs new file mode 100644 index 0000000..d3f5477 --- /dev/null +++ b/legacy_utils/cfg_file/src/config.rs @@ -0,0 +1,263 @@ +use async_trait::async_trait; +use bincode2; +use ron; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Cow, + env::current_dir, + io::Error, + path::{Path, PathBuf}, +}; +use tokio::{fs, io::AsyncReadExt}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConfigFormat { + Yaml, + Toml, + Ron, + Json, + Bincode, +} + +impl ConfigFormat { + fn from_filename(filename: &str) -> Option<Self> { + if filename.ends_with(".yaml") || filename.ends_with(".yml") { + Some(Self::Yaml) + } else if filename.ends_with(".toml") || filename.ends_with(".tom") { + Some(Self::Toml) + } else if filename.ends_with(".ron") { + Some(Self::Ron) + } else if filename.ends_with(".json") { + Some(Self::Json) + } else if filename.ends_with(".bcfg") { + Some(Self::Bincode) + } else { + None + } + } +} + +/// # Trait - ConfigFile +/// +/// Used to implement more convenient persistent storage functionality for structs +/// +/// This trait requires the struct to implement Default and serde's Serialize and Deserialize traits +/// +/// ## Implementation +/// +/// ```ignore +/// // Your struct +/// #[derive(Default, Serialize, Deserialize)] +/// struct YourData; +/// +/// impl ConfigFile for YourData { +/// type DataType = YourData; +/// +/// // Specify default path +/// fn default_path() -> Result<PathBuf, Error> { +/// Ok(current_dir()?.join("data.json")) +/// } +/// } +/// ``` +/// +/// > **Using derive macro** +/// > +/// > We provide the derive macro `#[derive(ConfigFile)]` +/// > +/// > You can implement this trait more quickly, please check the module cfg_file::cfg_file_derive +/// +#[async_trait] +pub trait ConfigFile: Serialize + for<'a> Deserialize<'a> + Default { + type DataType: Serialize + for<'a> Deserialize<'a> + Default + Send + Sync; + + fn default_path() -> Result<PathBuf, Error>; + + /// # Read from default path + /// + /// Read data from the path specified by default_path() + /// + /// ```ignore + /// fn main() -> Result<(), std::io::Error> { + /// let data = YourData::read().await?; + /// } + /// ``` + async fn read() -> Result<Self::DataType, std::io::Error> + where + Self: Sized + Send + Sync, + { + let path = Self::default_path()?; + Self::read_from(path).await + } + + /// # Read from the given path + /// + /// Read data from the path specified by the path parameter + /// + /// ```ignore + /// fn main() -> Result<(), std::io::Error> { + /// let data_path = current_dir()?.join("data.json"); + /// let data = YourData::read_from(data_path).await?; + /// } + /// ``` + async fn read_from(path: impl AsRef<Path> + Send) -> Result<Self::DataType, std::io::Error> + where + Self: Sized + Send + Sync, + { + let path = path.as_ref(); + let cwd = current_dir()?; + let file_path = cwd.join(path); + + // Check if file exists + if fs::metadata(&file_path).await.is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Config file not found", + )); + } + + // Determine file format first + let format = file_path + .file_name() + .and_then(|name| name.to_str()) + .and_then(ConfigFormat::from_filename) + .unwrap_or(ConfigFormat::Bincode); // Default to Bincode + + // Deserialize based on format + let result = match format { + ConfigFormat::Yaml => { + let mut file = fs::File::open(&file_path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + serde_yaml::from_str(&contents) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? + } + ConfigFormat::Toml => { + let mut file = fs::File::open(&file_path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + toml::from_str(&contents) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? + } + ConfigFormat::Ron => { + let mut file = fs::File::open(&file_path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + ron::from_str(&contents) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? + } + ConfigFormat::Json => { + let mut file = fs::File::open(&file_path).await?; + let mut contents = String::new(); + file.read_to_string(&mut contents).await?; + serde_json::from_str(&contents) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? + } + ConfigFormat::Bincode => { + // For Bincode, we need to read the file as bytes directly + let bytes = fs::read(&file_path).await?; + bincode2::deserialize(&bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? + } + }; + + Ok(result) + } + + /// # Write to default path + /// + /// Write data to the path specified by default_path() + /// + /// ```ignore + /// fn main() -> Result<(), std::io::Error> { + /// let data = YourData::default(); + /// YourData::write(&data).await?; + /// } + /// ``` + async fn write(val: &Self::DataType) -> Result<(), std::io::Error> + where + Self: Sized + Send + Sync, + { + let path = Self::default_path()?; + Self::write_to(val, path).await + } + /// # Write to the given path + /// + /// Write data to the path specified by the path parameter + /// + /// ```ignore + /// fn main() -> Result<(), std::io::Error> { + /// let data = YourData::default(); + /// let data_path = current_dir()?.join("data.json"); + /// YourData::write_to(&data, data_path).await?; + /// } + /// ``` + async fn write_to( + val: &Self::DataType, + path: impl AsRef<Path> + Send, + ) -> Result<(), std::io::Error> + where + Self: Sized + Send + Sync, + { + let path = path.as_ref(); + + if let Some(parent) = path.parent() + && !parent.exists() + { + tokio::fs::create_dir_all(parent).await?; + } + + let cwd = current_dir()?; + let file_path = cwd.join(path); + + // Determine file format + let format = file_path + .file_name() + .and_then(|name| name.to_str()) + .and_then(ConfigFormat::from_filename) + .unwrap_or(ConfigFormat::Bincode); // Default to Bincode + + match format { + ConfigFormat::Yaml => { + let contents = serde_yaml::to_string(val) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + fs::write(&file_path, contents).await? + } + ConfigFormat::Toml => { + let contents = toml::to_string(val) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + fs::write(&file_path, contents).await? + } + ConfigFormat::Ron => { + let mut pretty_config = ron::ser::PrettyConfig::new(); + pretty_config.new_line = Cow::from("\n"); + pretty_config.indentor = Cow::from(" "); + + let contents = ron::ser::to_string_pretty(val, pretty_config) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + fs::write(&file_path, contents).await? + } + ConfigFormat::Json => { + let contents = serde_json::to_string(val) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + fs::write(&file_path, contents).await? + } + ConfigFormat::Bincode => { + let bytes = bincode2::serialize(val) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + fs::write(&file_path, bytes).await? + } + } + Ok(()) + } + + /// Check if the file returned by `default_path` exists + fn exist() -> bool + where + Self: Sized + Send + Sync, + { + let Ok(path) = Self::default_path() else { + return false; + }; + path.exists() + } +} diff --git a/legacy_utils/cfg_file/src/lib.rs b/legacy_utils/cfg_file/src/lib.rs new file mode 100644 index 0000000..72246e7 --- /dev/null +++ b/legacy_utils/cfg_file/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "derive")] +extern crate cfg_file_derive; + +#[cfg(feature = "derive")] +pub use cfg_file_derive::*; + +pub mod config; |
