Forgeconf is a small attribute macro and runtime for loading configuration files into strongly typed Rust structs. It is built for services that need predictable merge semantics, compile-time validation, and the ability to override values from the command line or the environment without sprinkling glue code throughout the application.
- π§± Single source of truth β annotate your struct once and Forgeconf generates the loader, builder, and conversion logic.
- π§ͺ Compile-time safety β missing values or type mismatches become compile errors inside the generated code, so you fail fast during development.
- π Composable sources β merge any combination of files, CLI flags, and environment variables with explicit priorities.
- π§© Nested structures β nested structs can be annotated with
#[forgeconf]as well, enabling deeply nested configuration trees without boilerplate. - π§· Format agnostic β enable just the parsers you need through Cargo features (
toml,yaml,json).
Add Forgeconf to your workspace:
[dependencies]
forgeconf = "0.1"The crate enables TOML, YAML, and regex-powered validators by default. Add json if you want JSON support, or disable defaults to pick a subset:
[dependencies.forgeconf]
version = "0.1"
default-features = false
features = ["json", "regex"]Disable regex if you want to skip the regex crate entirely, or re-enable it explicitly (as shown above) when using validators::matches_regex.
use forgeconf::{forgeconf, ConfigError};
#[forgeconf(config(path = "config/app.toml"))]
struct AppConfig {
#[field(default = 8080)]
port: u16,
#[field(env = "APP_DATABASE_URL")]
database_url: String,
}
fn main() -> Result<(), ConfigError> {
let cfg = AppConfig::loader()
.with_config() // load every `config(...)` entry
.with_cli(200) // merge `--key=value` CLI arguments
.load()?;
println!("listening on {}", cfg.port);
println!("db url: {}", cfg.database_url);
Ok(())
}#[forgeconf(...)] accepts zero or more config(...) entries. Each entry takes:
| key | type | description |
|---|---|---|
path |
string (req.) | Relative or absolute path to the file |
format |
"toml" / ... |
Overrides format detection |
priority |
u8 |
Higher numbers win when merging (default 10) |
Use #[field(...)] on struct fields to fine tune the behaviour:
| option | type | effect |
|---|---|---|
name |
string | Rename the lookup key |
insensitive |
bool | Perform case-insensitive lookups |
env |
string | Pull from an environment variable first |
cli |
string | Check --<cli>=value CLI flags before files |
default |
expression | Fall back to the provided literal/expression |
optional |
bool | Treat Option<T> fields as optional |
validate |
expression | Invoke a validator after parsing (repeatable) |
All lookups resolve in the following order:
- Field-level CLI override (
#[field(cli = "...")]) - Field-level env override (
#[field(env = "...")]) - Sources registered on the loader (
with_cli,with_config, oradd_source)
Validators are plain expressions that evaluate to something callable with (&T, &str) and returning Result<(), ConfigError>. You can reference free functions, closures, or the helpers under forgeconf::validators:
fn ensure_https(value: &String, key: &str) -> Result<(), ConfigError> {
if value.starts_with("https://") {
Ok(())
} else {
Err(ConfigError::mismatch(key, "https url", value.clone()))
}
}
#[forgeconf]
struct SecureConfig {
#[field(validate = forgeconf::validators::range(1024, 65535))]
port: u16,
#[field(
validate = ensure_https,
validate = forgeconf::validators::len_range(12, 128),
validate = forgeconf::validators::matches_regex(regex::Regex::new("^https://").unwrap()),
)]
endpoint: String,
}The most common helpers:
non_empty(),min_len(n),max_len(n), andlen_range(min, max)β work with any type implementingvalidators::HasLen(Strings, Vecs, maps, sets, β¦).range(min, max)β enforce numeric/string bounds viaPartialOrd.one_of([..])β restrict values to a predefined set.matches_regex(regex::Regex)β ensure the value matches a regular expression (enable theregexCargo feature and add theregexcrate to yourCargo.tomlwhen using this helper).
Each helper returns a closure that you can combine or wrap to build higher-level policies.
The generated <Struct>Loader exposes:
with_config()β loads everyconfig(...)entry from the attribute.with_cli(priority)β merges parsed CLI arguments at the provided priority.add_source(source)β supply any customConfigSource.load()β merges the queued sources and deserializes the struct.
You can construct sources manually using items re-exported from the crate:
let cfg = AppConfig::loader()
.add_source(forgeconf::ConfigFile::new("settings.toml"))
.add_source(forgeconf::CliArguments::new().with_args(["--port=9090"]))
.load()?;| Feature | Dependency | File extensions |
|---|---|---|
toml |
toml crate |
.toml |
yaml |
yaml-rust2 |
.yml, .yaml |
json |
jzon |
.json |
Each parser lives behind a feature flag. Disable defaults if you want to ship with no parsers enabled.
The repository ships with scripts/release.sh to automate version bumps, changelog generation, tagging, and pushes. Requirements:
cargo set-version(cargo install cargo-edit)git-cliff- Rust nightly toolchain (for formatting) plus the regular stable toolchain
To publish a new release (for example 0.2.1):
./scripts/release.sh 0.2.1The script ensures the working tree is clean, bumps every crate in the workspace, regenerates CHANGELOG.md through git-cliff, runs formatting and tests, commits the results, tags the release (v0.2.1), and pushes both the branch and tag. Once the tag hits GitHub, the release workflow publishes the crates and attaches the same changelog to the GitHub Release entry.
Forgeconf is released under the MIT License.