diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 700d516..7da0990 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -1,7 +1,14 @@
---
name: Rust
-on: [push, pull_request]
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+ - dev
jobs:
format:
@@ -10,7 +17,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Install stable components
uses: actions-rs/toolchain@v1
@@ -31,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Install stable components
uses: actions-rs/toolchain@v1
@@ -46,34 +53,13 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
args: --verbose --release -- -D warnings
- wasm:
- name: Check (wasm)
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
-
- - name: Install WASM target
- uses: actions-rs/toolchain@v1
- with:
- toolchain: stable
- override: true
- target: wasm32-unknown-unknown
-
- - name: Check for WASM
- uses: actions-rs/cargo@v1
- with:
- command: check
- args: --verbose --release --target wasm32-unknown-unknown
-
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
@@ -85,7 +71,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
- args: --verbose
+ args: --verbose -- --test-threads=1
bench:
name: Benchmark
@@ -93,7 +79,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
diff --git a/Cargo.lock b/Cargo.lock
index c3ee27b..40a4620 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -45,7 +45,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "confgr"
-version = "0.1.1"
+version = "0.2.0"
dependencies = [
"confgr_core",
"confgr_derive",
@@ -58,7 +58,7 @@ dependencies = [
[[package]]
name = "confgr_core"
-version = "0.1.2"
+version = "0.2.0"
dependencies = [
"config",
"serde",
@@ -67,7 +67,7 @@ dependencies = [
[[package]]
name = "confgr_derive"
-version = "0.1.2"
+version = "0.2.0"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index cf00370..dfd2610 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
description = "A simple rust application configuration derive macro."
name = "confgr"
-version = "0.1.1"
+version = "0.2.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/N4D1K-lgtm/confgr"
@@ -13,8 +13,8 @@ categories = ["config", "rust-patterns", "development-tools"]
homepage = "https://github.com/N4D1K-lgtm/confgr"
[dependencies]
-confgr_derive = { path = "crates/confgr_derive", version = "0.1.2" }
-confgr_core = { path = "crates/confgr_core", version = "0.1.2" }
+confgr_derive = { path = "crates/confgr_derive", version = "0.2.0" }
+confgr_core = { path = "crates/confgr_core", version = "0.2.0" }
config = "0.14.0"
[dev-dependencies]
@@ -27,7 +27,7 @@ tempfile = "3.10.1"
members = ["crates/*"]
[workspace.package]
-version = "0.1.2"
+version = "0.2.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/N4D1K-lgtm/confgr"
diff --git a/README.md b/README.md
index f13a303..38aff73 100644
--- a/README.md
+++ b/README.md
@@ -46,72 +46,183 @@
Built with ❤️ by Kidan Nelson
-## Overview
+# Overview
-[`confgr`](https://docs.rs/confgr/latest/confgr) is a crate that enables easily managing rust application configuration by automatically deriving functionality to load settings from environment variables, configuration files, and default values. This is done by procedurally parsing struct fields to build environment variable keys as well as deserialization using [`serde`](https://docs.rs/serde/latest/serde/) from a provided config file path. Functionality is customizable through several macro attribute helpers.
+The [`Config`](https://docs.rs/confgr/latest/confgr/prelude/derive.Config.html) derive macro simplifies application configuration by automatically loading
+settings from various sources in the following order:
-The order of priority is Environment Variable -> Config File -> Default Value. If a `config(path = "filepath")` attribute is not present, a config file will not be loaded, and `config(skip)` may be used to skip the environment variable step.
-
-| Attribute | Functionality |
-| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
-| prefix | Sets the prefix for environment variables, can be set at the struct or field level. |
-| path | Specifies the path to the configuration file, the extension may be omitted. |
-| key | Overrides the default key name for an attribute, ignores the prefix and field name. |
-| nest | Necessary for non standard types, these must also derive `Config` |
-| skip | Skips loading the attribute from an environment variable. |
-| separator | Sets the separator character that is placed between the prefix and the field name, can be set at the struct or field level, default is "\_" |
+1. **Environment Variables**.
+2. **Configuration File** (e.g., `toml`, `json`, `yaml`, `ini`, `ron`, `json5`).
+3. **Default Values**.
## Key Features
-- **Simplicity**: Minimal boilerplate, as simple as annotating your struct and a struct with named fields and a single method.
-- **Flexibility**: Supports loading configuration data from environment variables, a single `toml`, `json`, `yaml`, `xml`, `ini`, `ron` or `json5` configuration file with default trait implementations as a fall-back.
-- **Integration**: Integrates conveniently with other macros such as [`smart_default`](https://docs.rs/smart-default/latest/smart_default/derive.SmartDefault.html).
+- **Simplicity**: Minimal boilerplate. Define your configuration struct, customize the macro, and you're good to go.
+- **Flexibility**: Supports a variety of configuration file formats including `toml`, `json`, `yaml`, `ini`, `ron`, and `json5`.
+- **Integration**: Synergy with other crates, such as [`smart_default`](https://docs.rs/smart_default/latest/smart_default/).
+
+There are also several useful helper attributes for customizing the behavior of the derive macro.
+
+| Attribute | Functionality |
+| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `prefix` | Sets a prefix for environment variables. Can be applied at the struct or field level. |
+| `path` | Specifies the static path to a configuration file. The file extension may (though probably shouldn't) be omitted. |
+| `env_path` | Resolves an environment variable at runtime to determine the configuration file path. |
+| `default_path` | Specifies a fallback path used if the path determined by `env_path` does not exist. |
+| `key` | Overrides the default environment variable name. This ignores the prefix and uses the provided key directly. |
+| `name` | forwards to `#[serde(rename = "_")]` to rename fields during serialization/deserialization. It does not affect environment variable names. |
+| `nest` | Required for non-standard types which must also derive [`Config`](https://docs.rs/confgr/latest/confgr/prelude/derive.Config.html), used for nesting configuration structs. |
+| `skip` | Skips loading the attribute from an environment variable. Necessary for types that don't implement [`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html) but are present in the configuration file. |
+| `separator` | Specifies a character to separate the prefix and the field name. The default separator is "\_". |
+
+## Path Attribute Behavior
+
+- **`env_path`**: Resolves the provided environment variable into configuration filepath. This
+ takes precedence over `path` and `default_path`, but will not panic if the file or environment
+ does not exist.
+- **`path`**: Directly sets the path to the configuration file. When set, `default_path` may not be used. Panics if the file does not exist.
+- **`default_path`**: Identical to `path`, but does not panic if the file does not exist.
## Usage
-The simplest way to use `confgr` is as follows:
+[`serde`](https://docs.rs/serde) is a required dependency.
+
+```toml
+ [dependencies]
+ confgr = "0.2.0"
+ serde = { version = "1.0", features = ["derive"] }
+```
+
+Then define your configuration like so:
```rust
use confgr::prelude::*;
#[derive(Config)]
-#[config(path = "config.toml")]
+#[config(path = "docs.toml", prefix = "APP")]
pub struct AppConfig {
port: u32,
address: String,
+ #[config(key = "DEBUG_MODE")]
+ debug: bool,
}
-// Default implementations are required.
+// Default implementation is required.
impl Default for AppConfig {
fn default() -> Self {
Self {
port: 3000,
address: "127.0.0.1".to_string(),
+ debug: false
}
}
}
+
+std::env::set_var("APP_PORT", "4000");
+std::env::set_var("DEBUG_MODE", "true");
+
+let settings = AppConfig::load_config();
+
+assert_eq!(settings.port, 4000);
+assert_eq!(settings.address, "127.0.0.1");
+assert!(settings.debug)
+```
+
+> Check out the [examples](/examples) directory for more.
+
+## Warnings/Pitfalls
+
+- Nested structs do not load separate files based on their own `path` attributes. If
+ you would like multiple files to be loaded, you must use multiple structs with multiple
+ [`load_config()`]() calls. This may change in a future version.
+- Types that do not implement [`FromStr`](std::str::FromStr) must use `#[config(skip)]` or `#[config(nest)]`.
+- The `separator` character is only inserted between the prefix and the field name, not in any
+ part of the parsed field name.
+- The `prefix` is applied per field or for the entire struct, but is ignored if `#[config(key = "_")]` is used.
+- All configuration structs must implement [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html).
+- Types used in configuration structs must implement [`Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html),
+ [`Clone`](https://doc.rust-lang.org/std/clone/trait.Clone.html),
+ [`Debug`](https://doc.rust-lang.org/std/fmt/trait.Debug.html) and
+ [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html).
+- [`Option`](https://doc.rust-lang.org/std/option/enum.Option.html) is not currently compatible with `#[config(nest)]`
+ on types that implement [`Confgr`](https://docs.rs/confgr/latest/confgr/core/trait.Confgr.html).
+
+## Debugging
+
+When encountering issues using the macro, the following methods may be of use.
+
+### Verifying Environment Variables
+
+The [`get_env_keys()`](core::Confgr::get_env_keys) method can be used to retrieve the
+resolved environment variable keys based on the struct's configuration.
+
+```rust
+use std::collections::HashMap;
+use confgr::prelude::*;
+
+#[derive(Config, Default)]
+#[config(prefix = "APP")]
+pub struct AppConfig {
+ port: u32,
+ #[config(separator = "__")]
+ address: String,
+ #[config(key = "DEBUG_MODE")]
+ debug: bool,
+}
+
+let keys: HashMap = AppConfig::get_env_keys();
+
+assert_eq!(keys["port"], "APP_PORT");
+assert_eq!(keys["address"], "APP__ADDRESS");
+assert_eq!(keys["debug"], "DEBUG_MODE");
```
-Then you can load your settings like so:
+### Verifying Configuration File Path
+
+You can use [`check_file()`](core::Confgr::check_file) to ensure that the configuration file
+is accessible the path specified or resolved in the `path`, or `env_path` or `default_path` attributes.
```rust
-fn main() {
- std::env::set_var("PORT", "4000");
-
- // AppConfig {
- // port: 4000,
- // address: "127.0.0.1"
- // }
- let settings = AppConfig::load_config();
+use confgr::prelude::*;
+
+#[derive(Config, Default)]
+#[config(path = "docs.toml", env_path = "APP_CONFIG_FILE")]
+pub struct AppConfig {
+ port: u32,
+ debug: bool,
}
+
+std::env::set_var("APP_CONFIG_FILE", "env_config.toml");
+AppConfig::check_file().expect("Failed to open configuration file.");
+
+std::env::remove_var("APP_CONFIG_FILE");
+AppConfig::check_file().expect("Failed to open configuration file.");
+
```
-This is intended to easily be used inside of something like [`std::sync::OnceLock`](https://doc.rust-lang.org/nightly/std/sync/struct.OnceLock.html)
+### Test Deserialization
+
+The [`deserialize_from_file()`]() method can be used to manually test the config deserialization step. This
+will give you the parsed configuration struct before default values are applied.
+
+```rust
+use confgr::prelude::\*;
+
+#[derive(Config, Default)] #[config(path = "docs.toml")]
+pub struct AppConfig {
+port: u32,
+debug: bool,
+}
+
+let config = AppConfig::deserialize_from_file().expect("Failed to deserialize configuration.");
+println!("Deserialized configuration: {:?}", config);
+```
## Considerations
-- **Version Flexibility**: This is an initial release (v0.1.0), and as such, it is not fully optimized. The implementation involves some cloning for simplicity, which may impact performance in large-scale applications.
+- **Version Instability**: As of now, this crate is in an unstable development phase and I reserve the right to make breaking changes in future versions
+ without obligation to maintain backwards compatibility.
- **Production Use Caution**: This is my first published Rust crate, while it is fully functional and useful for me, it's advisable not to rely heavily on this library in critical production environments without thorough testing, especially where guarantees of stability and performance are required.
-- **Contribution**: Contributions are welcome! Whether it's feature requests, bug reports, or pull requests, i'd love some constructive feedback!
+- **Contribution**: Contributions are welcome! Whether it's feature requests, bug reports, or pull requests, I'd love some constructive feedback!
> I highly recommend checking out the [`config`](https://docs.rs/config/latest/config/) crate as it is a feature complete non-proc-macro alternative. This crate actually relies on `config` for file parsing.
diff --git a/crates/confgr_core/src/lib.rs b/crates/confgr_core/src/lib.rs
index df13ace..0419786 100644
--- a/crates/confgr_core/src/lib.rs
+++ b/crates/confgr_core/src/lib.rs
@@ -1,12 +1,12 @@
use std::collections::HashMap;
use thiserror::Error;
-/// Error type for configuration operations.
+/// Shared error type for configuration-related errors.
#[derive(Error, Debug)]
pub enum ConfgrError {
#[error("Config File IO Error: {0}")]
File(#[from] std::io::Error),
- #[error("Missing 'path' or 'path_env' attribute.")]
+ #[error("Configured filepath does not exist.")]
NoFilePath,
#[error("Config Error: {0}")]
Config(#[from] config::ConfigError),
@@ -35,34 +35,106 @@ pub trait FromFile: Sized {
fn get_file_path() -> Option;
}
-/// Configuration trait that combines [`FromEnv`], [`FromFile`], and [`Merge`] traits. Implemented
-/// by the derive macro on the original configuration struct.
+/// Provides a unified approach to load configurations from environment variables,
+/// files, and default settings. This trait is typically derived using a macro to automate
+/// implementations based on struct field names and annotations.
pub trait Confgr
where
Self: Sized,
{
type Layer: Default + FromEnv + Merge + FromFile + From + Into;
+ /// Loads and merges configurations from files, environment variables, and default values.
+ /// Order of precedence: Environment variables, file configurations, default values.
+ ///
+ /// # Examples
+ ///
+ /// ```rust no_run
+ /// let config = AppConfig::load_config();
+ /// assert_eq!(config.port, 8080);
+ /// ```
fn load_config() -> Self {
- let file_layer = Self::Layer::from_file().unwrap_or_else(|_| Self::Layer::default());
+ let file_layer = match Self::deserialize_from_file() {
+ Ok(file_layer) => file_layer,
+ Err(_e) => Self::Layer::default(),
+ };
+
let default_layer = Self::Layer::default();
let env_layer = Self::Layer::from_env();
env_layer.merge(file_layer.merge(default_layer)).into()
}
+ /// Attempts to deserialize configuration from a file.
+ /// This method is a part of the file loading phase of the configuration process.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`ConfgrError`] if the file cannot be read.
+ ///
+ /// # Examples
+ ///
+ /// ``` rust no_run
+ /// let file_layer = AppConfig::deserialize_from_file();
+ /// match file_layer {
+ /// Ok(layer) => println!("Configuration loaded from file."),
+ /// Err(e) => eprintln!("Failed to load configuration: {}", e),
+ /// }
+ /// ```
fn deserialize_from_file() -> Result {
Self::Layer::from_file()
}
+ /// Checks the accessibility of the specified configuration file.
+ ///
+ /// # Returns
+ ///
+ /// [`Ok`] if the file is accessible, otherwise an [`Err`]\([`ConfgrError`]) if the file cannot be found or opened.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// if AppConfig::check_file().is_ok() {
+ /// println!("Configuration file is accessible.");
+ /// } else {
+ /// println!("Cannot access configuration file.");
+ /// }
+ /// ```
fn check_file() -> Result<(), ConfgrError> {
Self::Layer::check_file()
}
+ /// Retrieves the map of environment variable keys associated with the configuration properties.
+ ///
+ /// # Returns
+ ///
+ /// A [`HashMap`] where the keys are property names and the values are the corresponding environment variable names.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let env_keys = AppConfig::get_env_keys();
+ /// assert_eq!(env_keys["port"], "APP_PORT");
+ /// ```
fn get_env_keys() -> HashMap {
Self::Layer::get_env_keys()
}
+ /// Gets the file path used for loading the configuration, if specified.
+ ///
+ /// # Returns
+ ///
+ /// An [`Option`] which is `Some(path)` if a path is set, otherwise `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// if let Some(path) = AppConfig::get_file_path() {
+ /// println!("Configuration file used: {}", path);
+ /// } else {
+ /// println!("No specific configuration file used.");
+ /// }
+ /// ```
fn get_file_path() -> Option {
Self::Layer::get_file_path()
}
diff --git a/crates/confgr_derive/src/config.rs b/crates/confgr_derive/src/config.rs
index fddd3f3..7b3e03f 100644
--- a/crates/confgr_derive/src/config.rs
+++ b/crates/confgr_derive/src/config.rs
@@ -7,6 +7,7 @@ pub fn generate_config_impl(name: &Ident) -> TokenStream {
let layer_name = format_ident!("{}{}", name, SUFFIX);
quote! {
+ #[automatically_derived]
impl ::confgr::core::Confgr for #name {
type Layer = #layer_name;
}
diff --git a/crates/confgr_derive/src/convert.rs b/crates/confgr_derive/src/convert.rs
index 941ae59..d7313f0 100644
--- a/crates/confgr_derive/src/convert.rs
+++ b/crates/confgr_derive/src/convert.rs
@@ -42,6 +42,7 @@ pub fn generate_conversion_impl(
});
quote! {
+ #[automatically_derived]
impl ::core::convert::From<#layer_name> for #name {
fn from(#LAYER_PARAMETER: #layer_name) -> Self {
Self {
@@ -50,6 +51,7 @@ pub fn generate_conversion_impl(
}
}
+ #[automatically_derived]
impl ::core::convert::From<#name> for #layer_name {
fn from(#BASE_PARAMETER: #name) -> Self {
Self {
diff --git a/crates/confgr_derive/src/env.rs b/crates/confgr_derive/src/env.rs
index 3bb99bd..35bf618 100644
--- a/crates/confgr_derive/src/env.rs
+++ b/crates/confgr_derive/src/env.rs
@@ -104,6 +104,7 @@ pub fn generate_from_env(
});
quote! {
+ #[automatically_derived]
impl ::confgr::core::FromEnv for #layer_name {
fn from_env() -> Self {
Self {
diff --git a/crates/confgr_derive/src/file.rs b/crates/confgr_derive/src/file.rs
index bce602d..bce00b2 100644
--- a/crates/confgr_derive/src/file.rs
+++ b/crates/confgr_derive/src/file.rs
@@ -10,17 +10,17 @@ pub(crate) fn generate_from_file(name: &Ident, attributes: &ConfigAttributes) ->
panic!("'path' and 'default_path' attributes cannot be used alongside eachother");
}
- let get_file_path_def = if let Some(path_env) = &attributes.path_env {
+ let get_file_path_def = if let Some(env_path) = &attributes.env_path {
match (&attributes.path, &attributes.default_path) {
(Some(path), None) => {
quote! {
fn get_file_path() -> Option {
- match std::env::var(#path_env) {
+ match std::env::var(#env_path) {
Ok(env_val) => if std::path::Path::new(&env_val).exists() { Some(env_val) }
else if std::path::Path::new(#path).exists() { Some(#path.to_string()) }
- else { panic!("'path_env' and 'path' attributes resolve to non-existent or invalid files.") },
+ else { panic!("'env_path' and 'path' attributes resolve to non-existent or invalid files.") },
Err(_) => if std::path::Path::new(#path).exists() { Some(#path.to_string()) }
- else { panic!("'path_env' variable is not set and the provided 'path' attribute is invalid or references a non-existent file.") }
+ else { panic!("'env_path' variable is not set and the provided 'path' attribute is invalid or references a non-existent file.") }
}
}
}
@@ -28,7 +28,7 @@ pub(crate) fn generate_from_file(name: &Ident, attributes: &ConfigAttributes) ->
(None, Some(default_path)) => {
quote! {
fn get_file_path() -> Option {
- match std::env::var(#path_env) {
+ match std::env::var(#env_path) {
Ok(env_val) => if std::path::Path::new(&env_val).exists() { Some(env_val) }
else if std::path::Path::new(#default_path).exists() { Some(#default_path.to_string()) }
else { None },
@@ -41,7 +41,7 @@ pub(crate) fn generate_from_file(name: &Ident, attributes: &ConfigAttributes) ->
_ => {
quote! {
fn get_file_path() -> Option {
- std::env::var(#path_env)
+ std::env::var(#env_path)
.map(|env_val| {
if std::path::Path::new(&env_val).exists() {
Some(env_val)
@@ -82,6 +82,7 @@ pub(crate) fn generate_from_file(name: &Ident, attributes: &ConfigAttributes) ->
};
quote! {
+ #[automatically_derived]
impl ::confgr::core::FromFile for #layer_name {
#get_file_path_def
diff --git a/crates/confgr_derive/src/lib.rs b/crates/confgr_derive/src/lib.rs
index 2dfe9ce..27d5487 100644
--- a/crates/confgr_derive/src/lib.rs
+++ b/crates/confgr_derive/src/lib.rs
@@ -175,15 +175,15 @@ pub(crate) fn parse_config_field_attributes(
if named_value.path.is_ident(ENV_PATH_ATTRIBUTE) =>
{
if let Expr::Lit(ExprLit {
- lit: Lit::Str(path_env),
+ lit: Lit::Str(env_path),
..
}) = &named_value.value
{
- attributes.path_env = Some(path_env.value());
+ attributes.env_path = Some(env_path.value());
} else {
errors.push(Error::new_spanned(
named_value.into_token_stream(),
- "Expected a string for 'path_env'",
+ "Expected a string for 'env_path'",
));
}
}
@@ -255,7 +255,7 @@ pub(crate) struct ConfigAttributes {
key: Option,
separator: Option,
path: Option,
- path_env: Option,
+ env_path: Option,
default_path: Option,
name: Option,
}
diff --git a/crates/confgr_derive/src/merge.rs b/crates/confgr_derive/src/merge.rs
index 742ba5b..6b5b60c 100644
--- a/crates/confgr_derive/src/merge.rs
+++ b/crates/confgr_derive/src/merge.rs
@@ -56,6 +56,7 @@ pub fn generate_layer(
);
quote! {
+ #[automatically_derived]
#[derive(::serde::Deserialize, Debug, Clone)]
#[doc(hidden)]
#struct_rename
@@ -63,12 +64,14 @@ pub fn generate_layer(
#( #field_defs ),*
}
+ #[automatically_derived]
impl Default for #layer_name {
fn default() -> Self {
#name::default().into()
}
}
+ #[automatically_derived]
impl ::confgr::core::Merge for #layer_name {
fn merge(self, other: Self) -> Self {
Self {
@@ -77,6 +80,7 @@ pub fn generate_layer(
}
}
+ #[automatically_derived]
impl ::confgr::core::Empty for #layer_name {
fn empty() -> Self {
Self {
@@ -86,4 +90,3 @@ pub fn generate_layer(
}
}
}
-
diff --git a/src/lib.rs b/src/lib.rs
index 3a4e96a..536f698 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,31 +1,54 @@
//! # Overview
//!
-//! The [`confgr`](self) crate simplifies application configuration by automatically loading
-//! settings from environment variables, then from a configuration file, and then default values.
+//! The [`Config`](self::derive::Config) derive macro simplifies application configuration by automatically loading
+//! settings from various sources in the following order:
+//! 1. **Environment Variables**.
+//! 2. **Configuration File** (e.g., `toml`, `json`, `yaml`, `ini`, `ron`, `json5`).
+//! 3. **Default Values**.
//!
-//! As such, your configuration may never fail, without sacrificing flexibility.
+//! ## Key Features
//!
-//! Several useful helper attributes are provided to customize the behavior of the proc-macro.
+//! - **Simplicity**: Minimal boilerplate. Define your configuration struct, customize the macro, and you're good to go.
+//! - **Flexibility**: Supports a variety of configuration file formats including `toml`, `json`, `yaml`, `ini`, `ron`, and `json5`.
+//! - **Integration**: Synergy with other crates, such as [`smart_default`](https://docs.rs/smart_default/latest/smart_default/).
//!
-//! | Attribute | Functionality |
-//! |-------------|--------------------------------------------------------------------------------------------------------------------------------------------|
-//! | `prefix` | Sets a prefix for environment variables. Can be applied at the struct or field level. |
-//! | `path` | Specifies the path to the configuration file. The file extension may be omitted. |
-//! | `path_env` | Resolves an environment variable at runtime to determine the configuration file path. |
-//! | `key` | Overrides the default environment variable name. This ignores the prefix and uses the provided key directly. |
-//! | `nest` | Required for non-standard types which must also derive [`Config`](self::derive::Config), useful for nesting configuration structs. |
-//! | `skip` | Skips loading the attribute from an environment variable. Useful for types that don't implement [`FromStr`](std::str::FromStr) but are present in the configuration file. |
-//! | `separator` | Specifies a character to separate the prefix and the field name. The default separator is "_". |
+//! There are also several useful helper attributes for customizing the behavior of the derive macro.
//!
-//! ## Key Features
+//! | Attribute | Functionality |
+//! |---------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+//! | `prefix` | Sets a prefix for environment variables. Can be applied at the struct or field level. |
+//! | `path` | Specifies the static path to a configuration file. The file extension may (though probably shouldn't) be omitted. |
+//! | `env_path` | Resolves an environment variable at runtime to determine the configuration file path. |
+//! | `default_path`| Specifies a fallback path used if the path determined by `env_path` does not exist. |
+//! | `key` | Overrides the default environment variable name. This ignores the prefix and uses the provided key directly. |
+//! | `name` | forwards to `#[serde(rename = "_")]` to rename fields during serialization/deserialization. It does not affect environment variable names. |
+//! | `nest` | Required for non-standard types which must also derive [`Config`](self::derive::Config), used for nesting configuration structs. |
+//! | `skip` | Skips loading the attribute from an environment variable. Necessary for types that don't implement [`FromStr`](std::str::FromStr) but are present in the configuration file. |
+//! | `separator` | Specifies a character to separate the prefix and the field name. The default separator is "_". |
//!
-//! - **Simplicity**: Minimal boilerplate. Define your configuration struct, add useful annotations, and you're good to go.
-//! - **Flexibility**: Supports a variety of configuration file formats including `toml`, `json`, `yaml`, `ini`, `ron`, and `json5`.
-//! - **Integration**: Synergy with other Rust macros and libraries, such as [`smart_default`](https://docs.rs/smart-default/latest/smart_default/).
+//! ## Path Attribute Behavior
+//!
+//! - **`env_path`**: Resolves the provided environment variable into configuration filepath. This
+//! takes precedence over `path` and `default_path`, but will not panic if the file or environment
+//! does not exist.
+//!
+//! - **`path`**: Directly sets the path to the configuration file. When set, `default_path` may not be used. Panics if the file does not exist.
+//!
+//! - **`default_path`**: Identical to `path`, but does not panic if the file does not exist.
+//!
+//! ## Usage
//!
-//! ## Usage Example
+//!
//!
-//! Below is a simple example demonstrating how to use [`confgr`](self) to load application settings:
+//! [`serde`](https://docs.rs/serde) is a required dependency.
+//!
+//! ```toml
+//! [dependencies]
+//! confgr = "0.2.0"
+//! serde = { version = "1.0", features = ["derive"] }
+//! ```
+//!
+//! Then define your configuration like so:
//!
//! ```rust
//! use confgr::prelude::*;
@@ -43,7 +66,7 @@
//! debug: bool,
//! }
//!
-//! // A Default implementation is required.
+//! // Default implementation is required.
//! impl Default for AppConfig {
//! fn default() -> Self {
//! Self {
@@ -58,6 +81,7 @@
//! std::env::set_var("DEBUG_MODE", "true");
//!
//! let settings = AppConfig::load_config();
+//!
//! # std::fs::remove_file("docs.toml").unwrap();
//!
//! assert_eq!(settings.port, 4000);
@@ -65,15 +89,18 @@
//! assert!(settings.debug)
//! ```
//!
-//! ## Important Information
+//! ## Warnings/Pitfalls
//!
//! - Nested structs do not load separate files based on their own `path` attributes. If
//! you would like multiple files to be loaded, you must use multiple structs with multiple
-//! [`load_config()`](core::Confgr::load_config()) calls.
-//! - Types that do not implement [`FromStr`](std::str::FromStr) must use `config(skip)` or `config(nest)`.
-//! - The `separator` character is only inserted between the prefix and the field name.
-//! - The `prefix` is applied per field or for the entire struct, and it's ignored if `config(key)` is used.
-//! - All structs must implement [`Default`].
+//! [`load_config()`](core::Confgr::load_config()) calls. This may change in a future version.
+//! - Types that do not implement [`FromStr`](std::str::FromStr) must use `#[config(skip)]` or `#[config(nest)]`.
+//! - The `separator` character is only inserted between the prefix and the field name, not in any
+//! part of the parsed field name.
+//! - The `prefix` is applied per field or for the entire struct, but is ignored if `#[config(key = "_")]` is used.
+//! - All configuration structs must implement [`Default`].
+//! - Types used in configuration structs must implement [`Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html), [`Clone`], [`Debug`] and [`Default`].
+//! - [`Option`] is not currently compatible with `#[config(nest)]` on types that implement [`Confgr`](self::core::Confgr).
//!
//! ## Debugging
//!
@@ -108,28 +135,32 @@
//! ### Verifying Configuration File Path
//!
//! You can use [`check_file()`](core::Confgr::check_file) to ensure that the configuration file
-//! is accessible the path specified or resolved in the `path`, or `path_env` attribute.
+//! is accessible the path specified or resolved in the `path`, or `env_path` attribute.
//!
//! ```rust
//! use confgr::prelude::*;
//!
//! # use std::fs::File;
//! # use std::io::Write;
+//! # let mut file = std::fs::File::create("env_config.toml").unwrap();
+//! # writeln!(file, "port = 3000\ndebug = false");
//! # let mut file = std::fs::File::create("docs.toml").unwrap();
//! # writeln!(file, "port = 3000\ndebug = false");
//! #[derive(Config, Default)]
-//! #[config(path = "docs.toml", path_env = "APP_CONFIG_FILE")]
+//! #[config(path = "docs.toml", env_path = "APP_CONFIG_FILE")]
//! pub struct AppConfig {
//! port: u32,
//! debug: bool,
//! }
//!
-//! std::env::set_var("APP_CONFIG_FILE", "config.toml");
+//! std::env::set_var("APP_CONFIG_FILE", "env_config.toml");
//! AppConfig::check_file().expect("Failed to open configuration file.");
//!
//! std::env::remove_var("APP_CONFIG_FILE");
//! AppConfig::check_file().expect("Failed to open configuration file.");
+//!
//! # std::fs::remove_file("docs.toml").unwrap();
+//! # std::fs::remove_file("env_config.toml").unwrap();
//! ```
//!
//! ### Test Deserialization
@@ -156,18 +187,23 @@
//!
//! # std::fs::remove_file("docs.toml").unwrap();
//! ```
+
+/// Traits and types consumed by the [`Config`](confgr_derive::Config) derive macro. Re-export from [`confgr_core`].
pub mod core {
pub use confgr_core::*;
}
+/// Derive macro for the [`Confgr`](confgr_core::Confgr) trait. Re-export from [`confgr_derive`].
pub mod derive {
pub use confgr_derive::*;
}
+#[doc(hidden)]
pub mod config {
pub use config::{Config, ConfigError, File};
}
+/// Macro and trait exports for convenience.
pub mod prelude {
pub use crate::core::{Confgr, Empty, FromEnv, FromFile, Merge};
pub use crate::derive::Config;
diff --git a/tests/complex.rs b/tests/complex.rs
index 3e1848e..a96e0f1 100644
--- a/tests/complex.rs
+++ b/tests/complex.rs
@@ -8,11 +8,11 @@ use std::fs::File;
use std::io::Write;
#[derive(Config, SmartDefault)]
-#[config(path = "tests/complex_settings.toml", name = "config")]
-struct Config {
+#[config(path = "tests/complex_settings.toml")]
+struct AppConfig {
#[config(nest)]
- service: Service,
- #[config(skip)]
+ service: ServiceConfig,
+ #[config(skip, name = "secret")]
secret_key: Option,
#[config(skip)]
features: Vec,
@@ -21,8 +21,7 @@ struct Config {
}
#[derive(Config, SmartDefault)]
-#[config(name = "service")]
-struct Service {
+struct ServiceConfig {
#[default = "https://localhost:3000"]
url: String,
#[default = true]
@@ -34,17 +33,17 @@ struct Service {
}
#[derive(Config, Default)]
-#[config(env_path = "OPTIONAL_ENV_CONFIG", name = "optional_env")]
+#[config(env_path = "OPTIONAL_ENV_CONFIG")]
struct OptionalEnvConfig {
#[config(skip)]
debug_mode: Option,
- #[config(nest)]
- db_settings: DbSettings,
+ #[config(nest, name = "db")]
+ db_settings: DatabaseConfig,
}
#[derive(Config, SmartDefault)]
-#[config(name = "db")]
-struct DbSettings {
+struct DatabaseConfig {
+ #[default = "localhost"]
host: String,
port: u32,
#[config(skip)]
@@ -52,61 +51,75 @@ struct DbSettings {
credentials: Option,
}
-#[derive(Config, SmartDefault, Debug, Clone, Deserialize)]
-#[config(name = "credentials")]
+#[derive(SmartDefault, Debug, Clone, Deserialize)]
struct Credentials {
#[default = "admin"]
username: String,
#[default = "password"]
+ #[allow(dead_code)]
password: String,
}
fn setup_complex_config_files() {
let complex_settings = r#"
+ secret = "sup3rs3cr3t"
+
[service]
url = "http://example.com/api"
enabled = true
ports = [8080, 8081]
+
[service.metadata]
environment = "production"
version = "1.0.0"
- features = ["alpha", "beta"]
+
[parameters]
retries = "3"
timeout = "100"
"#;
- let mut settings_file = File::create("tests/complex_settings.toml").unwrap();
- writeln!(settings_file, "{}", complex_settings).unwrap();
+ let mut settings_file =
+ File::create("tests/complex_settings.toml").expect("Failed to create file");
+ writeln!(settings_file, "{}", complex_settings).expect("Failed to write to file");
}
fn cleanup_complex_config_files() {
- std::fs::remove_file("tests/complex_settings.toml").unwrap();
+ std::fs::remove_file("tests/complex_settings.toml").expect("Failed to cleanup config file");
+}
+
+#[test]
+fn test_complex_config_loads_correctly() {
+ setup_complex_config_files();
+
+ let config = AppConfig::load_config();
+
+ assert_eq!(config.service.url, "http://example.com/api");
+ assert!(config.service.enabled);
+ assert_eq!(config.service.ports.len(), 2);
+ assert_eq!(config.secret_key, Some("sup3rs3cr3t".to_string()));
+ assert_eq!(config.features.len(), 0);
+ assert_eq!(config.parameters.get("retries").unwrap(), "3");
+
+ cleanup_complex_config_files();
}
-// #[test]
-// fn test_complex_config_loads_correctly() {
-// setup_complex_config_files();
-//
-// let config = Config::load_config();
-// assert_eq!(config.service.url, "http://example.com/api");
-// assert!(config.service.enabled);
-// assert_eq!(config.service.ports.len(), 2);
-// assert_eq!(config.features.len(), 2);
-// assert_eq!(config.parameters.get("retries").unwrap(), "3");
-//
-// cleanup_complex_config_files();
-// }
-//
-// #[test]
-// fn test_optional_env_config_loads_with_defaults() {
-// env::set_var("OPTIONAL_ENV_CONFIG", "tests/complex_settings.toml");
-//
-// let config = OptionalEnvConfig::load_config();
-// assert!(config.debug_mode.is_none()); // Assuming not set in config
-// assert_eq!(config.db_settings.host, "localhost"); // Assuming default values
-// assert!(config.db_settings.credentials.is_none()); // Assuming not provided
-//
-// env::remove_var("OPTIONAL_ENV_CONFIG");
-// }
+#[test]
+fn test_optional_env_config_loads_with_defaults() {
+ env::set_var("OPTIONAL_ENV_CONFIG", "tests/complex_settings.toml");
+
+ let config = OptionalEnvConfig::load_config();
+
+ assert!(config.debug_mode.is_none());
+ assert_eq!(config.db_settings.host, "localhost");
+ assert_eq!(
+ config
+ .db_settings
+ .credentials
+ .expect("Credentials not found")
+ .username,
+ "admin"
+ );
+
+ env::remove_var("OPTIONAL_ENV_CONFIG");
+}
diff --git a/tests/complex_settings.toml b/tests/complex_settings.toml
deleted file mode 100644
index 82e4bf8..0000000
--- a/tests/complex_settings.toml
+++ /dev/null
@@ -1,14 +0,0 @@
-
- [service]
- url = "http://example.com/api"
- enabled = true
- ports = [8080, 8081]
- [service.metadata]
- environment = "production"
- version = "1.0.0"
-
- features = ["alpha", "beta"]
- [parameters]
- retries = "3"
- timeout = "100"
-
diff --git a/tests/env.rs b/tests/env.rs
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/path.rs b/tests/path.rs
index 30b92b6..55c582e 100644
--- a/tests/path.rs
+++ b/tests/path.rs
@@ -1,35 +1,32 @@
use confgr::prelude::*;
-use std::env;
-
-use std::fs::File;
-use std::io::Write;
+use std::{env, fs::File, io::Write};
#[derive(Config, Default)]
-#[config(env_path = "CONFIG_PATH_ENV", path = "tests/common/path.toml")]
+#[config(env_path = "CONFIG_ENV_PATH", path = "tests/common/path.toml")]
struct TestPathEnvAndValidPath {
name: String,
}
#[derive(Config, Default)]
-#[config(env_path = "CONFIG_PATH_ENV", path = "nonexistent.toml")]
+#[config(env_path = "CONFIG_ENV_PATH", path = "nonexistent.toml")]
struct TestPathEnvAndInvalidPath {
name: String,
}
#[derive(Config, Default)]
-#[config(env_path = "CONFIG_PATH_ENV", default_path = "test.toml")]
+#[config(env_path = "CONFIG_ENV_PATH", default_path = "test.toml")]
struct TestPathEnvWithDefault {
name: String,
}
#[derive(Config, Default)]
-#[config(env_path = "CONFIG_PATH_ENV")]
+#[config(env_path = "CONFIG_ENV_PATH")]
struct TestPathEnv {
name: String,
}
#[derive(Config, Default)]
-#[config(default_path = "tests/common/path.toml")]
+#[config(default_path = "tests/common/default.toml")]
struct TestDefaultPathValid {
name: String,
}
@@ -40,114 +37,72 @@ struct TestDefaultPathInvalid {
name: String,
}
-fn setup_test_config_files() {
- let path_contents = r#"
- name = "Path"
- "#;
-
- let path_env_contents = r#"
- name = "PathEnv"
- "#;
-
- let mut path_file = File::create("tests/common/path.toml").unwrap();
- writeln!(path_file, "{}", path_contents).unwrap();
-
- let mut path_env_file = File::create("tests/common/path_env.toml").unwrap();
- writeln!(path_env_file, "{}", path_env_contents).unwrap();
-}
-
-fn setup_default_config_file() {
- let default_path_contents = r#"
- name = "DefaultPath"
- "#;
-
- let mut default_path_file = File::create("tests/common/default.toml").unwrap();
- writeln!(default_path_file, "{}", default_path_contents).unwrap();
+fn setup_files(file_name: &str, contents: &str) {
+ let mut file = File::create(file_name).expect("Failed to create file");
+ writeln!(file, "{}", contents).expect("Failed to write to file");
}
-fn cleanup_test_config_files() {
- std::fs::remove_file("tests/common/path.toml").unwrap();
- std::fs::remove_file("tests/common/path_env.toml").unwrap();
+fn cleanup_file(file_name: &str) {
+ std::fs::remove_file(file_name).expect("Failed to delete file");
}
#[test]
-fn path_env_valid() {
- setup_test_config_files();
- env::set_var("CONFIG_PATH_ENV", "tests/common/path_env.toml");
+fn test_env_path_valid() {
+ setup_files("tests/common/env_path.toml", r#"name = "EnvPath""#);
+ env::set_var("CONFIG_ENV_PATH", "tests/common/env_path.toml");
let config = TestPathEnvAndValidPath::load_config();
+ assert_eq!(config.name, "EnvPath");
- assert_eq!(config.name, "PathEnv");
-
- env::remove_var("CONFIG_PATH_ENV");
- cleanup_test_config_files();
+ cleanup_env_and_files("CONFIG_ENV_PATH", "tests/common/env_path.toml");
}
#[test]
-fn invalid_path_env_continues_with_valid_path() {
- setup_test_config_files();
- env::set_var("CONFIG_PATH_ENV", "nonexistent_path.toml");
+fn test_invalid_env_path_continues_with_valid_path() {
+ setup_files("tests/common/path.toml", r#"name = "Path""#);
+ env::set_var("CONFIG_ENV_PATH", "nonexistent_path.toml");
let config = TestPathEnvAndValidPath::load_config();
-
assert_eq!(config.name, "Path");
- env::remove_var("CONFIG_PATH_ENV");
- cleanup_test_config_files();
+ cleanup_env_and_files("CONFIG_ENV_PATH", "tests/common/path.toml");
}
#[test]
#[should_panic]
-fn invalid_path_env_fails_with_invalid_path() {
- env::set_var("CONFIG_PATH_ENV", "nonexistent_path.toml");
-
+fn test_invalid_env_path_fails_with_invalid_path() {
+ env::set_var("CONFIG_ENV_PATH", "nonexistent_path.toml");
let _config = TestPathEnvAndInvalidPath::load_config();
-
- env::remove_var("CONFIG_PATH_ENV");
-}
-
-#[test]
-fn invalid_path_env_continues_without_path() {
- env::set_var("CONFIG_PATH_ENV", "nonexistent_path.toml");
- env::set_var("NAME", "EnvName");
-
- let config = TestPathEnv::load_config();
-
- assert_eq!(config.name, "EnvName");
-
- env::remove_var("CONFIG_PATH_ENV");
- env::remove_var("NAME");
+ env::remove_var("CONFIG_ENV_PATH");
}
#[test]
-fn default_path_valid() {
- setup_default_config_file();
-
+fn test_default_path_valid() {
+ setup_files("tests/common/default.toml", r#"name = "DefaultPath""#);
let config = TestDefaultPathValid::load_config();
-
- assert_eq!(config.name, "");
-
- std::fs::remove_file("tests/common/default.toml").unwrap();
+ println!("{:?}", TestDefaultPathValid::get_file_path());
+ assert_eq!(config.name, "DefaultPath");
+ cleanup_file("tests/common/default.toml");
}
#[test]
-fn default_path_invalid() {
+fn test_default_path_invalid() {
let config = TestDefaultPathInvalid::load_config();
-
assert_eq!(config.name, "");
}
#[test]
-fn env_path_with_default_path() {
- setup_test_config_files();
- setup_default_config_file();
- env::set_var("CONFIG_PATH_ENV", "nonexistent_path.toml");
+fn test_env_path_with_default_path() {
+ setup_files("tests/common/path.toml", r#"name = "DefaultPath""#);
+ env::set_var("CONFIG_ENV_PATH", "nonexistent_path.toml");
let config = TestPathEnv::load_config();
-
assert_eq!(config.name, "");
- env::remove_var("CONFIG_PATH_ENV");
- cleanup_test_config_files();
- std::fs::remove_file("tests/common/default.toml").unwrap();
+ cleanup_env_and_files("CONFIG_ENV_PATH", "tests/common/path.toml");
+}
+
+fn cleanup_env_and_files(env_var: &str, file_path: &str) {
+ env::remove_var(env_var);
+ cleanup_file(file_path);
}
diff --git a/tests/priority.rs b/tests/priority.rs
index 8ba211f..5ae9b64 100644
--- a/tests/priority.rs
+++ b/tests/priority.rs
@@ -69,7 +69,7 @@ fn cleanup_config_file() {
}
#[test]
-fn env_overrides_config_and_default() {
+fn test_env_overrides_config_and_default() {
setup_env_vars();
create_config_file();
@@ -83,7 +83,7 @@ fn env_overrides_config_and_default() {
}
#[test]
-fn file_overrides_default() {
+fn test_file_overrides_default() {
cleanup_env_vars();
create_config_file();
@@ -96,7 +96,7 @@ fn file_overrides_default() {
}
#[test]
-fn default_without_config() {
+fn test_default_without_config() {
cleanup_env_vars();
cleanup_config_file();
@@ -107,7 +107,7 @@ fn default_without_config() {
}
#[test]
-fn skip_env_with_file() {
+fn test_skip_env_with_file() {
setup_env_vars();
create_config_file();
diff --git a/tests/separator.rs b/tests/separator.rs
index 1927023..17f0191 100644
--- a/tests/separator.rs
+++ b/tests/separator.rs
@@ -11,7 +11,7 @@ pub struct SeparatorTest {
}
#[test]
-fn custom_separator() {
+fn test_custom_separator() {
std::env::set_var("TEST__FIELD_SEP", "field_sep");
std::env::set_var("TEST_sep_STRUCT_SEP", "struct_sep");
diff --git a/tests/simple.rs b/tests/simple.rs
index 25be83f..a38c900 100644
--- a/tests/simple.rs
+++ b/tests/simple.rs
@@ -9,17 +9,17 @@ struct AppConfig {
url: String,
port: u32,
enabled: bool,
- #[config(nest)]
- db_ignored: DbIgnored,
+ #[config(nest, name = "db")]
+ database: DatabaseConfig,
}
#[derive(Config, Default, Debug)]
-struct DbIgnored {
+struct DatabaseConfig {
#[config(skip)]
host: String,
#[config(skip)]
username: String,
- #[config(skip)]
+ #[config(skip, name = "secret")]
password: String,
}
@@ -29,10 +29,10 @@ fn setup_simple_config_file() {
port = 8080
enabled = true
- [db_ignored]
+ [db]
host = "localhost"
username = "admin"
- password = "securepass"
+ secret = "securepass"
"#;
let mut settings_file = File::create("tests/simple_settings.toml").unwrap();
@@ -44,7 +44,7 @@ fn cleanup_simple_config_files() {
}
#[test]
-fn test_app_config_loads_correctly() {
+fn test_simple_config() {
setup_simple_config_file();
let config = AppConfig::load_config();
@@ -54,7 +54,7 @@ fn test_app_config_loads_correctly() {
assert_eq!(config.port, 8080);
assert!(config.enabled);
- assert_eq!(config.db_ignored.password, "securepass");
+ assert_eq!(config.database.password, "securepass");
cleanup_simple_config_files();
}
diff --git a/tests/skipping.rs b/tests/skipping.rs
index 8bf3856..099ada5 100644
--- a/tests/skipping.rs
+++ b/tests/skipping.rs
@@ -9,7 +9,7 @@ pub struct SkipTest {
}
#[test]
-fn skipped() {
+fn test_skipped() {
std::env::set_var("SKIP_IGNORED", "true");
let config = SkipTest::load_config();