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/Cargo.toml b/Cargo.toml
index a70bf9e..60bad6a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,42 +1,3 @@
-[package]
-name = "fast_config"
-version = "1.2.1"
-edition = "2021"
-authors = ["FlooferLand"]
-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 = "1.0"
-
-# Optional
-json5 = { version = "0.4", optional = true }
-toml = { version = "0.8", 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..a62c717 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,73 +61,158 @@ anything that isn't working as expected!
---
## Examples:
+
+### Basic Usage
+
```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)]
+// Create a config struct and derive FastConfig
+#[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();
+// Create the data with default values
+let mut data = MyData {
+ student_debt: 20
+};
- // Creating our data (default values)
- let data = MyData {
- student_debt: 20,
- };
+// Save to create the file
+data.save("test/myconfig.json5", Format::JSON5).unwrap();
- // Creating a new config struct with our data struct
- let mut config = Config::new("./config/myconfig.json5", data).unwrap();
+// Load from the file
+data.load("test/myconfig.json5", 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);
+// 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
- config.save().unwrap();
-}
+// Save it back to disk
+data.save("test/myconfig.json5", Format::JSON5).unwrap();
```
-## Getting started
+### Creating Config from File
+
+```rust
+let data = MyData::new("example_config.json", Format::JSON).unwrap();
+```
-1. Add the crate to your project via
`cargo add fast_config`
- - Additionally, also add `serde` as it is required!
+### String Serialization
-2. Enable the feature(s) for the format(s) you'd like to use
- - Currently only `json5`, `toml`, and `yaml` are supported
+```rust
+// 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();
+```
-3. Create a struct to hold your data that derives `Serialize` and `Deserialize`
+### Pretty Formatting
-4. Create an instance of your data struct
- - Optionally `use` the crate's `Config` type for convenience: `use fast_config::Config;`
+```rust
+// Saves in a format thats indented and human-readable
+data.save_pretty("config.json", Format::JSON).unwrap();
+```
+
+## Getting started
+
+1. Add the crate to your project:
+ ```bash
+ cargo add fast_config
+ ```
+ - Also add `serde` with derive features:
+ ```bash
+ cargo add serde --features derive
+ ```
+
+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
+ 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();
```
- 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 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
+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 828331e..0000000
--- a/examples/advanced.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use fast_config::{Config, ConfigSetupOptions};
-use fast_config::error::{ConfigError, DataParseError};
-use serde::{Serialize, Deserialize};
-
-// 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)]
-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() {
- // 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()
- );
-
- // 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!")
- }
- }
- };
-
- // 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();
-}
diff --git a/examples/simple.rs b/examples/simple.rs
deleted file mode 100644
index 649381a..0000000
--- a/examples/simple.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use fast_config::Config;
-use serde::{Serialize, Deserialize};
-
-// Creating a config struct to store our data
-#[derive(Serialize, Deserialize)]
-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();
-}
diff --git a/fast_config/Cargo.toml b/fast_config/Cargo.toml
new file mode 100644
index 0000000..100e754
--- /dev/null
+++ b/fast_config/Cargo.toml
@@ -0,0 +1,45 @@
+[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"]
+
+[lib]
+doctest = false
+
+# 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..c942362
--- /dev/null
+++ b/fast_config/src/lib.rs
@@ -0,0 +1,134 @@
+#![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 enable 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 file's 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 FastConfig
+where
+ 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>;
+ 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")]
+ 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)
+ }
+ #[cfg(any(
+ feature = "json",
+ feature = "json5",
+ feature = "toml",
+ feature = "yaml"
+ ))]
+ 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)
+ }
+ #[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")]
+ 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)
+ }
+ 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)]
+#[cfg(feature = "derive")]
+mod tests;
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/Cargo.toml b/fast_config_derive/Cargo.toml
new file mode 100644
index 0000000..74a6700
--- /dev/null
+++ b/fast_config_derive/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "fast_config_derive"
+version = "0.1.0"
+authors = ["Younes Torshizi ", "FlooferLand"]
+edition = "2024"
+
+[lib]
+name = "fast_config_derive"
+proc-macro = true
+doctest = false
+
+[dependencies]
+proc-macro2 = { version = "1", features = ["proc-macro"] }
+quote = { version = "1", features = ["proc-macro"] }
+syn = { version = "2" }
diff --git a/fast_config_derive/src/lib.rs b/fast_config_derive/src/lib.rs
new file mode 100644
index 0000000..3d34a56
--- /dev/null
+++ b/fast_config_derive/src/lib.rs
@@ -0,0 +1,79 @@
+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, attributes(fast_config))]
+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 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
+ Self: for<'a> Deserialize<'a> + Serialize + Sized
+ };
+ quote! {
+ 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)?;
+ std::io::Read::read_to_string(&mut file, &mut content)?;
+ *self = #crate_path::FastConfig::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::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> {
+ 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::FastConfig::to_string_pretty(self, format)?;
+ use std::io::Write;
+ write!(file, "{}", content)?;
+ Ok(())
+ }
+ }
+ }.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 }
+}
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
-}