Skip to content

Commit

Permalink
v0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
N4D1K-lgtm committed May 1, 2024
1 parent 3a4c8a4 commit 04ed07f
Show file tree
Hide file tree
Showing 20 changed files with 429 additions and 262 deletions.
40 changes: 13 additions & 27 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
---
name: Rust

on: [push, pull_request]
on:
push:
branches:
- master
pull_request:
branches:
- master
- dev

jobs:
format:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -85,15 +71,15 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --verbose
args: --verbose -- --test-threads=1

bench:
name: Benchmark
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
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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]
Expand All @@ -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"
Expand Down
171 changes: 141 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,72 +46,183 @@
<small>Built with ❤️ by Kidan Nelson</small>
</div>

## 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()`](<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`](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<String, String> = 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()`](<core::Confgr::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.
Loading

0 comments on commit 04ed07f

Please sign in to comment.