From 8b38e22525adcafecd8b50daf041a1dc2d672d0b Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 11 Mar 2026 15:34:47 +0800 Subject: Replace cfg_file with new config system --- Cargo.lock | 18 ++ Cargo.toml | 14 +- legacy_actions/Cargo.toml | 2 +- legacy_data/Cargo.toml | 2 +- legacy_data/tests/Cargo.toml | 2 +- legacy_utils/cfg_file/Cargo.toml | 23 ++ legacy_utils/cfg_file/cfg_file_derive/Cargo.toml | 11 + legacy_utils/cfg_file/cfg_file_derive/src/lib.rs | 130 +++++++++++ legacy_utils/cfg_file/cfg_file_test/Cargo.toml | 9 + legacy_utils/cfg_file/cfg_file_test/src/lib.rs | 95 ++++++++ legacy_utils/cfg_file/src/config.rs | 263 +++++++++++++++++++++++ legacy_utils/cfg_file/src/lib.rs | 7 + systems/_config/Cargo.toml | 6 + systems/_config/src/main.rs | 3 + utils/cfg_file/Cargo.toml | 23 -- utils/cfg_file/cfg_file_derive/Cargo.toml | 11 - utils/cfg_file/cfg_file_derive/src/lib.rs | 130 ----------- utils/cfg_file/cfg_file_test/Cargo.toml | 9 - utils/cfg_file/cfg_file_test/src/lib.rs | 95 -------- utils/cfg_file/src/config.rs | 263 ----------------------- utils/cfg_file/src/lib.rs | 7 - 21 files changed, 577 insertions(+), 546 deletions(-) create mode 100644 legacy_utils/cfg_file/Cargo.toml create mode 100644 legacy_utils/cfg_file/cfg_file_derive/Cargo.toml create mode 100644 legacy_utils/cfg_file/cfg_file_derive/src/lib.rs create mode 100644 legacy_utils/cfg_file/cfg_file_test/Cargo.toml create mode 100644 legacy_utils/cfg_file/cfg_file_test/src/lib.rs create mode 100644 legacy_utils/cfg_file/src/config.rs create mode 100644 legacy_utils/cfg_file/src/lib.rs create mode 100644 systems/_config/Cargo.toml create mode 100644 systems/_config/src/main.rs delete mode 100644 utils/cfg_file/Cargo.toml delete mode 100644 utils/cfg_file/cfg_file_derive/Cargo.toml delete mode 100644 utils/cfg_file/cfg_file_derive/src/lib.rs delete mode 100644 utils/cfg_file/cfg_file_test/Cargo.toml delete mode 100644 utils/cfg_file/cfg_file_test/src/lib.rs delete mode 100644 utils/cfg_file/src/config.rs delete mode 100644 utils/cfg_file/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 33d16d8..1f8bd66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,10 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "config_system" +version = "0.1.0" + [[package]] name = "const-oid" version = "0.9.6" @@ -972,6 +976,7 @@ dependencies = [ "asset_system", "cfg_file", "chrono", + "config_system", "constants", "criterion", "data_struct", @@ -986,6 +991,7 @@ dependencies = [ "vcs_actions", "vcs_data", "vcs_docs", + "workspace_system", ] [[package]] @@ -2585,6 +2591,18 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "workspace_system" +version = "0.1.0" +dependencies = [ + "asset_system", + "config_system", + "constants", + "framework", + "serde", + "tokio", +] + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index 3b6c1b5..e67b07c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,7 @@ members = [ "systems/sheet", "systems/sheet/macros", "systems/vault", - "utils/cfg_file", - "utils/cfg_file/cfg_file_derive", - "utils/cfg_file/cfg_file_test", + "systems/workspace", "utils/data_struct", "utils/hex_display", "utils/sha1_hash", @@ -44,11 +42,15 @@ members = [ "utils/tcp_connection/tcp_connection_test", # LEGACY AREA + "legacy_actions", "legacy_data", "legacy_data/tests", - "legacy_actions", "legacy_systems/action", "legacy_systems/action/action_macros", + "legacy_utils/cfg_file", + "legacy_utils/cfg_file/cfg_file_derive", + "legacy_utils/cfg_file/cfg_file_test", + "systems/_config", # LEGACY AREA ] @@ -94,7 +96,7 @@ jvlib = { path = "ffi" } vcs_docs = { path = "docs" } # Utils -cfg_file = { path = "utils/cfg_file" } +cfg_file = { path = "legacy_utils/cfg_file" } data_struct = { path = "utils/data_struct" } hex_display = { path = "utils/hex_display" } sha1_hash = { path = "utils/sha1_hash" } @@ -102,9 +104,11 @@ tcp_connection = { path = "utils/tcp_connection" } # Systems asset_system = { path = "systems/_asset" } +config_system = { path = "systems/_config" } constants = { path = "systems/_constants" } sheet_system = { path = "systems/sheet" } vault_system = { path = "systems/vault" } +workspace_system = { path = "systems/workspace" } # Legacy vcs_data = { path = "legacy_data" } diff --git a/legacy_actions/Cargo.toml b/legacy_actions/Cargo.toml index 0ee8068..47567f0 100644 --- a/legacy_actions/Cargo.toml +++ b/legacy_actions/Cargo.toml @@ -7,7 +7,7 @@ version.workspace = true # Utils tcp_connection = { path = "../utils/tcp_connection" } -cfg_file = { path = "../utils/cfg_file", features = ["default"] } +cfg_file = { path = "../legacy_utils/cfg_file", features = ["default"] } sha1_hash = { path = "../utils/sha1_hash" } just_fmt = "0.1.2" diff --git a/legacy_data/Cargo.toml b/legacy_data/Cargo.toml index e2f4dc6..c3b6b63 100644 --- a/legacy_data/Cargo.toml +++ b/legacy_data/Cargo.toml @@ -6,7 +6,7 @@ version.workspace = true [dependencies] # Utils -cfg_file = { path = "../utils/cfg_file", features = ["default"] } +cfg_file = { path = "../legacy_utils/cfg_file", features = ["default"] } data_struct = { path = "../utils/data_struct" } sha1_hash = { path = "../utils/sha1_hash" } tcp_connection = { path = "../utils/tcp_connection" } diff --git a/legacy_data/tests/Cargo.toml b/legacy_data/tests/Cargo.toml index b022436..de2b78c 100644 --- a/legacy_data/tests/Cargo.toml +++ b/legacy_data/tests/Cargo.toml @@ -6,7 +6,7 @@ version.workspace = true [dependencies] tcp_connection = { path = "../../utils/tcp_connection" } tcp_connection_test = { path = "../../utils/tcp_connection/tcp_connection_test" } -cfg_file = { path = "../../utils/cfg_file", features = ["default"] } +cfg_file = { path = "../../legacy_utils/cfg_file", features = ["default"] } vcs_data = { path = "../../legacy_data" } # Async & Networking 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 { + Ok(#path_expr) + } + } + }; + + TokenStream::from(expanded) +} + +enum PathExpr { + StringLiteral(String), + PathExpression(syn::Expr), +} + +fn find_cfg_file_path(attrs: &[Attribute]) -> Option { + 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, + secret: HashMap, + } + + #[derive(ConfigFile, Deserialize, Serialize, Default)] + #[cfg_file(path = "./.temp/example_bincode.bcfg")] + struct ExampleBincodeConfig { + name: String, + age: i32, + hobby: Vec, + secret: HashMap, + } + + #[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 { + 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 { +/// 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; + + /// # 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 + 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 + Send) -> Result + 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 + 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; diff --git a/systems/_config/Cargo.toml b/systems/_config/Cargo.toml new file mode 100644 index 0000000..eacbcda --- /dev/null +++ b/systems/_config/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "config_system" +edition = "2024" +version.workspace = true + +[dependencies] diff --git a/systems/_config/src/main.rs b/systems/_config/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/systems/_config/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/utils/cfg_file/Cargo.toml b/utils/cfg_file/Cargo.toml deleted file mode 100644 index 0685329..0000000 --- a/utils/cfg_file/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[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/utils/cfg_file/cfg_file_derive/Cargo.toml b/utils/cfg_file/cfg_file_derive/Cargo.toml deleted file mode 100644 index ce5e77f..0000000 --- a/utils/cfg_file/cfg_file_derive/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[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/utils/cfg_file/cfg_file_derive/src/lib.rs b/utils/cfg_file/cfg_file_derive/src/lib.rs deleted file mode 100644 index e916311..0000000 --- a/utils/cfg_file/cfg_file_derive/src/lib.rs +++ /dev/null @@ -1,130 +0,0 @@ -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 { - Ok(#path_expr) - } - } - }; - - TokenStream::from(expanded) -} - -enum PathExpr { - StringLiteral(String), - PathExpression(syn::Expr), -} - -fn find_cfg_file_path(attrs: &[Attribute]) -> Option { - 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/utils/cfg_file/cfg_file_test/Cargo.toml b/utils/cfg_file/cfg_file_test/Cargo.toml deleted file mode 100644 index 5db1010..0000000 --- a/utils/cfg_file/cfg_file_test/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[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/utils/cfg_file/cfg_file_test/src/lib.rs b/utils/cfg_file/cfg_file_test/src/lib.rs deleted file mode 100644 index f70d00d..0000000 --- a/utils/cfg_file/cfg_file_test/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -#[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, - secret: HashMap, - } - - #[derive(ConfigFile, Deserialize, Serialize, Default)] - #[cfg_file(path = "./.temp/example_bincode.bcfg")] - struct ExampleBincodeConfig { - name: String, - age: i32, - hobby: Vec, - secret: HashMap, - } - - #[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/utils/cfg_file/src/config.rs b/utils/cfg_file/src/config.rs deleted file mode 100644 index d3f5477..0000000 --- a/utils/cfg_file/src/config.rs +++ /dev/null @@ -1,263 +0,0 @@ -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 { - 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 { -/// 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; - - /// # 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 - 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 + Send) -> Result - 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 + 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/utils/cfg_file/src/lib.rs b/utils/cfg_file/src/lib.rs deleted file mode 100644 index 72246e7..0000000 --- a/utils/cfg_file/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[cfg(feature = "derive")] -extern crate cfg_file_derive; - -#[cfg(feature = "derive")] -pub use cfg_file_derive::*; - -pub mod config; -- cgit