# Forgeconf
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.
## Table of Contents
- [Forgeconf](#forgeconf)
- [Highlights](#highlights)
- [Install](#install)
- [Quick start](#quick-start)
- [Attribute reference](#attribute-reference)
- [Field modifiers](#field-modifiers)
- [Validators](#validators)
- [Loader API](#loader-api)
- [Format support](#format-support)
- [Releasing](#releasing)
- [License](#license)
## Highlights
- ๐งฑ **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`).
## Install
Add Forgeconf to your workspace:
```toml
[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:
```toml
[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`.
## Quick start
```rust,no_run
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()
.with_config() // load every `config(...)` entry
.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(())
}
```
## Attribute reference
`#[forgeconf(...)]` accepts zero or more `config(...)` entries. Each entry takes:
| `path` | string (req.) | Relative or absolute path to the file |
| `format` | `"toml" / ...` | Overrides format detection |
| `priority` | `u8` | Higher numbers win when merging (default 10) |
### Field modifiers
Use `#[field(...)]` on struct fields to fine tune the behaviour:
| `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:
1. Field-level CLI override (`#[field(cli = "...")]`)
2. Field-level env override (`#[field(env = "...")]`)
3. Sources registered on the loader (`with_config` or `add_source`)
#### Validators
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`:
```rust,no_run
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)`, and `len_range(min, max)` โ work with any type implementing `validators::HasLen` (Strings, Vecs, maps, sets, โฆ).
- `range(min, max)` โ enforce numeric/string bounds via `PartialOrd`.
- `one_of([..])` โ restrict values to a predefined set.
- `matches_regex(regex::Regex)` โ ensure the value matches a regular expression (enable the `regex` Cargo feature and add the [`regex`](https://crates.io/crates/regex) crate to your `Cargo.toml` when using this helper).
Each helper returns a closure that you can combine or wrap to build higher-level policies.
### Loader API
The generated `<Struct>Loader` exposes:
- `with_config()` โ loads every `config(...)` entry from the attribute.
- `add_source(source)` โ supply any custom `ConfigSource` (including `CliArguments`).
- `load()` โ merges the queued sources and deserializes the struct.
You can construct sources manually using items re-exported from the crate:
```rust
let cfg = AppConfig::loader()
.add_source(forgeconf::ConfigFile::new("settings.toml"))
.add_source(forgeconf::CliArguments::new().with_args(["--port=9090"]))
.load()?;
```
## Format support
| `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.
## Releasing
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`):
```bash
./scripts/release.sh 0.2.1
```
The 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.
## License
Forgeconf is released under the [MIT License](./LICENSE).