From 42150451eafb18894eee754ed4a674fbde60b6c7 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sun, 7 Sep 2025 12:05:11 +0330 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=94=A7=20Update=20the=20Cargo.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a70bf9e..6b6c40c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "fast_config" -version = "1.2.1" -edition = "2021" -authors = ["FlooferLand"] +version = "1.3.0" +edition = "2024" +authors = ["FlooferLand", "Younes Torshizi "] description = "A small and simple multi-format crate to handle config files" keywords = ["settings", "config", "configuration", "simple", "json5"] categories = ["config"] @@ -26,17 +26,17 @@ env_logger = "0.11" [dependencies] serde = { version = "1.0", features = ["derive"], optional = false } log = "0.4" -thiserror = "1.0" +thiserror = "2.0" # Optional json5 = { version = "0.4", optional = true } -toml = { version = "0.8", optional = true } +toml = { version = "0.9", optional = true } serde_yml = { version = "0.0", optional = true } serde_json = { version = "1.0", optional = true } [features] default = [] -json = ["dep:serde_json"] +json = ["dep:serde_json"] json5 = ["dep:json5", "dep:serde_json"] -toml = ["dep:toml"] -yaml = ["dep:serde_yml"] +toml = ["dep:toml"] +yaml = ["dep:serde_yml"] From ddf391d76cd3f960bc4e98fd4adec849896f63ec Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Mon, 8 Sep 2025 11:57:41 +0330 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=8E=A8=20Macro=20implemented,=20tes?= =?UTF-8?q?ts=20passed.=20docs=20required?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 45 +--- README.md | 44 ++-- examples/advanced.rs | 66 ++---- examples/simple.rs | 25 +- fast_config/Cargo.toml | 43 ++++ fast_config/src/lib.rs | 120 ++++++++++ fast_config/src/tests.rs | 127 ++++++++++ fast_config_derive/Cargo.toml | 21 ++ fast_config_derive/src/lib.rs | 46 ++++ scripts/test.sh | 0 src/error.rs | 105 --------- src/error_messages.rs | 73 ------ src/extensions.rs | 18 -- src/format_dependant.rs | 109 --------- src/lib.rs | 427 ---------------------------------- src/tests.rs | 236 ------------------- src/utils.rs | 11 - 17 files changed, 417 insertions(+), 1099 deletions(-) create mode 100644 fast_config/Cargo.toml create mode 100644 fast_config/src/lib.rs create mode 100644 fast_config/src/tests.rs create mode 100644 fast_config_derive/Cargo.toml create mode 100644 fast_config_derive/src/lib.rs mode change 100644 => 100755 scripts/test.sh delete mode 100644 src/error.rs delete mode 100644 src/error_messages.rs delete mode 100644 src/extensions.rs delete mode 100644 src/format_dependant.rs delete mode 100644 src/lib.rs delete mode 100644 src/tests.rs delete mode 100644 src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 6b6c40c..60bad6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,42 +1,3 @@ -[package] -name = "fast_config" -version = "1.3.0" -edition = "2024" -authors = ["FlooferLand", "Younes Torshizi "] -description = "A small and simple multi-format crate to handle config files" -keywords = ["settings", "config", "configuration", "simple", "json5"] -categories = ["config"] -exclude = ["src/tests.rs", "test.cmd"] - -# GitHub stuff -readme = "README.md" -license = "MIT" -documentation = "https://docs.rs/fast_config" -repository = "https://github.com/FlooferLand/fast_config" - -[package.metadata.docs.rs] -all-features = true - -[badges] -maintenance = { status = "actively-developed" } - -[dev-dependencies] -env_logger = "0.11" - -[dependencies] -serde = { version = "1.0", features = ["derive"], optional = false } -log = "0.4" -thiserror = "2.0" - -# Optional -json5 = { version = "0.4", optional = true } -toml = { version = "0.9", optional = true } -serde_yml = { version = "0.0", optional = true } -serde_json = { version = "1.0", optional = true } - -[features] -default = [] -json = ["dep:serde_json"] -json5 = ["dep:json5", "dep:serde_json"] -toml = ["dep:toml"] -yaml = ["dep:serde_yml"] +[workspace] +resolver = "3" +members = ["fast_config", "fast_config_derive"] diff --git a/README.md b/README.md index c9a5399..8445fbf 100644 --- a/README.md +++ b/README.md @@ -63,33 +63,39 @@ anything that isn't working as expected! ## Examples: ```rust use fast_config::Config; -use serde::{Serialize, Deserialize}; +use fast_config::FastConfig; +use fast_config::Format; +use serde::Serialize; +use serde::Deserialize; // Creating a config struct to store our data -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, FastConfig)] pub struct MyData { pub student_debt: i32, } fn main() { - // Initializing a logging system (needed to show some warnings/errors) - env_logger::init(); - // Creating our data (default values) - let data = MyData { - student_debt: 20, - }; - - // Creating a new config struct with our data struct - let mut config = Config::new("./config/myconfig.json5", data).unwrap(); - - // Read/writing to the data - println!("I am ${} in debt", config.data.student_debt); - config.data.student_debt = i32::MAX; - println!("Oh no, i am now ${} in debt!!", config.data.student_debt); - - // Saving it back to the disk - config.save().unwrap(); + let config_path = "test/myconfig.json5"; + // Creating our data (default values) + let mut data = MyData { + student_debt: 20 + }; + # // use save to create the file for test + # data.save(config_path, Format::JSON5).unwrap(); + // load the data from the file + data.load(config_path, Format::JSON5).unwrap(); + + // Read/writing to the data + println!("I am ${} in debt", data.student_debt); + data.student_debt = i32::MAX; + println!("Oh no, i am now ${} in debt!!", data.student_debt); + + // Saving it back to the disk + data.save(config_path, Format::JSON5).unwrap(); + + // clean up + # std::fs::remove_dir_all("test").unwrap(); } ``` diff --git a/examples/advanced.rs b/examples/advanced.rs index 828331e..1c89c71 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -1,20 +1,22 @@ -use fast_config::{Config, ConfigSetupOptions}; -use fast_config::error::{ConfigError, DataParseError}; -use serde::{Serialize, Deserialize}; +use fast_config::Config; +use fast_config::FastConfig; +use fast_config::Format; +use serde::Deserialize; +use serde::Serialize; // Sub-structs #[derive(Serialize, Deserialize)] pub struct Person { pub name: String, pub age: u64, - pub skill_issue: bool + pub skill_issue: bool, } // Creating a config struct to store our data -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, FastConfig)] pub struct MyData { pub student_debt: i32, - pub person: Person + pub person: Person, } // Setting the default values for the data @@ -25,55 +27,25 @@ impl Default for MyData { person: Person { name: "Joe Mama".into(), age: 400, - skill_issue: true - } + skill_issue: true, + }, } } } fn main() { - // Initializing a logging system (needed to show some warnings/errors) - env_logger::init(); - - // Creating options - let options = ConfigSetupOptions { - pretty: false, - ..Default::default() - }; - - // Creating a new config struct with our data struct (it can also guess the file extension) - let result = Config::from_options( - "./config/myconfig", - options, - MyData::default() - ); + let config_path = "./config/myconfig.json5"; + // Creating our data (default values) + let mut data = MyData::default(); - // Error matching - let mut config = match result { - Ok(cfg) => { - cfg - } - Err(error) => { - match error { - // Failed parsing the config - ConfigError::DataParseError(parse_err) => { - match parse_err { - DataParseError::Serialize(format) => - panic!("Failed to serialize format {format}!"), - DataParseError::Deserialize(format, _string) => - panic!("Failed to deserialize format {format}!") - } - } - _ => panic!("Other error!") - } - } - }; + // load the data from the file + data.load(config_path, Format::JSON5).unwrap(); // Read/writing to the data - println!("I am ${} in debt", config.data.student_debt); - config.data.student_debt = i32::MAX; - println!("Oh no, i am now ${} in debt!!", config.data.student_debt); + println!("I am ${} in debt", data.student_debt); + data.student_debt = i32::MAX; + println!("Oh no, i am now ${} in debt!!", data.student_debt); // Saving it back to the disk - config.save().unwrap(); + data.save(config_path, Format::JSON5).unwrap(); } diff --git a/examples/simple.rs b/examples/simple.rs index 649381a..960c623 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,29 +1,30 @@ use fast_config::Config; -use serde::{Serialize, Deserialize}; +use fast_config::FastConfig; +use fast_config::Format; +use serde::Serialize; +use serde::Deserialize; // Creating a config struct to store our data -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, FastConfig)] pub struct MyData { pub student_debt: i32 } fn main() { - // Initializing a logging system (needed to show some warnings/errors) - env_logger::init(); - + let config_path = "./config/myconfig.json5"; // Creating our data (default values) - let data = MyData { + let mut data = MyData { student_debt: 20 }; - // Creating a new config struct with our data struct - let mut config = Config::new("./config/myconfig.json5", data).unwrap(); + // load the data from the file + data.load(config_path, Format::JSON5).unwrap(); // Read/writing to the data - println!("I am ${} in debt", config.data.student_debt); - config.data.student_debt = i32::MAX; - println!("Oh no, i am now ${} in debt!!", config.data.student_debt); + println!("I am ${} in debt", data.student_debt); + data.student_debt = i32::MAX; + println!("Oh no, i am now ${} in debt!!", data.student_debt); // Saving it back to the disk - config.save().unwrap(); + data.save(config_path, Format::JSON5).unwrap(); } diff --git a/fast_config/Cargo.toml b/fast_config/Cargo.toml new file mode 100644 index 0000000..78a69da --- /dev/null +++ b/fast_config/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "fast_config" +version = "2.0.0" +edition = "2024" +authors = ["FlooferLand", "Younes Torshizi "] +description = "A small and simple multi-format crate to handle config files" +keywords = ["settings", "config", "configuration", "simple", "json5"] +categories = ["config"] +exclude = ["src/tests.rs", "test.cmd"] + +# GitHub stuff +readme = "README.md" +license = "MIT" +documentation = "https://docs.rs/fast_config" +repository = "https://github.com/FlooferLand/fast_config" + +[package.metadata.docs.rs] +all-features = true + +[badges] +maintenance = { status = "actively-developed" } + + +[dependencies] +serde = { version = "1.0", features = ["derive"], optional = false } +thiserror = "2.0" + +# Optional +fast_config_derive = { path = "../fast_config_derive", optional = true } +json5 = { version = "0.4", optional = true } +toml = { version = "0.9", optional = true } +serde_yml = { version = "0.0.11", optional = true } +serde_json = { version = "1.0", optional = true } + +[features] +default = ["all"] +all = ["json", "json5", "toml", "yaml", "derive"] + +derive = ["dep:fast_config_derive"] +json = ["dep:serde_json"] +json5 = ["dep:json5", "dep:serde_json"] +toml = ["dep:toml"] +yaml = ["dep:serde_yml"] diff --git a/fast_config/src/lib.rs b/fast_config/src/lib.rs new file mode 100644 index 0000000..a3ea346 --- /dev/null +++ b/fast_config/src/lib.rs @@ -0,0 +1,120 @@ +#![doc = include_str!("../../README.md")] +use serde::Deserialize; +use serde::Serialize; + +use std::path::Path; + +#[cfg(not(any( + feature = "json", + feature = "json5", + feature = "toml", + feature = "yaml" +)))] +compile_error!("You must install at least one format feature: `json`, `json5`, `toml`, or `yaml`"); + + +#[cfg(feature = "derive")] +extern crate fast_config_derive; + +/// Derive macro available if serde is built with `features = ["derive"]`. +#[cfg(feature = "derive")] +pub use fast_config_derive::FastConfig; + + +/// Enum used to configure the [`Config`]s file format. +/// +/// ## ⚠️ Make sure to enable the feature flag for a format! +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum Format { + #[cfg(feature = "json")] + JSON, + #[cfg(feature = "json5")] + JSON5, + #[cfg(feature = "toml")] + TOML, + #[cfg(feature = "yaml")] + YAML, +} + + +/// The main result error type of the crate.
+/// Each type has it's own documentation. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[cfg(feature = "json")] + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[cfg(feature = "json5")] + #[error(transparent)] + Json5(#[from] json5::Error), + + #[cfg(feature = "toml")] + #[error(transparent)] + TomlSerialize(#[from] toml::ser::Error), + #[cfg(feature = "toml")] + #[error(transparent)] + TomlDeserialize(#[from] toml::de::Error), + + #[cfg(feature = "yaml")] + #[error(transparent)] + Yaml(#[from] serde_yml::Error), +} + + +pub trait Config +where + for<'a> Self: Deserialize<'a> + Serialize + Sized, +{ + fn load(&mut self, path: impl AsRef, format: Format) -> Result<(), Error>; + fn save(&self, path: impl AsRef, format: Format) -> Result<(), Error>; + fn save_pretty(&self, path: impl AsRef, format: Format) -> Result<(), Error>; + fn from_string(content: &str, format: Format) -> Result { + let result = match format { + #[cfg(feature = "json")] + Format::JSON => serde_json::from_str::(content)?, + #[cfg(feature = "json5")] + Format::JSON5 => json5::from_str::(content)?, + #[cfg(feature = "toml")] + Format::TOML => toml::from_str::(content)?, + #[cfg(feature = "yaml")] + Format::YAML => serde_yml::from_str::(content)?, + }; + Ok(result) + } + fn to_string(&self, format: Format) -> Result { + let result = match format { + #[cfg(feature = "json")] + Format::JSON => serde_json::to_string(self)?, + #[cfg(feature = "json5")] + Format::JSON5 => json5::to_string(self)?, + #[cfg(feature = "toml")] + Format::TOML => toml::to_string(self)?, + #[cfg(feature = "yaml")] + Format::YAML => serde_yml::to_string(self)?, + }; + Ok(result) + } + fn to_string_pretty(&self, format: Format) -> Result { + let result = match format { + #[cfg(feature = "json")] + Format::JSON => serde_json::to_string_pretty(self)?, + #[cfg(feature = "json5")] + Format::JSON5 => json5::to_string(self)?, + #[cfg(feature = "toml")] + Format::TOML => toml::to_string_pretty(self)?, + #[cfg(feature = "yaml")] + Format::YAML => serde_yml::to_string(self)?, + }; + Ok(result) + } +} + +#[cfg(test)] +#[cfg(feature = "derive")] +mod tests; + + diff --git a/fast_config/src/tests.rs b/fast_config/src/tests.rs new file mode 100644 index 0000000..5715265 --- /dev/null +++ b/fast_config/src/tests.rs @@ -0,0 +1,127 @@ +#![allow(dead_code)] +use crate as fast_config; +use crate::Config; +use crate::FastConfig; +use crate::Format::*; + +use serde::Deserialize; +use serde::Serialize; + +use std::path::PathBuf; +// Sub-data +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct SubData { + pub string: String, + pub unsigned: u64, + pub boolean: bool, +} + +// Data +#[derive(Serialize, Deserialize, PartialEq, Debug, FastConfig)] +pub struct MyData { + pub number: i32, + pub subdata: SubData, +} +impl Default for MyData { + fn default() -> Self { + Self { + number: 20, + subdata: SubData { + string: "Joe Mama".into(), + unsigned: 400, + boolean: true, + }, + } + } +} + +struct Setup<'a> { + path: PathBuf, + manager: &'a Manager, +} + +impl<'a> Drop for Setup<'a> { + fn drop(&mut self) { + if self + .manager + .0 + .fetch_sub(1, std::sync::atomic::Ordering::SeqCst) + == 1 + { + std::fs::remove_dir_all(&self.path).expect("Failed to remove test directory"); + } + } +} + +struct Manager(std::sync::atomic::AtomicUsize); +impl Manager { + fn setup<'a>(&'a self) -> Setup<'a> { + self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Setup { + path: PathBuf::from("../config/"), + manager: self, + } + } +} + +static MANAGER: Manager = Manager(std::sync::atomic::AtomicUsize::new(0)); + +#[cfg(feature = "json")] +#[test] +fn save_load_json() { + let c = MANAGER.setup(); + let path = c.path.join("config.json"); + let mut to_save = MyData::default(); + to_save.number = i32::MAX; + to_save.save(&path, JSON).unwrap(); + + let mut to_load = MyData::default(); + to_load.load(&path, JSON).unwrap(); + + assert_eq!(to_load, to_save); +} + +#[cfg(feature = "json5")] +#[test] +fn save_load_json5() { + let c = MANAGER.setup(); + let path = c.path.join("config.json5"); + let mut to_save = MyData::default(); + to_save.number = i32::MAX; + to_save.save(&path, JSON5).unwrap(); + + let mut to_load = MyData::default(); + to_load.load(&path, JSON5).unwrap(); + + assert_eq!(to_load, to_save); +} + +#[cfg(feature = "toml")] +#[test] +fn save_load_toml() { + let c = MANAGER.setup(); + let path = c.path.join("config.toml"); + let mut to_save = MyData::default(); + to_save.number = i32::MAX; + to_save.save(&path, TOML).unwrap(); + + let mut to_load = MyData::default(); + to_load.load(&path, TOML).unwrap(); + + assert_eq!(to_load, to_save); +} + +#[cfg(feature = "yaml")] +#[test] +fn save_load_yaml() { + let c = MANAGER.setup(); + let path = c.path.join("config.yaml"); + let mut to_save = MyData::default(); + to_save.number = i32::MAX; + to_save.save(&path, YAML).unwrap(); + + let mut to_load = MyData::default(); + to_load.load(&path, YAML).unwrap(); + + assert_eq!(to_load, to_save); +} diff --git a/fast_config_derive/Cargo.toml b/fast_config_derive/Cargo.toml new file mode 100644 index 0000000..dbd2d0a --- /dev/null +++ b/fast_config_derive/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fast_config_derive" +version = "0.1.0" +authors = ["Younes Torshizi "] +edition = "2024" + +[lib] +name = "fast_config_derive" +proc-macro = true + + +[dependencies] +proc-macro2 = { version = "1", features = ["proc-macro"] } +quote = { version = "1", features = ["proc-macro"] } +syn = { version = "2", features = [ + # "clone-impls", + # "derive", + # "parsing", + # "printing", + # "proc-macro", +] } diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs new file mode 100644 index 0000000..3e652a1 --- /dev/null +++ b/fast_config_derive/src/lib.rs @@ -0,0 +1,46 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::DeriveInput; +use syn::parse_macro_input; + +#[proc_macro_derive(FastConfig)] +pub fn derive_config(input: TokenStream) -> TokenStream { + let crate_path = quote! {fast_config}; + let path_type = quote! {impl AsRef}; + + let input = parse_macro_input!(input as DeriveInput); + + let ident = &input.ident; + + quote! { + impl #crate_path::Config for #ident { + fn load(&mut self, path: #path_type, format: #crate_path::Format ) -> Result<(), #crate_path::Error> { + let mut content = String::new(); + let mut file = std::fs::File::open(path)?; + std::io::Read::read_to_string(&mut file, &mut content)?; + *self = #crate_path::Config::from_string(&content, format)?; + Ok(()) + } + fn save(&self, path: #path_type, format: #crate_path::Format ) -> Result<(), #crate_path::Error> { + if let Some(parent_dir) = path.as_ref().parent() { + std::fs::create_dir_all(parent_dir)?; + } + let mut file = std::fs::File::create(path)?; + let content = #crate_path::Config::to_string(self, format)?; + use std::io::Write; + write!(file, "{}", content)?; + Ok(()) + } + fn save_pretty(&self, path: #path_type, format: #crate_path::Format ) -> Result<(), #crate_path::Error> { + if let Some(parent_dir) = path.as_ref().parent() { + std::fs::create_dir_all(parent_dir)?; + } + let mut file = std::fs::File::create(path)?; + let content = #crate_path::Config::to_string_pretty(self, format)?; + use std::io::Write; + write!(file, "{}", content)?; + Ok(()) + } + } + }.into() +} \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh old mode 100644 new mode 100755 diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index ff8e4dd..0000000 --- a/src/error.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::path::PathBuf; -#[allow(unused)] -use serde::de::value::Error; -use thiserror::Error; -use crate::ConfigFormat; - -// - Some of the display/error traits are implemented with -// `thiserror` to save time and source code readability -// - Other are however implemented manually -// inside `error_messages.rs` - -/// Represents an error related to serialization/deserialization of your data -#[derive(Debug)] -pub enum DataParseError { - /// Serialization: From an object, to a string (stringification) - /// - Stores the format that failed - Serialize(ConfigFormat), - - /// Deserialization: From a string, to an object (objectification) - /// - Stores the format that failed, as well as the data in string form - Deserialize(ConfigFormat, String) -} - -/// Represents an error related to the file format not being able to be found or guessed -#[derive(Debug)] -pub struct UnknownFormatError { - /// The error message itself - pub message: Option, - - /// The [`ConfigFormat`]s found in your environment. - pub found_formats: Vec -} -impl UnknownFormatError { - pub fn new(message: Option, found_formats: Vec) -> Self { - Self { message, found_formats } - } -} - -/// The main result error type of the crate.
-/// Each type has it's own documentation. -#[derive(Error, Debug)] -pub enum ConfigError { - /// Occurs when a file isn't composed of valid UTF-8 characters. - /// - Stores the path to the erroring file - #[error("InvalidFileEncoding: Failed to read file data of \"{:?}\" into a valid UTF-8 string.", .0)] - InvalidFileEncoding(std::io::Error, PathBuf), - - /// Occurs when the file could not be saved due to filesystem-related errors.
- /// Usually when one of the parent directories for the config file could not - /// be located or automatically created. - /// - Stores the [`std::io::Error`] in question - #[error(transparent)] - IoError(std::io::Error), - - /// Occurs when Serde fails to serialize/deserialize your data - /// - Stores an enum containing the section of data parsing failed for - /// (serialization/deserialization)
- #[error(transparent)] - DataParseError(DataParseError), - - /// Occurs when `fast_config` cannot guess what format your data should be. - /// - Stores some data related to the error - /// - /// # This error can be avoided by either: - /// 1. Adding a file extension at the end of the path name - /// (`json`/`json5`, `toml`, `yaml`/`yml`)
- /// *(It would be appreciated if you create an issue on the project's Github if - /// you notice an extension type is missing)* - /// 2. Passing a `ConfigSetupOptions` struct into `Config::from_options`, and defining - /// the format there. - /// 3. Or only having one enabled `format` feature in your `cargo.toml` - #[error(transparent)] - UnknownFormat(UnknownFormatError) -} - -impl From for ConfigError { - fn from(item: std::io::Error) -> Self { - ConfigError::IoError(item) - } -} - -impl From for ConfigError { - fn from(item: DataParseError) -> Self { - ConfigError::DataParseError(item) - } -} - -#[derive(Error, Debug)] -pub enum ConfigSaveError { - /// Occurs when the file could not be saved due to filesystem-related errors. - /// - Stores the [`std::io::Error`] in question - #[error(transparent)] - IoError(std::io::Error), - - /// Occurs when the save data could not be serialized.
- /// - Stores an error in string form explaining why serialization failed. - #[error("{}", .0)] - SerializationError(String) -} - -impl From for ConfigSaveError { - fn from(item: std::io::Error) -> Self { - ConfigSaveError::IoError(item) - } -} diff --git a/src/error_messages.rs b/src/error_messages.rs deleted file mode 100644 index 7b2944c..0000000 --- a/src/error_messages.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt::Formatter; -use crate::error::{DataParseError, UnknownFormatError}; - -// - This module serves as a way to print out useful error messages -// for both the end user, and the developer. -// ------------------------------------------------------------------ -// #[cfg(debug_assertions)] - are developer-shown errors -// #[cfg(not(debug_assertions))] - are user-shown errors -// ------------------------------------------------------------------ - - -// Data parsing error -impl std::error::Error for DataParseError {} -impl std::fmt::Display for DataParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - // Object to string - DataParseError::Serialize(_format) => { - let tip = { - #[cfg(debug_assertions)] { - "Your config's data types must all implement Serialize and Deserialize!" - } - #[cfg(not(debug_assertions))] { - "This is likely an issue caused by the `Serialize` implementation of the program you are using." - } - }; - write!(f, "Serialization: An error occurred trying to convert the config to a string.\n - [tip]: {tip}") - }, - // String to object - DataParseError::Deserialize(format, _string) => { - let tip = { - #[cfg(debug_assertions)] { - "Make sure your data structs types/names match up with the config file you're trying to read.\n - Alternatively, make sure all of your types implement serde::Deserialize and Serialize!\n - -- You might want to:\n - 1. Check that the format feature you're trying to use is enabled in your `cargo.toml` (JSON, TOML, YAML, etc)\n - 2. Check that your data is valid (some types like vectors and custom types cannot be converted to Serde by default, you might want to implement Deserialize and Serialize for them manually)\n - 3. Report this bug to the project's \"Issues\" page if nothing seems to be solving the issue (https://github.com/FlooferLand/fast_config/issues)" - } - #[cfg(not(debug_assertions))] { - "If you edited a config file, make sure you were following the configuration format's syntax rules!" - } - }; - write!(f, "Deserialization: An error occurred trying to convert a string into a config object.\n - [err] Config file isn't valid according to it's format ({format})\n - [tip]: {tip}") - } - } - } -} - -// Unknown file format (json, toml, etc) -impl std::error::Error for UnknownFormatError {} -impl std::fmt::Display for UnknownFormatError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let error_message; - if let Some(message) = &self.message { - error_message = format!("Error: {message}"); - } else { - error_message = String::new(); - } - - write!(f, "The format had to be guessed from {} other features.\ - {error_message} - \nYou should consider:\ - \n- Adding a file extension at the end of your config file's path\ - \n- Passing a `ConfigSetupOptions` struct into `Config::from_options`\ - \n- Enabling only one format in the fast_config features", - self.found_formats.len() - ) - } -} diff --git a/src/extensions.rs b/src/extensions.rs deleted file mode 100644 index 6cee023..0000000 --- a/src/extensions.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::fmt::Debug; -use std::result::Result; - -pub type GenericResult = Result; - -pub trait ResultGeneralize where E: Debug { - fn generalize(self) -> GenericResult; -} - -impl ResultGeneralize for Result where E: Debug { - fn generalize(self: Result) -> GenericResult { - match self { - Ok(value) => Ok(value), - Err(error) => Err(format!("{:?}", error)) - } - } -} - diff --git a/src/format_dependant.rs b/src/format_dependant.rs deleted file mode 100644 index 03c3d95..0000000 --- a/src/format_dependant.rs +++ /dev/null @@ -1,109 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; -use crate::{ConfigFormat, InternalOptions}; -use crate::extensions::GenericResult; - -// Fixes an unused warning when the user hasn't selected any format -#[cfg(any(feature = "json", feature = "json5", feature = "toml", feature = "yaml"))] -use crate::extensions::ResultGeneralize; - -// Getting the enabled features via code -pub fn get_enabled_features() -> Vec { - vec![ - #[cfg(feature = "json")] ConfigFormat::JSON, - #[cfg(feature = "json5")] ConfigFormat::JSON5, - #[cfg(feature = "toml")] ConfigFormat::TOML, - #[cfg(feature = "yaml")] ConfigFormat::YAML, - ] -} - -// Getting a singular enabled feature -// SAFETY: Should only be used if there is exactly one feature enabled -pub fn get_first_enabled_feature() -> ConfigFormat { - let features = get_enabled_features(); - if let Some(first) = features.first() { - // If there is one feature - *first - } else if features.is_empty() { - // If there is no feature - panic!("No file formats installed or selected. You must enable at least one format feature"); - } else { - // If there are multiple features - // TODO/FIXME: Unpredictable code, should return an Option or a Result! - let first = features[0]; - log::warn!("Too many format features enabled, with no format specified in the extension or the config's settings."); - log::warn!("Defaulting to picking the first available format.. ({:?})", &first); - first - } -} - -// Creates a new string from an existing data object (Serialization) -pub fn to_string(value: &D, options: &InternalOptions) -> GenericResult where D: Serialize { - match options.format { - #[cfg(feature = "json")] - ConfigFormat::JSON => { - match options.pretty { - true => serde_json::to_string_pretty(value).generalize(), - false => serde_json::to_string(value).generalize() - } - }, - - #[cfg(feature = "json5")] - ConfigFormat::JSON5 => { - json5::to_string(value).generalize() - }, - - #[cfg(feature = "toml")] - ConfigFormat::TOML => { - match options.pretty { - true => toml::to_string_pretty(value).generalize(), - false => toml::to_string(value).generalize() - } - }, - - #[cfg(feature = "yaml")] - ConfigFormat::YAML => { - match options.pretty { - true => serde_yml::to_string(value).generalize(), - false => { - let string = serde_yml::to_string(value); - if string.is_err() { - return Err(string.err().unwrap().to_string()); - } - Ok(crate::utils::compress_string(string.unwrap())) - } - } - }, - - // Note: This is here to stop unused pattern warns/errors - #[cfg(not(all(feature = "json", feature = "json5", feature = "toml", feature = "yaml")))] - _ => Err(format!("Missing feature for format \"{}\". Try enabling it in your Cargo.toml", options.format)) - } -} - - -// Creates a new data object from a string (Deserialization) -pub fn from_string(value: &str, format: &ConfigFormat) -> GenericResult where D: DeserializeOwned { - match format { - #[cfg(feature = "json")] - ConfigFormat::JSON => - serde_json::from_str::(value).generalize(), - - #[cfg(feature = "json5")] - ConfigFormat::JSON5 => - json5::from_str::(value).generalize(), - - #[cfg(feature = "toml")] - ConfigFormat::TOML => - toml::from_str::(value).generalize(), - - #[cfg(feature = "yaml")] - ConfigFormat::YAML => - serde_yml::from_str::(value).generalize(), - - // Note: This is here to stop unused pattern warns/errors - #[cfg(not(all(feature = "json", feature = "json5", feature = "toml", feature = "yaml")))] - _ => Err(format!("Missing feature for format \"{}\". Try enabling it in your Cargo.toml", format)) - } -} - diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index ab53ee6..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,427 +0,0 @@ -#![doc = include_str!("../README.md")] - -pub mod error; -pub mod error_messages; -mod extensions; -mod format_dependant; -mod utils; - -use serde::{Deserialize, Serialize}; -use std::ffi::OsStr; -use std::fmt::{Display, Formatter}; -use std::fs; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; - -#[cfg(not(any(feature = "json", feature = "json5", feature = "toml", feature = "yaml")))] -compile_error!("You must install at least one format feature: `json`, `json5`, `toml`, or `yaml`"); -// ^ --- HEY, user! --- ^ -// To do this, you can replace `fast_config = ".."` with -// `fast_config = { version = "..", features = ["json"] }` in your cargo.toml file. -// You can simply replace that "json" with any of the stated above if you want other formats. - -// Bug testing -#[cfg(test)] -mod tests; - -// Separated things -#[allow(unused)] -pub use error_messages::*; - -/// Enum used to configure the [`Config`]s file format. -/// -/// You can use it in a [`ConfigSetupOptions`], inside [`Config::from_options`] -/// -/// ## ⚠️ Make sure to enable the feature flag for a format before using it! -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum ConfigFormat { - JSON, - JSON5, - TOML, - YAML, -} - -impl ConfigFormat { - /// Mainly used to convert file extensions into [`ConfigFormat`]s
- /// Also chooses the correct extension for both JSON types based on the enabled feature. _(ex: if JSON5 is enabled, it chooses it for the "json" file extension)_
- /// Returns [`None`] if the string/extension doesn't match any known format. - /// - /// # Example: - /// ``` - /// # use std::ffi::OsStr; - /// # use fast_config::ConfigFormat; - /// if cfg!(feature = "json") { - /// assert_eq!( - /// ConfigFormat::from_extension(OsStr::new("json")).unwrap(), - /// ConfigFormat::JSON - /// ); - /// } else if cfg!(feature = "json5") { - /// assert_eq!( - /// ConfigFormat::from_extension(OsStr::new("json5")).unwrap(), - /// ConfigFormat::JSON5 - /// ); - /// } - /// ``` - pub fn from_extension(ext: &OsStr) -> Option { - let ext = ext - .to_ascii_lowercase() - .to_string_lossy() - .replace('\u{FFFD}', ""); - - // Special case for JSON5 since it shares a format with JSON - if cfg!(feature = "json5") && !cfg!(feature = "json") && (ext == "json" || ext == "json5") { - return Some(ConfigFormat::JSON5); - } - - // Matching - match ext.as_str() { - "json" => Some(ConfigFormat::JSON), - "toml" => Some(ConfigFormat::TOML), - "yaml" | "yml" => Some(ConfigFormat::YAML), - _ => None, - } - } -} - -impl Display for ConfigFormat { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let output = match self { - ConfigFormat::JSON => "json", - ConfigFormat::JSON5 => "json5", - ConfigFormat::TOML => "toml", - ConfigFormat::YAML => "yaml", - }; - write!(f, "{output}") - } -} - -impl Default for ConfigFormat { - fn default() -> Self { - format_dependant::get_first_enabled_feature() - } -} - -/// Used to configure the [`Config`] object -/// -/// [`UnknownFormatError`]: error::UnknownFormatError -/// -/// # Attributes -/// - `pretty` - Makes the contents of the config file more humanly-readable. -/// When `false`, it will try to compact down the config file data so it takes up less storage space. -/// I recommend you keep it on unless you know what you're doing as most modern systems have enough -/// space to handle spaces and newline characters even at scale. -/// -/// - `format` - An [`Option`] containing an enum of type [`ConfigFormat`]. -/// Used to specify the format language to use *(ex: JSON, TOML, etc.)*
-/// If you don't select a format *(Option::None)* it will try to guess the format -/// based on the file extension and enabled features.
-/// If this step fails, an [`UnknownFormatError`] will be returned. -/// -/// # More options are to be added later! -/// Pass `.. `[`Default::default()`] at the end of your construction -/// to prevent yourself from getting errors in the future! -/// -/// # Examples: -/// ``` -/// use fast_config::{ConfigSetupOptions, ConfigFormat, Config}; -/// use serde::{Serialize, Deserialize}; -/// -/// // Creating a config struct to store our data -/// #[derive(Serialize, Deserialize)] -/// pub struct MyData { -/// pub some_data: i32 -/// } -/// -/// // Creating the options -/// let options = ConfigSetupOptions { -/// pretty: false, -/// format: Some(ConfigFormat::JSON), -/// .. Default::default() -/// }; -/// -/// // Creating the data and setting it's default values -/// let data = MyData { -/// some_data: 12345 -/// }; -/// -/// // Creating the config itself -/// let mut config = Config::from_options("./config/myconfig", options, data).unwrap(); -/// // [.. do stuff here] -/// # // Cleanup -/// # match std::fs::remove_dir_all("./config/") { -/// # Err(e) => { -/// # log::error!("{e}"); -/// # }, -/// # Ok(_) => {} -/// # } -/// ``` -#[derive(Clone, Copy)] -pub struct ConfigSetupOptions { - pub pretty: bool, - pub format: Option, - - #[allow(deprecated)] - #[deprecated(note = "This option can result in I/O during program exit and can potentially corrupt config files!\nUse [`Config::save`] while your program is exiting instead!")] - pub save_on_drop: bool, -} - -impl Default for ConfigSetupOptions { - fn default() -> Self { - #[allow(deprecated)] - Self { - pretty: true, - format: None, - save_on_drop: false, - } - } -} - -/// The internally-stored settings type for [`Config`]
-/// Works and looks like [`ConfigSetupOptions`], with a few internally-required key differences. -pub struct InternalOptions { - pub pretty: bool, - pub format: ConfigFormat, - pub save_on_drop: bool, -} -impl TryFrom for InternalOptions { - /// This function converts a [`ConfigSetupOptions`] into an internally-used [`InternalOptions`]. - /// - /// This function is not recommended to be used outside the `fast_config` source code - /// unless you know what you're doing and accept the risks.
- /// The signature or behaviour of the function may be modified in the future. - type Error = String; - - fn try_from(options: ConfigSetupOptions) -> Result { - // Getting the formatting language. - let format = match options.format { - Some(format) => format, - None => Err("The file format could not be guessed! It appears to be None!")?, - }; - - // Constructing a converted type - Ok(Self { - pretty: options.pretty, - format, - #[allow(deprecated)] save_on_drop: options.save_on_drop, - }) - } -} - -/// The main class you use to create/access your configuration files! -/// -/// # Construction -/// See [`Config::new`] and [`Config::from_options`] if you wish to construct a new `Config`! -/// -/// # Data -/// This class stores data within a data struct you define yourself. -/// This allows for the most amount of performance and safety, -/// while also allowing you to add additional features by adding `impl` blocks on your struct. -/// -/// Your data struct needs to implement [`Serialize`] and [`Deserialize`]. -/// In most cases you can just use `#[derive(Serialize, Deserialize)]` to derive them. -/// -/// # Examples -/// Here is a code example on how you could define the data to pass into the constructors on this class: -/// ``` -/// use serde::{Serialize, Deserialize}; -/// -/// // Creating a config struct to store our data -/// #[derive(Serialize, Deserialize)] -/// struct MyData { -/// pub student_debt: i32, -/// } -/// -/// // Making our data and setting its default values -/// let data = MyData { -/// student_debt: 20 -/// }; -/// // .. -/// ``` -/// Implementing [`Serialize`] and [`Deserialize`] yourself is quite complicated but will provide the most flexibility. -/// -/// *If you wish to implement them yourself I'd recommend reading the Serde docs on it* -/// -pub struct Config -where - for<'a> D: Deserialize<'a> + Serialize, -{ - pub data: D, - pub path: PathBuf, - pub options: InternalOptions, -} - -impl Config -where - for<'a> D: Deserialize<'a> + Serialize, -{ - /// Constructs and returns a new config object using the default options. - /// - /// If there is a file at `path`, the file will be opened.
- /// - /// - `path`: Takes in a path to where the config file is or should be located. - /// If the file has no extension, the crate will attempt to guess the extension from one available format `feature`. - /// - /// - `data`: Takes in a struct that inherits [`Serialize`] and [`Deserialize`] - /// You have to make this struct yourself, construct it, and pass it in. - /// More info about it is provided at [`Config`]. - /// - /// If you'd like to configure this object, you should take a look at using [`Config::from_options`] instead. - pub fn new(path: impl AsRef, data: D) -> Result, error::ConfigError> { - Self::construct(path, ConfigSetupOptions::default(), data) - } - - /// Constructs and returns a new config object from a set of custom options. - /// - /// - `path`: Takes in a path to where the config file is or should be located.
- /// If the file has no extension, and there is no `format` selected in your `options`, - /// the crate will attempt to guess the extension from one available format `feature`s. - // - /// - `options`: Takes in a [`ConfigSetupOptions`], - /// used to configure the format language, styling of the data, and other things.
- /// Remember to add `..` [`Default::default()`] at the end of your `options` as more options are - /// going to be added to the crate later on. - /// - /// - `data`: Takes in a struct that inherits [`Serialize`] and [`Deserialize`] - /// You have to make this struct yourself, construct it, and pass it in. - /// More info is provided at [`Config`]. - pub fn from_options( - path: impl AsRef, - options: ConfigSetupOptions, - data: D, - ) -> Result, error::ConfigError> { - Self::construct(path, options, data) - } - - // Main, private constructor - fn construct( - path: impl AsRef, - mut options: ConfigSetupOptions, - mut data: D, - ) -> Result, error::ConfigError> { - let mut path = PathBuf::from(path.as_ref()); - - // Setting up variables - let enabled_features = format_dependant::get_enabled_features(); - let first_enabled_feature = format_dependant::get_first_enabled_feature(); - let guess_from_feature = || { - if enabled_features.len() > 1 { - Err(error::ConfigError::UnknownFormat( - error::UnknownFormatError::new(None, enabled_features.clone()), - )) - } else { - Ok(Some(first_enabled_feature)) - } - }; - - // Manual format option > file extension > guessed feature - if options.format.is_none() { - options.format = match path.extension() { - Some(extension) => { - // - Based on the extension - match ConfigFormat::from_extension(extension) { - Some(value) => Some(value), - None => guess_from_feature()?, - } - } - _ => { - // - Guessing based on the enabled features - guess_from_feature()? - } - }; - } - - // Converting the user options into a more convenient internally-used type - let options: InternalOptions = match InternalOptions::try_from(options) { - Ok(value) => value, - Err(message) => { - return Err(error::ConfigError::UnknownFormat( - error::UnknownFormatError::new(Some(message), enabled_features), - )); - } - }; - - // Setting the file format - if path.extension().is_none() { - path.set_extension(options.format.to_string()); - } - - // Reading from the file if a file was found - if let Ok(mut file) = fs::File::open(&path) { - let mut content = String::new(); - if let Err(err) = file.read_to_string(&mut content) { - return Err(error::ConfigError::InvalidFileEncoding(err, path)); - }; - - // Deserialization - // (Getting data from a string) - if let Ok(value) = format_dependant::from_string(&content, &options.format) { - data = value; - } else { - return Err(error::ConfigError::DataParseError( - error::DataParseError::Deserialize(options.format, content), - )); - }; - } - - // Returning the Config object - - Ok(Self { - data, - path, - options, - }) - } - - /// Saves the config file to the disk. - /// - /// It uses the [`Config`]'s object own internal `path` property to get the path required to save the file - /// so there is no need to pass in the path to save it at. - /// - /// If you wish to specify the path to save it at - /// you can change the path yourself by setting the Config's `path` property. - ///

- /// ## save_at method - /// There used to be a built-in function called `save_at` while i was developing the crate, - /// but I ended up removing it due to the fact i didn't see anyone actually using it, - /// and it might've ended up in some users getting confused, as well as a tiny bit of performance overhead. - /// - /// If you'd like this feature to be back feel free to open an issue and I'll add it back right away! - pub fn save(&self) -> Result<(), error::ConfigSaveError> { - let to_string = format_dependant::to_string(&self.data, &self.options); - match to_string { - // If the conversion was successful - Ok(data) => { - if let Some(parent_dir) = self.path.parent() { - fs::create_dir_all(parent_dir)?; - }; - - let mut file = fs::File::create(&self.path)?; - - write!(file, "{data}")?; - } - // If the conversion failed - Err(e) => { - // This error triggering sometimes seems to mean a data type you're using in your - // custom data struct isn't supported, but I haven't fully tested it. - return Err(error::ConfigSaveError::SerializationError(e)); - } - }; - Ok(()) - } - - /// Gets the name of the config file - pub fn filename(&self) -> String { - self.path.file_name().unwrap().to_string_lossy().to_string() - } -} - -impl Drop for Config -where - for<'a> D: Deserialize<'a> + Serialize, -{ - fn drop(&mut self) { - if self.options.save_on_drop { - let _ = self.save(); - } - } -} diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index 8cdb8db..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,236 +0,0 @@ -#![allow(dead_code)] - -use crate::{format_dependant, Config, ConfigSetupOptions}; -use crate::{Deserialize, Serialize}; -use log::LevelFilter; - -// Sub-data -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct SubData { - pub string: String, - pub unsigned: u64, - pub boolean: bool, -} - -// Data -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct MyData { - pub number: i32, - pub subdata: SubData, -} -impl Default for MyData { - fn default() -> Self { - Self { - number: 20, - subdata: SubData { - string: "Joe Mama".into(), - unsigned: 400, - boolean: true, - }, - } - } -} - -#[test] -fn run() { - // Logging - let _ = env_logger::builder() - .is_test(true) - .filter_level(LevelFilter::Info) - .try_init(); - - // Creating options - let options = ConfigSetupOptions { - pretty: true, - format: { - // These test the format auto-picking - // Chooses JSON by default when all features are enabled; in the normal library this would throw an error - #[cfg(all(feature = "json", feature = "json5", feature = "toml", feature = "yaml"))] { - Some(crate::ConfigFormat::JSON) - } - #[cfg(not(all(feature = "json", feature = "json5", feature = "toml", feature = "yaml")))] { - None - } - }, - ..Default::default() - }; - - // Creating the config and saving it - { - let mut config = - Config::from_options("./config/testconfig", options, MyData::default()).unwrap(); - config.data.number = i32::MAX; - config.save().unwrap(); - } - - // Reading from that config + assertions - { - // Test data - let data = MyData::default(); - let config = Config::from_options("./config/testconfig", options, data).unwrap(); - let default = MyData::default(); - assert_eq!(config.data.number, i32::MAX); - assert_eq!(config.data.subdata.string, default.subdata.string); - assert_eq!(config.data.subdata.unsigned, default.subdata.unsigned); - assert_eq!(config.data.subdata.boolean, default.subdata.boolean); - } - - // Advanced test - if let Ok(value) = std::env::var("ADVANCED_TEST") { - if !value.is_empty() { - advanced_test(); - } - } - - // Cleanup - std::thread::sleep(std::time::Duration::from_millis(20)); - match std::fs::remove_dir_all("./config/") { - Ok(_) => {} - Err(e) => { - log::error!("{e}"); - } - } -} - -// Called by `run` when ADVANCED_TEST env argument is enabled -fn advanced_test() { - #[derive(Debug)] - pub enum FormatFinder { - GuessExtension(String), - Config(crate::ConfigFormat), - Feature, - } - - #[derive(Debug)] - pub struct Case { - pub format_finder: FormatFinder, - pub pretty: bool, - } - - impl Case { - pub fn new(format_finder: FormatFinder, pretty: bool) -> Self { - Self { - format_finder, - pretty, - } - } - } - - // Adding all different possible cases - // Could probably be made slightly faster and cleaner by - // being moved into an array via a macro - let available = format_dependant::get_enabled_features(); - let mut cases = Vec::with_capacity( - 2 /* how many `push` calls there are in the for loop below */ - * 2, /* `pretty` switches (for _ in 0..2) */ - ); - let mut pretty = false; - for _ in 0..2 { - for format in &available { - // TODO: Add the GuessExtension test case back in (and update cases vec above accordingly) - // Currently this is bugged because of the new JSON/JSON5 extension guessing - //cases.push(Case::new( - // FormatFinder::GuessExtension(format.to_string()), - // pretty, - //)); - - cases.push(Case::new(FormatFinder::Config(*format), pretty)); - #[cfg(not(all(feature = "json", feature = "json5", feature = "toml", feature = "yaml")))] { - cases.push(Case::new(FormatFinder::Feature, pretty)); - } - } - pretty = !pretty; - } - - // Automated case-based tests - println!("######## Case test started! ########"); - println!("| All errors will now be split into sections |"); - for case in cases { - let mut path = String::from("./config/advtestconfig"); - let mut format = None; - match case.format_finder { - FormatFinder::GuessExtension(ext) => { - println!("\n\n------ GUESS EXTENSION ------ "); - path += format!(".{ext}").as_str(); - } - FormatFinder::Config(fmt) => { - println!("\n\n---------- CONFIG ----------- "); - format = Some(fmt); - } - FormatFinder::Feature => { - println!("\n\n---------- FEATURE ---------- "); - format = Some(format_dependant::get_first_enabled_feature()); - } - }; - - // Creating options - let options = ConfigSetupOptions { - pretty: case.pretty, - format, - ..Default::default() - }; - - // Creating the config and saving it - { - let config = Config::from_options(&path, options, MyData::default()).unwrap(); - config.save().unwrap(); - } - - // Reading from that config + assertions - { - // Test data - let data = MyData::default(); - let config = Config::from_options(&path, options, data).unwrap(); - let default = MyData::default(); - assert_eq!(config.data.number, default.number); - assert_eq!(config.data.subdata.string, default.subdata.string); - assert_eq!(config.data.subdata.unsigned, default.subdata.unsigned); - assert_eq!(config.data.subdata.boolean, default.subdata.boolean); - } - } -} - -// what happens if no existing config file? -#[test] -fn no_create_if_missing() { - const CONFIG_FILE_PATH: &str = "./config/testconfig"; - - // Logging - let _ = env_logger::builder() - .is_test(true) - .filter_level(LevelFilter::Info) - .try_init(); - - // Creating options - let options = ConfigSetupOptions { - pretty: true, - format: { - #[cfg(feature = "toml")] { - Some(crate::ConfigFormat::TOML) - } - #[cfg(not(feature = "toml"))] { - None - } - }, - ..Default::default() - }; - - let default = MyData::default(); - - assert!( - std::fs::metadata(CONFIG_FILE_PATH).is_err(), - "precondition: no config file" - ); - - let config = Config::from_options(CONFIG_FILE_PATH, options, default).unwrap(); - - assert_eq!( - config.data, - MyData::default(), - "post_test: config == defaults" - ); - - let md = std::fs::metadata(CONFIG_FILE_PATH); - log::error!("md is {:?}", md); - assert!(md.is_err(), "should not have created config file"); -} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index d7fbafa..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Gets rid of unnecessary lines -#[cfg(feature = "yaml")] -pub fn compress_string(string: String) -> String { - let mut result = String::new(); - for line in string.split('\n') { - let new_line = line.trim_end(); - if new_line.is_empty() { continue } - result += format!("{}\n", new_line).as_str(); - } - result -} From 88246afb722503c0cd21a71102eed9648a36efe4 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Mon, 8 Sep 2025 12:33:07 +0330 Subject: [PATCH 03/13] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Set=20default=20feat?= =?UTF-8?q?ures=20to=20[]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast_config/Cargo.toml b/fast_config/Cargo.toml index 78a69da..d8c0ba5 100644 --- a/fast_config/Cargo.toml +++ b/fast_config/Cargo.toml @@ -33,7 +33,7 @@ serde_yml = { version = "0.0.11", optional = true } serde_json = { version = "1.0", optional = true } [features] -default = ["all"] +default = [] all = ["json", "json5", "toml", "yaml", "derive"] derive = ["dep:fast_config_derive"] From 0a398dcc07a48f0cd43038c6d107645a31371e06 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Mon, 8 Sep 2025 12:55:42 +0330 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20clippy=20lints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 47 +++++++++++++++---------------- fast_config/Cargo.toml | 2 +- fast_config/src/lib.rs | 64 +++++++++++++++++++++++++----------------- 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 8445fbf..d160046 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,11 @@ anything that isn't working as expected! ## Examples: ```rust -use fast_config::Config; -use fast_config::FastConfig; -use fast_config::Format; -use serde::Serialize; -use serde::Deserialize; +# use fast_config::Config; +# use fast_config::FastConfig; +# use fast_config::Format; +# use serde::Serialize; +# use serde::Deserialize; // Creating a config struct to store our data #[derive(Serialize, Deserialize, FastConfig)] @@ -74,29 +74,28 @@ pub struct MyData { pub student_debt: i32, } -fn main() { - let config_path = "test/myconfig.json5"; - // Creating our data (default values) - let mut data = MyData { - student_debt: 20 - }; - # // use save to create the file for test - # data.save(config_path, Format::JSON5).unwrap(); - // load the data from the file - data.load(config_path, Format::JSON5).unwrap(); +let config_path = "test/myconfig.json5"; +// Creating our data (default values) +let mut data = MyData { + student_debt: 20 +}; +# // use save to create the file for test +# data.save(config_path, Format::JSON5).unwrap(); +// load the data from the file +data.load(config_path, Format::JSON5).unwrap(); - // Read/writing to the data - println!("I am ${} in debt", data.student_debt); - data.student_debt = i32::MAX; - println!("Oh no, i am now ${} in debt!!", data.student_debt); +// Read/writing to the data +println!("I am {}$ in debt", data.student_debt); +data.student_debt = i32::MAX; +println!("Oh no, i am now {}$ in debt!!", data.student_debt); - // Saving it back to the disk - data.save(config_path, Format::JSON5).unwrap(); +// Saving it back to the disk +data.save(config_path, Format::JSON5).unwrap(); + +// clean up +# std::fs::remove_dir_all("test").unwrap(); - // clean up - # std::fs::remove_dir_all("test").unwrap(); -} ``` ## Getting started diff --git a/fast_config/Cargo.toml b/fast_config/Cargo.toml index d8c0ba5..78a69da 100644 --- a/fast_config/Cargo.toml +++ b/fast_config/Cargo.toml @@ -33,7 +33,7 @@ serde_yml = { version = "0.0.11", optional = true } serde_json = { version = "1.0", optional = true } [features] -default = [] +default = ["all"] all = ["json", "json5", "toml", "yaml", "derive"] derive = ["dep:fast_config_derive"] diff --git a/fast_config/src/lib.rs b/fast_config/src/lib.rs index a3ea346..5b9a091 100644 --- a/fast_config/src/lib.rs +++ b/fast_config/src/lib.rs @@ -10,8 +10,7 @@ use std::path::Path; feature = "toml", feature = "yaml" )))] -compile_error!("You must install at least one format feature: `json`, `json5`, `toml`, or `yaml`"); - +compile_error!("You must enable at least one format feature: `json`, `json5`, `toml`, or `yaml`"); #[cfg(feature = "derive")] extern crate fast_config_derive; @@ -20,7 +19,6 @@ extern crate fast_config_derive; #[cfg(feature = "derive")] pub use fast_config_derive::FastConfig; - /// Enum used to configure the [`Config`]s file format. /// /// ## ⚠️ Make sure to enable the feature flag for a format! @@ -36,35 +34,33 @@ pub enum Format { YAML, } - /// The main result error type of the crate.
/// Each type has it's own documentation. #[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Io(#[from] std::io::Error), +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), - #[cfg(feature = "json")] - #[error(transparent)] - Json(#[from] serde_json::Error), + #[cfg(feature = "json")] + #[error(transparent)] + Json(#[from] serde_json::Error), - #[cfg(feature = "json5")] - #[error(transparent)] - Json5(#[from] json5::Error), + #[cfg(feature = "json5")] + #[error(transparent)] + Json5(#[from] json5::Error), - #[cfg(feature = "toml")] - #[error(transparent)] - TomlSerialize(#[from] toml::ser::Error), - #[cfg(feature = "toml")] - #[error(transparent)] - TomlDeserialize(#[from] toml::de::Error), + #[cfg(feature = "toml")] + #[error(transparent)] + TomlSerialize(#[from] toml::ser::Error), + #[cfg(feature = "toml")] + #[error(transparent)] + TomlDeserialize(#[from] toml::de::Error), - #[cfg(feature = "yaml")] - #[error(transparent)] - Yaml(#[from] serde_yml::Error), + #[cfg(feature = "yaml")] + #[error(transparent)] + Yaml(#[from] serde_yml::Error), } - pub trait Config where for<'a> Self: Deserialize<'a> + Serialize + Sized, @@ -72,6 +68,12 @@ where fn load(&mut self, path: impl AsRef, format: Format) -> Result<(), Error>; fn save(&self, path: impl AsRef, format: Format) -> Result<(), Error>; fn save_pretty(&self, path: impl AsRef, format: Format) -> Result<(), Error>; + #[cfg(any( + feature = "json", + feature = "json5", + feature = "toml", + feature = "yaml" + ))] fn from_string(content: &str, format: Format) -> Result { let result = match format { #[cfg(feature = "json")] @@ -85,6 +87,12 @@ where }; Ok(result) } + #[cfg(any( + feature = "json", + feature = "json5", + feature = "toml", + feature = "yaml" + ))] fn to_string(&self, format: Format) -> Result { let result = match format { #[cfg(feature = "json")] @@ -98,6 +106,12 @@ where }; Ok(result) } + #[cfg(any( + feature = "json", + feature = "json5", + feature = "toml", + feature = "yaml" + ))] fn to_string_pretty(&self, format: Format) -> Result { let result = match format { #[cfg(feature = "json")] @@ -115,6 +129,4 @@ where #[cfg(test)] #[cfg(feature = "derive")] -mod tests; - - +mod tests; \ No newline at end of file From 98a85ddada9134d650e30c67d709bb0f1ba2d9c1 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Tue, 23 Sep 2025 12:32:36 +0330 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=8E=A8=20Change=20the=20trait=20nam?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - fast_config/src/lib.rs | 2 +- fast_config/src/tests.rs | 1 - fast_config_derive/src/lib.rs | 14 +++++++------- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d160046..a16a81a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ anything that isn't working as expected! ## Examples: ```rust -# use fast_config::Config; # use fast_config::FastConfig; # use fast_config::Format; # use serde::Serialize; diff --git a/fast_config/src/lib.rs b/fast_config/src/lib.rs index 5b9a091..33306d0 100644 --- a/fast_config/src/lib.rs +++ b/fast_config/src/lib.rs @@ -61,7 +61,7 @@ pub enum Error { Yaml(#[from] serde_yml::Error), } -pub trait Config +pub trait FastConfig where for<'a> Self: Deserialize<'a> + Serialize + Sized, { diff --git a/fast_config/src/tests.rs b/fast_config/src/tests.rs index 5715265..6f503a2 100644 --- a/fast_config/src/tests.rs +++ b/fast_config/src/tests.rs @@ -1,6 +1,5 @@ #![allow(dead_code)] use crate as fast_config; -use crate::Config; use crate::FastConfig; use crate::Format::*; diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs index 3e652a1..6ec0cd6 100644 --- a/fast_config_derive/src/lib.rs +++ b/fast_config_derive/src/lib.rs @@ -13,30 +13,30 @@ pub fn derive_config(input: TokenStream) -> TokenStream { let ident = &input.ident; quote! { - impl #crate_path::Config for #ident { - fn load(&mut self, path: #path_type, format: #crate_path::Format ) -> Result<(), #crate_path::Error> { + impl #crate_path::FastConfig for #ident { + fn load(&mut self, path: #path_type, format: #crate_path::Format) -> Result<(), #crate_path::Error> { let mut content = String::new(); let mut file = std::fs::File::open(path)?; std::io::Read::read_to_string(&mut file, &mut content)?; - *self = #crate_path::Config::from_string(&content, format)?; + *self = #crate_path::FastConfig::from_string(&content, format)?; Ok(()) } - fn save(&self, path: #path_type, format: #crate_path::Format ) -> Result<(), #crate_path::Error> { + fn save(&self, path: #path_type, format: #crate_path::Format) -> Result<(), #crate_path::Error> { if let Some(parent_dir) = path.as_ref().parent() { std::fs::create_dir_all(parent_dir)?; } let mut file = std::fs::File::create(path)?; - let content = #crate_path::Config::to_string(self, format)?; + let content = #crate_path::FastConfig::to_string(self, format)?; use std::io::Write; write!(file, "{}", content)?; Ok(()) } - fn save_pretty(&self, path: #path_type, format: #crate_path::Format ) -> Result<(), #crate_path::Error> { + fn save_pretty(&self, path: #path_type, format: #crate_path::Format) -> Result<(), #crate_path::Error> { if let Some(parent_dir) = path.as_ref().parent() { std::fs::create_dir_all(parent_dir)?; } let mut file = std::fs::File::create(path)?; - let content = #crate_path::Config::to_string_pretty(self, format)?; + let content = #crate_path::FastConfig::to_string_pretty(self, format)?; use std::io::Write; write!(file, "{}", content)?; Ok(()) From 571039d1df7f71f6db2ae118496c22a94807df87 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sat, 27 Sep 2025 12:51:18 +0330 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=94=A7=20Remove=20unused=20features?= =?UTF-8?q?=20from=20Cargo.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config_derive/Cargo.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/fast_config_derive/Cargo.toml b/fast_config_derive/Cargo.toml index dbd2d0a..2299a5f 100644 --- a/fast_config_derive/Cargo.toml +++ b/fast_config_derive/Cargo.toml @@ -12,10 +12,4 @@ proc-macro = true [dependencies] proc-macro2 = { version = "1", features = ["proc-macro"] } quote = { version = "1", features = ["proc-macro"] } -syn = { version = "2", features = [ - # "clone-impls", - # "derive", - # "parsing", - # "printing", - # "proc-macro", -] } +syn = { version = "2" } From 8c5701a8531d2cdca5410bdbf23b549811fd2e84 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sun, 30 Nov 2025 16:04:42 +0330 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=8E=A8=20Add=20generic=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config/src/tests.rs | 31 ++++++++++++++----------------- fast_config_derive/src/lib.rs | 22 ++++++++++++++++++++-- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/fast_config/src/tests.rs b/fast_config/src/tests.rs index 6f503a2..29683c7 100644 --- a/fast_config/src/tests.rs +++ b/fast_config/src/tests.rs @@ -8,7 +8,7 @@ use serde::Serialize; use std::path::PathBuf; // Sub-data -#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Default, Debug, FastConfig)] pub struct SubData { pub string: String, pub unsigned: u64, @@ -17,19 +17,16 @@ pub struct SubData { // Data #[derive(Serialize, Deserialize, PartialEq, Debug, FastConfig)] -pub struct MyData { +pub struct MyData +{ pub number: i32, - pub subdata: SubData, + pub subdata: T, } -impl Default for MyData { +impl Default for MyData { fn default() -> Self { Self { number: 20, - subdata: SubData { - string: "Joe Mama".into(), - unsigned: 400, - boolean: true, - }, + subdata: T::default(), } } } @@ -70,11 +67,11 @@ static MANAGER: Manager = Manager(std::sync::atomic::AtomicUsize::new(0)); fn save_load_json() { let c = MANAGER.setup(); let path = c.path.join("config.json"); - let mut to_save = MyData::default(); + let mut to_save = MyData::::default(); to_save.number = i32::MAX; to_save.save(&path, JSON).unwrap(); - let mut to_load = MyData::default(); + let mut to_load = MyData::::default(); to_load.load(&path, JSON).unwrap(); assert_eq!(to_load, to_save); @@ -85,11 +82,11 @@ fn save_load_json() { fn save_load_json5() { let c = MANAGER.setup(); let path = c.path.join("config.json5"); - let mut to_save = MyData::default(); + let mut to_save = MyData::::default(); to_save.number = i32::MAX; to_save.save(&path, JSON5).unwrap(); - let mut to_load = MyData::default(); + let mut to_load = MyData::::default(); to_load.load(&path, JSON5).unwrap(); assert_eq!(to_load, to_save); @@ -100,11 +97,11 @@ fn save_load_json5() { fn save_load_toml() { let c = MANAGER.setup(); let path = c.path.join("config.toml"); - let mut to_save = MyData::default(); + let mut to_save = MyData::::default(); to_save.number = i32::MAX; to_save.save(&path, TOML).unwrap(); - let mut to_load = MyData::default(); + let mut to_load = MyData::::default(); to_load.load(&path, TOML).unwrap(); assert_eq!(to_load, to_save); @@ -115,11 +112,11 @@ fn save_load_toml() { fn save_load_yaml() { let c = MANAGER.setup(); let path = c.path.join("config.yaml"); - let mut to_save = MyData::default(); + let mut to_save = MyData::::default(); to_save.number = i32::MAX; to_save.save(&path, YAML).unwrap(); - let mut to_load = MyData::default(); + let mut to_load = MyData::::default(); to_load.load(&path, YAML).unwrap(); assert_eq!(to_load, to_save); diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs index 6ec0cd6..424288a 100644 --- a/fast_config_derive/src/lib.rs +++ b/fast_config_derive/src/lib.rs @@ -1,6 +1,8 @@ use proc_macro::TokenStream; +use quote::ToTokens; use quote::quote; use syn::DeriveInput; +use syn::WherePredicate; use syn::parse_macro_input; #[proc_macro_derive(FastConfig)] @@ -11,9 +13,25 @@ pub fn derive_config(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = &input.ident; + + // Extract generics and where clause + let mut generics = input.generics.clone(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + // Add: for<'a> Self: Deserialize<'a> + Serialize + let extra = quote! { + for<'a> #ident #ty_generics: ::serde::Deserialize<'a> + ::serde::Serialize + }; + + let where_clause = if let Some(mut wc) = where_clause.cloned() { + wc.predicates.push(syn::parse2(extra).unwrap()); + wc + } else { + syn::parse_quote!(where #extra) + }; quote! { - impl #crate_path::FastConfig for #ident { + impl #impl_generics #crate_path::FastConfig for #ident #ty_generics #where_clause { fn load(&mut self, path: #path_type, format: #crate_path::Format) -> Result<(), #crate_path::Error> { let mut content = String::new(); let mut file = std::fs::File::open(path)?; @@ -43,4 +61,4 @@ pub fn derive_config(input: TokenStream) -> TokenStream { } } }.into() -} \ No newline at end of file +} From 27aae6b3ab5eb3b6d3f537ceebe856559d22dbb5 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sun, 30 Nov 2025 16:22:16 +0330 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20for=20generic=20type?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config_derive/src/lib.rs | 38 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs index 424288a..47a4751 100644 --- a/fast_config_derive/src/lib.rs +++ b/fast_config_derive/src/lib.rs @@ -1,8 +1,7 @@ use proc_macro::TokenStream; -use quote::ToTokens; use quote::quote; use syn::DeriveInput; -use syn::WherePredicate; +use syn::GenericParam; use syn::parse_macro_input; #[proc_macro_derive(FastConfig)] @@ -11,27 +10,32 @@ pub fn derive_config(input: TokenStream) -> TokenStream { let path_type = quote! {impl AsRef}; let input = parse_macro_input!(input as DeriveInput); - let ident = &input.ident; - - // Extract generics and where clause + + // Clone generics so we can modify them let mut generics = input.generics.clone(); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let where_clause = generics.make_where_clause(); + + // For every type parameter T, add: + // T: for<'a> Deserialize<'a> + Serialize + for param in input.generics.params.iter() { + if let GenericParam::Type(ty) = param { + let ty_ident = &ty.ident; - // Add: for<'a> Self: Deserialize<'a> + Serialize - let extra = quote! { - for<'a> #ident #ty_generics: ::serde::Deserialize<'a> + ::serde::Serialize - }; + where_clause.predicates.push(syn::parse_quote! { + for<'a> #ty_ident: ::serde::Deserialize<'a> + }); + + where_clause.predicates.push(syn::parse_quote! { + #ty_ident: ::serde::Serialize + }); + } + } - let where_clause = if let Some(mut wc) = where_clause.cloned() { - wc.predicates.push(syn::parse2(extra).unwrap()); - wc - } else { - syn::parse_quote!(where #extra) - }; + let (impl_generics, ty_generics, where_clause_final) = generics.split_for_impl(); quote! { - impl #impl_generics #crate_path::FastConfig for #ident #ty_generics #where_clause { + impl #impl_generics #crate_path::FastConfig for #ident #ty_generics #where_clause_final { fn load(&mut self, path: #path_type, format: #crate_path::Format) -> Result<(), #crate_path::Error> { let mut content = String::new(); let mut file = std::fs::File::open(path)?; From 5b019cffa4efdc27e198985b838dab3f042fa1a3 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sun, 30 Nov 2025 17:17:51 +0330 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=90=9B=20Fix=20associated=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config/src/lib.rs | 2 +- fast_config/src/tests.rs | 21 ++++++++++++++------- fast_config_derive/src/lib.rs | 29 +++++------------------------ 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/fast_config/src/lib.rs b/fast_config/src/lib.rs index 33306d0..da1a9fe 100644 --- a/fast_config/src/lib.rs +++ b/fast_config/src/lib.rs @@ -63,7 +63,7 @@ pub enum Error { pub trait FastConfig where - for<'a> Self: Deserialize<'a> + Serialize + Sized, + Self: for<'a> Deserialize<'a> + Serialize + Sized, { fn load(&mut self, path: impl AsRef, format: Format) -> Result<(), Error>; fn save(&self, path: impl AsRef, format: Format) -> Result<(), Error>; diff --git a/fast_config/src/tests.rs b/fast_config/src/tests.rs index 29683c7..1e899cd 100644 --- a/fast_config/src/tests.rs +++ b/fast_config/src/tests.rs @@ -8,29 +8,36 @@ use serde::Serialize; use std::path::PathBuf; // Sub-data -#[derive(Serialize, Deserialize, PartialEq, Default, Debug, FastConfig)] +#[derive(Default, Debug ,Serialize, Deserialize, PartialEq)] pub struct SubData { pub string: String, pub unsigned: u64, pub boolean: bool, } -// Data +// GenericData #[derive(Serialize, Deserialize, PartialEq, Debug, FastConfig)] -pub struct MyData -{ - pub number: i32, +pub struct MyData { + pub number: T::AssociatedType, pub subdata: T, } -impl Default for MyData { +impl Default for MyData { fn default() -> Self { Self { - number: 20, + number: ::AssociatedType::default(), subdata: T::default(), } } } +pub trait MyTrait { + type AssociatedType: Serialize + for<'a> Deserialize<'a> + PartialEq + std::fmt::Debug + Default; +} + +impl MyTrait for SubData { + type AssociatedType = i32; +} + struct Setup<'a> { path: PathBuf, manager: &'a Manager, diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs index 47a4751..17de90d 100644 --- a/fast_config_derive/src/lib.rs +++ b/fast_config_derive/src/lib.rs @@ -1,7 +1,6 @@ use proc_macro::TokenStream; use quote::quote; use syn::DeriveInput; -use syn::GenericParam; use syn::parse_macro_input; #[proc_macro_derive(FastConfig)] @@ -12,30 +11,12 @@ pub fn derive_config(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = &input.ident; - // Clone generics so we can modify them - let mut generics = input.generics.clone(); - let where_clause = generics.make_where_clause(); - - // For every type parameter T, add: - // T: for<'a> Deserialize<'a> + Serialize - for param in input.generics.params.iter() { - if let GenericParam::Type(ty) = param { - let ty_ident = &ty.ident; - - where_clause.predicates.push(syn::parse_quote! { - for<'a> #ty_ident: ::serde::Deserialize<'a> - }); - - where_clause.predicates.push(syn::parse_quote! { - #ty_ident: ::serde::Serialize - }); - } - } - - let (impl_generics, ty_generics, where_clause_final) = generics.split_for_impl(); - + let (impl_generics, ty_generics, _) = input.generics.split_for_impl(); + let where_clause = quote! { where + Self: for<'a> ::serde::Deserialize<'a> + ::serde::Serialize + Sized + }; quote! { - impl #impl_generics #crate_path::FastConfig for #ident #ty_generics #where_clause_final { + impl #impl_generics #crate_path::FastConfig for #ident #ty_generics #where_clause { fn load(&mut self, path: #path_type, format: #crate_path::Format) -> Result<(), #crate_path::Error> { let mut content = String::new(); let mut file = std::fs::File::open(path)?; From 01dbf46db10822cc8e5f9641c3475f56efa89fea Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sun, 30 Nov 2025 17:33:38 +0330 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=8E=A8=20Add=20new=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fast_config/src/lib.rs b/fast_config/src/lib.rs index da1a9fe..359cc0c 100644 --- a/fast_config/src/lib.rs +++ b/fast_config/src/lib.rs @@ -125,6 +125,11 @@ where }; Ok(result) } + fn new(path: impl AsRef, format: Format) -> Result { + let content = std::fs::read_to_string(path)?; + let config = Self::from_string(&content, format)?; + Ok(config) + } } #[cfg(test)] From 2a487ed050c44c6150f003ae6e896285ee6b2cb4 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Sun, 30 Nov 2025 17:36:05 +0330 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=92=A9=20Remove=20serde=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fast_config_derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs index 17de90d..d3185b2 100644 --- a/fast_config_derive/src/lib.rs +++ b/fast_config_derive/src/lib.rs @@ -13,7 +13,7 @@ pub fn derive_config(input: TokenStream) -> TokenStream { let (impl_generics, ty_generics, _) = input.generics.split_for_impl(); let where_clause = quote! { where - Self: for<'a> ::serde::Deserialize<'a> + ::serde::Serialize + Sized + Self: for<'a> Deserialize<'a> + Serialize + Sized }; quote! { impl #impl_generics #crate_path::FastConfig for #ident #ty_generics #where_clause { From 344875113d477b429c6574999d8d4ca3600e1196 Mon Sep 17 00:00:00 2001 From: "y.torshizi" Date: Tue, 9 Dec 2025 14:38:59 +0330 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20documentaion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 - README.md | 202 ++++++++++++++++++++++------ examples/advanced.rs | 51 ------- examples/simple.rs | 30 ----- fast_config/src/lib.rs | 7 +- fast_config/src/tests.rs | 130 ------------------ fast_config/src/tests/associated.rs | 122 +++++++++++++++++ fast_config/src/tests/generics.rs | 116 ++++++++++++++++ fast_config/src/tests/mod.rs | 46 +++++++ fast_config/src/tests/nested.rs | 116 ++++++++++++++++ fast_config/src/tests/simple.rs | 98 ++++++++++++++ fast_config_derive/src/lib.rs | 34 ++++- 12 files changed, 694 insertions(+), 261 deletions(-) delete mode 100644 examples/advanced.rs delete mode 100644 examples/simple.rs delete mode 100644 fast_config/src/tests.rs create mode 100644 fast_config/src/tests/associated.rs create mode 100644 fast_config/src/tests/generics.rs create mode 100644 fast_config/src/tests/mod.rs create mode 100644 fast_config/src/tests/nested.rs create mode 100644 fast_config/src/tests/simple.rs diff --git a/.gitignore b/.gitignore index 4be060b..93ef2be 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ debug/ target/ -# Testing-specific -config/ - # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock diff --git a/README.md b/README.md index a16a81a..9a9219a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ A small, safe, lightweight, and easy-to-use Rust crate to read and write to config files. -Currently only supports: -[JSON](https://crates.io/crates/serde_json) & [JSON5](https://crates.io/crates/json5), [TOML](https://crates.io/crates/toml), and [YAML](https://crates.io/crates/serde_yml). +Currently supports: +[JSON](https://crates.io/crates/serde_json), [JSON5](https://crates.io/crates/json5), [TOML](https://crates.io/crates/toml), and [YAML](https://crates.io/crates/serde_yml). But more [Serde](https://serde.rs/)-supported formats *(such as RON)* are planned to be added later. @@ -61,77 +61,199 @@ anything that isn't working as expected! --- ## Examples: + +### Basic Usage + ```rust -# use fast_config::FastConfig; -# use fast_config::Format; -# use serde::Serialize; -# use serde::Deserialize; +use fast_config::FastConfig; +use fast_config::Format; +use serde::Serialize; +use serde::Deserialize; -// Creating a config struct to store our data +// Create a config struct and derive FastConfig #[derive(Serialize, Deserialize, FastConfig)] pub struct MyData { pub student_debt: i32, } - let config_path = "test/myconfig.json5"; -// Creating our data (default values) +// Create data with default values let mut data = MyData { student_debt: 20 }; -# // use save to create the file for test -# data.save(config_path, Format::JSON5).unwrap(); -// load the data from the file -data.load(config_path, Format::JSON5).unwrap(); +// Save to create the file +data.save(&config_path, Format::JSON5).unwrap(); +// Load the data from the file +data.load(&config_path, Format::JSON5).unwrap(); -// Read/writing to the data +// Read/write to the data println!("I am {}$ in debt", data.student_debt); data.student_debt = i32::MAX; println!("Oh no, i am now {}$ in debt!!", data.student_debt); -// Saving it back to the disk -data.save(config_path, Format::JSON5).unwrap(); - -// clean up +// Save it back to disk +data.save(&config_path, Format::JSON5).unwrap(); +# // Clean up # std::fs::remove_dir_all("test").unwrap(); +``` + +### Creating Config from File + +```rust +# use fast_config::FastConfig; +# use fast_config::Format; +# use serde::Serialize; +# use serde::Deserialize; +# #[derive(Serialize, Deserialize, FastConfig)] +# pub struct MyData { pub value: i32 } +# // First, create and save a config file +# let mut temp = MyData { value: 42 }; +# let config_path = "example_config.json"; +# temp.save(config_path, Format::JSON).unwrap(); +// Create config directly from a file path +let data = MyData::new(config_path, Format::JSON).unwrap(); +# // Clean up +# std::fs::remove_file(config_path).unwrap(); +``` + +### String Serialization + +```rust +# use fast_config::FastConfig; +# use fast_config::Format; +# use serde::Serialize; +# use serde::Deserialize; +# #[derive(Serialize, Deserialize, FastConfig)] +# pub struct MyData { pub value: i32 } +# let data = MyData { value: 42 }; + +// Convert config to string +let json_string = data.to_string(Format::JSON).unwrap(); +let pretty_json = data.to_string_pretty(Format::JSON).unwrap(); +// Create config from string +let loaded = MyData::from_string(&json_string, Format::JSON).unwrap(); ``` -## Getting started +### Pretty Formatting -1. Add the crate to your project via
`cargo add fast_config` - - Additionally, also add `serde` as it is required! +```rust +# use fast_config::FastConfig; +# use fast_config::Format; +# use serde::Serialize; +# use serde::Deserialize; +# #[derive(Serialize, Deserialize, FastConfig)] +# pub struct MyData { pub value: i32 } +# let data = MyData { value: 42 }; + +// Save with pretty formatting (indented, readable) +data.save_pretty("config.json", Format::JSON).unwrap(); +# // Clean up +# std::fs::remove_file("config.json").unwrap(); +``` -2. Enable the feature(s) for the format(s) you'd like to use
- - Currently only `json5`, `toml`, and `yaml` are supported
+## Getting started -3. Create a struct to hold your data that derives `Serialize` and `Deserialize` +1. Add the crate to your project: + ```bash + cargo add fast_config + ``` + - Also add `serde` with derive features: + ```bash + cargo add serde --features derive + ``` -4. Create an instance of your data struct - - Optionally `use` the crate's `Config` type for convenience: `use fast_config::Config;` +2. Enable the feature(s) for the format(s) you'd like to use in your `Cargo.toml`: + ```toml + [dependencies] + fast_config = { version = "...", features = ["json", "json5", "toml", "yaml", "derive"] } + ``` + - Available formats: `json`, `json5`, `toml`, `yaml` + - Enable the `derive` feature to use the `#[derive(FastConfig)]` macro + +3. Create a struct to hold your data and derive the necessary traits: + ```rust + use serde::Serialize; + use serde::Deserialize; + use fast_config::FastConfig; + + #[derive(Serialize, Deserialize, FastConfig)] + pub struct MyConfig { + pub setting: String, + } + ``` -5. To create and store your config file(s), use: - ```rust,ignore - let my_config = Config::new("./path/to/my_config_file", your_data).unwrap(); +4. Use the trait methods directly on your struct: + ```rust + # use fast_config::FastConfig; + # use fast_config::Format; + # use serde::Serialize; + # use serde::Deserialize; + # #[derive(Serialize, Deserialize, FastConfig)] + # pub struct MyConfig { pub setting: String } + # // Clean up any existing file first + # let _ = std::fs::remove_file("example_getting_started.json"); + let mut config = MyConfig { setting: "default".into() }; + let config_path = "example_getting_started.json"; + config.save(config_path, Format::JSON).unwrap(); + config.load(config_path, Format::JSON).unwrap(); + # // Clean up + # std::fs::remove_file(config_path).unwrap(); ``` - Alternatively, you can use `Config::from_settings` to style some things and manually set the format! --- -View the [examples](./examples) directory for more advanced examples. +## API Reference + +### The `FastConfig` Trait + +The `FastConfig` trait provides methods for loading, saving, and serializing config data. When you derive `FastConfig` on your struct, these methods become available: + +#### File Operations + +- **`load(path, format)`** - Loads config data from a file, replacing the current struct's values +- **`save(path, format)`** - Saves config data to a file (compact format) +- **`save_pretty(path, format)`** - Saves config data to a file with pretty formatting (indented, readable) + +#### String Operations + +- **`from_string(content, format)`** - Creates a new config instance from a string +- **`to_string(format)`** - Converts config to a compact string representation +- **`to_string_pretty(format)`** - Converts config to a pretty-formatted string + +#### Constructor + +- **`new(path, format)`** - Creates a new config instance by loading from a file path + +### The `#[derive(FastConfig)]` Macro + +The derive macro automatically implements the `FastConfig` trait for your struct. It requires that your struct also derives `Serialize` and `Deserialize` from `serde`. + +#### Custom Crate Path + +If you're re-exporting `fast_config` under a different name, you can specify the crate path: + +```rust,ignore +use serde::Serialize; +use serde::Deserialize; +use fast_config::FastConfig; + +#[derive(Serialize, Deserialize, FastConfig)] +#[fast_config(crate = "my_crate::fast_config")] +pub struct MyConfig { + pub value: i32, +} +``` + +--- -## NOTE: This project will be rewritten sometime -The code is currently very messy, but I'm too busy with other projects to deal with it.
-I've improved a lot as a Rust developer since the creation of this project and a lot of the ways you interface with it could be better. +View the [tests](./fast_config/src/tests/) directory for more advanced examples. -Some things I want to do for the rewrite are listed in a comment at the top of [lib.rs](./src/lib.rs) -Some other ideas I'll have to experiment with: -- Moving to a trait-based approach where you can slap a `#[derive(FastConfig)]` onto any struct to give it the `save`/`load` functions. - This makes the annoying `my_config.data.my_setting` into simply `my_config.my_setting` +## Migration Note -A conversion guide for the rewrite will be available, as I'll have to convert over my projects as well to use the rewritten `fast_config`. +The crate now uses a trait-based approach with `#[derive(FastConfig)]`. This makes the API cleaner and more ergonomic - you can now call `save()` and `load()` directly on your config struct instead of wrapping it in a `Config` type. -The rewrite should be smaller, safer, and the source code will most importantly be ***way more readable***. +If you're migrating from an older version, see the [conversion tutorial](./CONVERSION_TUTORIAL.md) for guidance. ---
diff --git a/examples/advanced.rs b/examples/advanced.rs deleted file mode 100644 index 1c89c71..0000000 --- a/examples/advanced.rs +++ /dev/null @@ -1,51 +0,0 @@ -use fast_config::Config; -use fast_config::FastConfig; -use fast_config::Format; -use serde::Deserialize; -use serde::Serialize; - -// Sub-structs -#[derive(Serialize, Deserialize)] -pub struct Person { - pub name: String, - pub age: u64, - pub skill_issue: bool, -} - -// Creating a config struct to store our data -#[derive(Serialize, Deserialize, FastConfig)] -pub struct MyData { - pub student_debt: i32, - pub person: Person, -} - -// Setting the default values for the data -impl Default for MyData { - fn default() -> Self { - Self { - student_debt: 20, - person: Person { - name: "Joe Mama".into(), - age: 400, - skill_issue: true, - }, - } - } -} - -fn main() { - let config_path = "./config/myconfig.json5"; - // Creating our data (default values) - let mut data = MyData::default(); - - // load the data from the file - data.load(config_path, Format::JSON5).unwrap(); - - // Read/writing to the data - println!("I am ${} in debt", data.student_debt); - data.student_debt = i32::MAX; - println!("Oh no, i am now ${} in debt!!", data.student_debt); - - // Saving it back to the disk - data.save(config_path, Format::JSON5).unwrap(); -} diff --git a/examples/simple.rs b/examples/simple.rs deleted file mode 100644 index 960c623..0000000 --- a/examples/simple.rs +++ /dev/null @@ -1,30 +0,0 @@ -use fast_config::Config; -use fast_config::FastConfig; -use fast_config::Format; -use serde::Serialize; -use serde::Deserialize; - -// Creating a config struct to store our data -#[derive(Serialize, Deserialize, FastConfig)] -pub struct MyData { - pub student_debt: i32 -} - -fn main() { - let config_path = "./config/myconfig.json5"; - // Creating our data (default values) - let mut data = MyData { - student_debt: 20 - }; - - // load the data from the file - data.load(config_path, Format::JSON5).unwrap(); - - // Read/writing to the data - println!("I am ${} in debt", data.student_debt); - data.student_debt = i32::MAX; - println!("Oh no, i am now ${} in debt!!", data.student_debt); - - // Saving it back to the disk - data.save(config_path, Format::JSON5).unwrap(); -} diff --git a/fast_config/src/lib.rs b/fast_config/src/lib.rs index 359cc0c..c942362 100644 --- a/fast_config/src/lib.rs +++ b/fast_config/src/lib.rs @@ -19,9 +19,7 @@ extern crate fast_config_derive; #[cfg(feature = "derive")] pub use fast_config_derive::FastConfig; -/// Enum used to configure the [`Config`]s file format. -/// -/// ## ⚠️ Make sure to enable the feature flag for a format! +/// Enum used to configure the file's format. #[derive(Debug, PartialEq, Copy, Clone)] pub enum Format { #[cfg(feature = "json")] @@ -131,7 +129,6 @@ where Ok(config) } } - #[cfg(test)] #[cfg(feature = "derive")] -mod tests; \ No newline at end of file +mod tests; diff --git a/fast_config/src/tests.rs b/fast_config/src/tests.rs deleted file mode 100644 index 1e899cd..0000000 --- a/fast_config/src/tests.rs +++ /dev/null @@ -1,130 +0,0 @@ -#![allow(dead_code)] -use crate as fast_config; -use crate::FastConfig; -use crate::Format::*; - -use serde::Deserialize; -use serde::Serialize; - -use std::path::PathBuf; -// Sub-data -#[derive(Default, Debug ,Serialize, Deserialize, PartialEq)] -pub struct SubData { - pub string: String, - pub unsigned: u64, - pub boolean: bool, -} - -// GenericData -#[derive(Serialize, Deserialize, PartialEq, Debug, FastConfig)] -pub struct MyData { - pub number: T::AssociatedType, - pub subdata: T, -} -impl Default for MyData { - fn default() -> Self { - Self { - number: ::AssociatedType::default(), - subdata: T::default(), - } - } -} - -pub trait MyTrait { - type AssociatedType: Serialize + for<'a> Deserialize<'a> + PartialEq + std::fmt::Debug + Default; -} - -impl MyTrait for SubData { - type AssociatedType = i32; -} - -struct Setup<'a> { - path: PathBuf, - manager: &'a Manager, -} - -impl<'a> Drop for Setup<'a> { - fn drop(&mut self) { - if self - .manager - .0 - .fetch_sub(1, std::sync::atomic::Ordering::SeqCst) - == 1 - { - std::fs::remove_dir_all(&self.path).expect("Failed to remove test directory"); - } - } -} - -struct Manager(std::sync::atomic::AtomicUsize); -impl Manager { - fn setup<'a>(&'a self) -> Setup<'a> { - self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - Setup { - path: PathBuf::from("../config/"), - manager: self, - } - } -} - -static MANAGER: Manager = Manager(std::sync::atomic::AtomicUsize::new(0)); - -#[cfg(feature = "json")] -#[test] -fn save_load_json() { - let c = MANAGER.setup(); - let path = c.path.join("config.json"); - let mut to_save = MyData::::default(); - to_save.number = i32::MAX; - to_save.save(&path, JSON).unwrap(); - - let mut to_load = MyData::::default(); - to_load.load(&path, JSON).unwrap(); - - assert_eq!(to_load, to_save); -} - -#[cfg(feature = "json5")] -#[test] -fn save_load_json5() { - let c = MANAGER.setup(); - let path = c.path.join("config.json5"); - let mut to_save = MyData::::default(); - to_save.number = i32::MAX; - to_save.save(&path, JSON5).unwrap(); - - let mut to_load = MyData::::default(); - to_load.load(&path, JSON5).unwrap(); - - assert_eq!(to_load, to_save); -} - -#[cfg(feature = "toml")] -#[test] -fn save_load_toml() { - let c = MANAGER.setup(); - let path = c.path.join("config.toml"); - let mut to_save = MyData::::default(); - to_save.number = i32::MAX; - to_save.save(&path, TOML).unwrap(); - - let mut to_load = MyData::::default(); - to_load.load(&path, TOML).unwrap(); - - assert_eq!(to_load, to_save); -} - -#[cfg(feature = "yaml")] -#[test] -fn save_load_yaml() { - let c = MANAGER.setup(); - let path = c.path.join("config.yaml"); - let mut to_save = MyData::::default(); - to_save.number = i32::MAX; - to_save.save(&path, YAML).unwrap(); - - let mut to_load = MyData::::default(); - to_load.load(&path, YAML).unwrap(); - - assert_eq!(to_load, to_save); -} diff --git a/fast_config/src/tests/associated.rs b/fast_config/src/tests/associated.rs new file mode 100644 index 0000000..ad62da6 --- /dev/null +++ b/fast_config/src/tests/associated.rs @@ -0,0 +1,122 @@ +use super::*; + +pub trait AssociatedTrait { + type AssociatedType: Serialize + for<'a> Deserialize<'a> + PartialEq + Sized; +} + +#[derive(PartialEq, Debug)] +pub enum ForigenData { + A, + B, + C, +} + +impl AssociatedTrait for ForigenData { + type AssociatedType = f64; +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, FastConfig)] +pub struct Data { + pub string: String, + pub number: i32, + pub unsigned: u64, + pub boolean: bool, + pub associated: T::AssociatedType, +} + +#[cfg(feature = "json")] +#[test] +fn create_save_change_save_load_json() { + let c = MANAGER.setup(); + let path = c.path.join("config_associated.json"); + + let mut config = Data:: { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + associated: 0.0, + }; + config.save(&path, JSON).unwrap(); + let loaded = Data::new(&path, JSON).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.associated = 1.0; + config.save(&path, JSON).unwrap(); + let updated = Data::new(&path, JSON).unwrap(); + config.load(&path, JSON).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "json5")] +#[test] +fn create_save_change_save_load_json5() { + let c = MANAGER.setup(); + let path = c.path.join("config_associated.json5"); + let mut config = Data:: { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + associated: 0.0, + }; + config.save(&path, JSON5).unwrap(); + let loaded = Data::new(&path, JSON5).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.associated = 1.0; + config.save(&path, JSON5).unwrap(); + let updated = Data::new(&path, JSON5).unwrap(); + config.load(&path, JSON5).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "toml")] +#[test] +fn create_save_change_save_load_toml() { + let c = MANAGER.setup(); + let path = c.path.join("config_associated.toml"); + let mut config = Data:: { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + associated: 0.0, + }; + config.save(&path, TOML).unwrap(); + let loaded = Data::new(&path, TOML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.associated = 1.0; + config.save(&path, TOML).unwrap(); + let updated = Data::new(&path, TOML).unwrap(); + config.load(&path, TOML).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "yaml")] +#[test] +fn create_save_change_save_load_yaml() { + let c = MANAGER.setup(); + let path = c.path.join("config_associated.yaml"); + let mut config = Data:: { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + associated: 0.0, + }; + config.save(&path, YAML).unwrap(); + let loaded = Data::new(&path, YAML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.associated = 1.0; + config.save(&path, YAML).unwrap(); + let updated = Data::new(&path, YAML).unwrap(); + config.load(&path, YAML).unwrap(); + assert_eq!(updated, config); +} diff --git a/fast_config/src/tests/generics.rs b/fast_config/src/tests/generics.rs new file mode 100644 index 0000000..55f2cc6 --- /dev/null +++ b/fast_config/src/tests/generics.rs @@ -0,0 +1,116 @@ +use super::*; + + +#[derive(Debug ,Serialize, Deserialize, PartialEq)] +pub enum NestedData { + A, + B, + C +} + + +#[derive(Debug ,Serialize, Deserialize, PartialEq, FastConfig)] +pub struct Data { + pub string: String, + pub number: i32, + pub unsigned: u64, + pub boolean: bool, + pub generic: T, +} + +#[cfg(feature = "json")] +#[test] +fn create_save_change_save_load_json() { + let c = MANAGER.setup(); + let path = c.path.join("config_generic.json"); + + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + generic: NestedData::A, + }; + config.save(&path, JSON).unwrap(); + let loaded = Data::new(&path, JSON).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.generic = NestedData::B; + config.save(&path, JSON).unwrap(); + let updated = Data::new(&path, JSON).unwrap(); + config.load(&path, JSON).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "json5")] +#[test] +fn create_save_change_save_load_json5() { + let c = MANAGER.setup(); + let path = c.path.join("config_generic.json5"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + generic: NestedData::A, + }; + config.save(&path, JSON5).unwrap(); + let loaded = Data::new(&path, JSON5).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.generic = NestedData::B; + config.save(&path, JSON5).unwrap(); + let updated = Data::new(&path, JSON5).unwrap(); + config.load(&path, JSON5).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "toml")] +#[test] +fn create_save_change_save_load_toml() { + let c = MANAGER.setup(); + let path = c.path.join("config_generic.toml"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + generic: NestedData::A, + }; + config.save(&path, TOML).unwrap(); + let loaded = Data::new(&path, TOML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.generic = NestedData::B; + config.save(&path, TOML).unwrap(); + let updated = Data::new(&path, TOML).unwrap(); + config.load(&path, TOML).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "yaml")] +#[test] +fn create_save_change_save_load_yaml() { + let c = MANAGER.setup(); + let path = c.path.join("config_generic.yaml"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + generic: NestedData::A, + }; + config.save(&path, YAML).unwrap(); + let loaded = Data::new(&path, YAML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.generic = NestedData::B; + config.save(&path, YAML).unwrap(); + let updated = Data::new(&path, YAML).unwrap(); + config.load(&path, YAML).unwrap(); + assert_eq!(updated, config); +} diff --git a/fast_config/src/tests/mod.rs b/fast_config/src/tests/mod.rs new file mode 100644 index 0000000..23830b9 --- /dev/null +++ b/fast_config/src/tests/mod.rs @@ -0,0 +1,46 @@ +#![allow(dead_code)] +pub use crate as fast_config; +pub use crate::FastConfig; +pub use crate::Format::*; + +pub use serde::Deserialize; +pub use serde::Serialize; + +pub use std::path::PathBuf; + +mod associated; +mod generics; +mod nested; +mod simple; + +struct Setup { + path: PathBuf, + manager: &'static Manager, +} + +impl Drop for Setup { + fn drop(&mut self) { + if self + .manager + .0 + .fetch_sub(1, std::sync::atomic::Ordering::SeqCst) + == 1 + { + let _ = std::fs::remove_dir_all(&self.path) + .inspect_err(|e| eprintln!("failed to clean up: {e}")); + } + } +} + +struct Manager(std::sync::atomic::AtomicUsize); +impl Manager { + fn setup(&'static self) -> Setup { + self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Setup { + path: PathBuf::from("../config/"), + manager: self, + } + } +} + +static MANAGER: Manager = Manager(std::sync::atomic::AtomicUsize::new(0)); diff --git a/fast_config/src/tests/nested.rs b/fast_config/src/tests/nested.rs new file mode 100644 index 0000000..89f2bc6 --- /dev/null +++ b/fast_config/src/tests/nested.rs @@ -0,0 +1,116 @@ +use super::*; + + +#[derive(Debug ,Serialize, Deserialize, PartialEq)] +pub enum NestedData { + A, + B, + C +} + + +#[derive(Debug ,Serialize, Deserialize, PartialEq, FastConfig)] +pub struct Data { + pub string: String, + pub number: i32, + pub unsigned: u64, + pub boolean: bool, + pub nested: NestedData, +} + +#[cfg(feature = "json")] +#[test] +fn create_save_change_save_load_json() { + let c = MANAGER.setup(); + let path = c.path.join("config_nested.json"); + + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + nested: NestedData::A, + }; + config.save(&path, JSON).unwrap(); + let loaded = Data::new(&path, JSON).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.nested = NestedData::B; + config.save(&path, JSON).unwrap(); + let updated = Data::new(&path, JSON).unwrap(); + config.load(&path, JSON).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "json5")] +#[test] +fn create_save_change_save_load_json5() { + let c = MANAGER.setup(); + let path = c.path.join("config_nested.json5"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + nested: NestedData::A, + }; + config.save(&path, JSON5).unwrap(); + let loaded = Data::new(&path, JSON5).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.nested = NestedData::B; + config.save(&path, JSON5).unwrap(); + let updated = Data::new(&path, JSON5).unwrap(); + config.load(&path, JSON5).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "toml")] +#[test] +fn create_save_change_save_load_toml() { + let c = MANAGER.setup(); + let path = c.path.join("config_nested.toml"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + nested: NestedData::A, + }; + config.save(&path, TOML).unwrap(); + let loaded = Data::new(&path, TOML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.nested = NestedData::B; + config.save(&path, TOML).unwrap(); + let updated = Data::new(&path, TOML).unwrap(); + config.load(&path, TOML).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "yaml")] +#[test] +fn create_save_change_save_load_yaml() { + let c = MANAGER.setup(); + let path = c.path.join("config_nested.yaml"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + nested: NestedData::A, + }; + config.save(&path, YAML).unwrap(); + let loaded = Data::new(&path, YAML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.nested = NestedData::B; + config.save(&path, YAML).unwrap(); + let updated = Data::new(&path, YAML).unwrap(); + config.load(&path, YAML).unwrap(); + assert_eq!(updated, config); +} diff --git a/fast_config/src/tests/simple.rs b/fast_config/src/tests/simple.rs new file mode 100644 index 0000000..a9839ea --- /dev/null +++ b/fast_config/src/tests/simple.rs @@ -0,0 +1,98 @@ +use super::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, FastConfig)] +pub struct Data { + pub string: String, + pub number: i32, + pub unsigned: u64, + pub boolean: bool, +} + +#[cfg(feature = "json")] +#[test] +fn create_save_change_save_load_json() { + let c = MANAGER.setup(); + let path = c.path.join("config_simple.json"); + + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + }; + config.save(&path, JSON).unwrap(); + let loaded = Data::new(&path, JSON).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.save(&path, JSON).unwrap(); + let updated = Data::new(&path, JSON).unwrap(); + config.load(&path, JSON).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "json5")] +#[test] +fn create_save_change_save_load_json5() { + let c = MANAGER.setup(); + let path = c.path.join("config_simple.json5"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + }; + config.save(&path, JSON5).unwrap(); + let loaded = Data::new(&path, JSON5).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.save(&path, JSON5).unwrap(); + let updated = Data::new(&path, JSON5).unwrap(); + config.load(&path, JSON5).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "toml")] +#[test] +fn create_save_change_save_load_toml() { + let c = MANAGER.setup(); + let path = c.path.join("config_simple.toml"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + }; + config.save(&path, TOML).unwrap(); + let loaded = Data::new(&path, TOML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.save(&path, TOML).unwrap(); + let updated = Data::new(&path, TOML).unwrap(); + config.load(&path, TOML).unwrap(); + assert_eq!(updated, config); +} + +#[cfg(feature = "yaml")] +#[test] +fn create_save_change_save_load_yaml() { + let c = MANAGER.setup(); + let path = c.path.join("config_simple.yaml"); + let mut config = Data { + string: "test".into(), + number: i32::MAX, + unsigned: 0, + boolean: true, + }; + config.save(&path, YAML).unwrap(); + let loaded = Data::new(&path, YAML).unwrap(); + assert_eq!(loaded, config); + + config.number = i32::MIN; + config.save(&path, YAML).unwrap(); + let updated = Data::new(&path, YAML).unwrap(); + config.load(&path, YAML).unwrap(); + assert_eq!(updated, config); +} diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs index d3185b2..3d34a56 100644 --- a/fast_config_derive/src/lib.rs +++ b/fast_config_derive/src/lib.rs @@ -1,15 +1,20 @@ use proc_macro::TokenStream; +use proc_macro2; use quote::quote; use syn::DeriveInput; use syn::parse_macro_input; +use syn::Attribute; +use syn::Meta; -#[proc_macro_derive(FastConfig)] +#[proc_macro_derive(FastConfig, attributes(fast_config))] pub fn derive_config(input: TokenStream) -> TokenStream { - let crate_path = quote! {fast_config}; let path_type = quote! {impl AsRef}; let input = parse_macro_input!(input as DeriveInput); let ident = &input.ident; + + // Extract crate path from attributes, default to "fast_config" + let crate_path = extract_crate_path(&input.attrs); let (impl_generics, ty_generics, _) = input.generics.split_for_impl(); let where_clause = quote! { where @@ -47,3 +52,28 @@ pub fn derive_config(input: TokenStream) -> TokenStream { } }.into() } + +fn extract_crate_path(attrs: &[Attribute]) -> proc_macro2::TokenStream { + for attr in attrs { + if attr.path().is_ident("fast_config") { + // Parse the attribute content - for #[fast_config(crate = "...")] + // parse_args parses what's inside the parentheses + if let Ok(meta) = attr.parse_args::() { + if let Meta::NameValue(name_value) = meta { + if name_value.path.is_ident("crate") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit_str), + .. + }) = name_value.value + { + let path_str = lit_str.value(); + return syn::parse_str::(&path_str) + .unwrap_or_else(|_| quote! { fast_config }); + } + } + } + } + } + } + quote! { fast_config } +} From def76c10a25c28aea4dd307de4d64b022b09ae73 Mon Sep 17 00:00:00 2001 From: FlooferLand <76737186+FlooferLand@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:03:44 +0200 Subject: [PATCH 13/13] Fix doc tests --- README.md | 65 +++++++---------------------------- fast_config/Cargo.toml | 4 ++- fast_config_derive/Cargo.toml | 4 +-- 3 files changed, 17 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 9a9219a..a62c717 100644 --- a/README.md +++ b/README.md @@ -76,15 +76,16 @@ pub struct MyData { pub student_debt: i32, } -let config_path = "test/myconfig.json5"; -// Create data with default values +// Create the data with default values let mut data = MyData { student_debt: 20 }; + // Save to create the file -data.save(&config_path, Format::JSON5).unwrap(); -// Load the data from the file -data.load(&config_path, Format::JSON5).unwrap(); +data.save("test/myconfig.json5", Format::JSON5).unwrap(); + +// Load from the file +data.load("test/myconfig.json5", Format::JSON5).unwrap(); // Read/write to the data println!("I am {}$ in debt", data.student_debt); @@ -92,41 +93,18 @@ data.student_debt = i32::MAX; println!("Oh no, i am now {}$ in debt!!", data.student_debt); // Save it back to disk -data.save(&config_path, Format::JSON5).unwrap(); -# // Clean up -# std::fs::remove_dir_all("test").unwrap(); +data.save("test/myconfig.json5", Format::JSON5).unwrap(); ``` ### Creating Config from File ```rust -# use fast_config::FastConfig; -# use fast_config::Format; -# use serde::Serialize; -# use serde::Deserialize; -# #[derive(Serialize, Deserialize, FastConfig)] -# pub struct MyData { pub value: i32 } -# // First, create and save a config file -# let mut temp = MyData { value: 42 }; -# let config_path = "example_config.json"; -# temp.save(config_path, Format::JSON).unwrap(); -// Create config directly from a file path -let data = MyData::new(config_path, Format::JSON).unwrap(); -# // Clean up -# std::fs::remove_file(config_path).unwrap(); +let data = MyData::new("example_config.json", Format::JSON).unwrap(); ``` ### String Serialization ```rust -# use fast_config::FastConfig; -# use fast_config::Format; -# use serde::Serialize; -# use serde::Deserialize; -# #[derive(Serialize, Deserialize, FastConfig)] -# pub struct MyData { pub value: i32 } -# let data = MyData { value: 42 }; - // Convert config to string let json_string = data.to_string(Format::JSON).unwrap(); let pretty_json = data.to_string_pretty(Format::JSON).unwrap(); @@ -138,18 +116,8 @@ let loaded = MyData::from_string(&json_string, Format::JSON).unwrap(); ### Pretty Formatting ```rust -# use fast_config::FastConfig; -# use fast_config::Format; -# use serde::Serialize; -# use serde::Deserialize; -# #[derive(Serialize, Deserialize, FastConfig)] -# pub struct MyData { pub value: i32 } -# let data = MyData { value: 42 }; - -// Save with pretty formatting (indented, readable) +// Saves in a format thats indented and human-readable data.save_pretty("config.json", Format::JSON).unwrap(); -# // Clean up -# std::fs::remove_file("config.json").unwrap(); ``` ## Getting started @@ -185,20 +153,11 @@ data.save_pretty("config.json", Format::JSON).unwrap(); 4. Use the trait methods directly on your struct: ```rust - # use fast_config::FastConfig; - # use fast_config::Format; - # use serde::Serialize; - # use serde::Deserialize; - # #[derive(Serialize, Deserialize, FastConfig)] - # pub struct MyConfig { pub setting: String } - # // Clean up any existing file first - # let _ = std::fs::remove_file("example_getting_started.json"); let mut config = MyConfig { setting: "default".into() }; let config_path = "example_getting_started.json"; config.save(config_path, Format::JSON).unwrap(); + config.setting = "something else"; config.load(config_path, Format::JSON).unwrap(); - # // Clean up - # std::fs::remove_file(config_path).unwrap(); ``` --- @@ -227,13 +186,13 @@ The `FastConfig` trait provides methods for loading, saving, and serializing con ### The `#[derive(FastConfig)]` Macro -The derive macro automatically implements the `FastConfig` trait for your struct. It requires that your struct also derives `Serialize` and `Deserialize` from `serde`. +The derive macro automatically implements the `FastConfig` trait for your struct. It requires that your struct also derives `Serialize` and `Deserialize` from the [`serde`](https://crates.io/crates/serde) crate. #### Custom Crate Path If you're re-exporting `fast_config` under a different name, you can specify the crate path: -```rust,ignore +```rust use serde::Serialize; use serde::Deserialize; use fast_config::FastConfig; diff --git a/fast_config/Cargo.toml b/fast_config/Cargo.toml index 78a69da..100e754 100644 --- a/fast_config/Cargo.toml +++ b/fast_config/Cargo.toml @@ -8,6 +8,9 @@ keywords = ["settings", "config", "configuration", "simple", "json5"] categories = ["config"] exclude = ["src/tests.rs", "test.cmd"] +[lib] +doctest = false + # GitHub stuff readme = "README.md" license = "MIT" @@ -20,7 +23,6 @@ all-features = true [badges] maintenance = { status = "actively-developed" } - [dependencies] serde = { version = "1.0", features = ["derive"], optional = false } thiserror = "2.0" diff --git a/fast_config_derive/Cargo.toml b/fast_config_derive/Cargo.toml index 2299a5f..74a6700 100644 --- a/fast_config_derive/Cargo.toml +++ b/fast_config_derive/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "fast_config_derive" version = "0.1.0" -authors = ["Younes Torshizi "] +authors = ["Younes Torshizi ", "FlooferLand"] edition = "2024" [lib] name = "fast_config_derive" proc-macro = true - +doctest = false [dependencies] proc-macro2 = { version = "1", features = ["proc-macro"] }