summaryrefslogtreecommitdiff
path: root/legacy_utils
diff options
context:
space:
mode:
Diffstat (limited to 'legacy_utils')
-rw-r--r--legacy_utils/cfg_file/Cargo.toml23
-rw-r--r--legacy_utils/cfg_file/cfg_file_derive/Cargo.toml11
-rw-r--r--legacy_utils/cfg_file/cfg_file_derive/src/lib.rs130
-rw-r--r--legacy_utils/cfg_file/cfg_file_test/Cargo.toml9
-rw-r--r--legacy_utils/cfg_file/cfg_file_test/src/lib.rs95
-rw-r--r--legacy_utils/cfg_file/src/config.rs263
-rw-r--r--legacy_utils/cfg_file/src/lib.rs7
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;