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 β annotate inner structs with
#[forgeconf]and mark the field with#[field(nested)]to map configuration sub-sections without boilerplate. - π§· Format agnostic β enable just the parsers you need through Cargo features (
toml,yaml,json).
Add Forgeconf to your workspace:
[dependencies]
forgeconf = "0.3"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.3"
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, CliArguments, 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()
.add_source(CliArguments::new().with_priority(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) |
nested |
flag | Treat the field as a nested #[forgeconf] struct, resolved from a sub-section of the same name |
All lookups resolve in the following order:
- Field-level CLI override (
#[field(cli = "...")]) - Field-level env override (
#[field(env = "...")]) - Sources registered on the loader via
add_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:
add_source(source)β supply any customConfigSource(includingCliArguments).load()β merges all sources (including anyconfig(...)entries declared on the struct) and deserializes into the struct.
Config files declared with #[forgeconf(config(path = "..."))] are loaded automatically when you call loader() β no extra call needed. Use add_source to layer additional files or CLI arguments on top:
let cfg = AppConfig::loader()
.add_source(forgeconf::ConfigFile::new("settings.override.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.