congen helps you build configuration systems that support partial updates from structured changes and CLI input.
It is designed around two ideas:
- your config type implements
Configuration - updates are represented by a companion
CongenChangetype
Most projects use the derive macros from congen-derive, so you usually do not need to write these trait impls manually.
- Derive support for config structs via
#[derive(Configuration)]. - Automatic generation of a strongly typed change object (
<Config>Change) for each config type. - Field-level update verbs:
setunset(for unsettable fields likeOption<_>orbool)use-default(when defaults are configured)
- Nested updates by path (
sub.field,outer.inner.value, etc.). - Optional field semantics with
Option<T>support. - Collection updates for:
Vec<T>(append,update,remove,empty)HashMap<K, T>(append,update,remove,empty)
- Typed list/map keys with support for string, signed, and unsigned map keys.
- Built-in primitive support, including:
bool- integer and float numeric primitives
String,CString, andOsString
clapbridge (CongenClap<T>) that generates subcommands from your configuration description.ValueEnumintegration via#[derive(ValueEnumConfiguration)]for enum fields used as config values.
clap support is feature-gated behind the clap crate feature (enabled by default).
congen re-exports derive macros from congen-derive:
Configurationfor structsValueEnumConfigurationforclap::ValueEnumenums (requires theclapfeature)
Example:
use congen::Configuration;
#[derive(Configuration, Debug)]
struct Config {
port: u16,
#[congen(default)]
log_file: Option<String>,
}This crate does not use rusts [Default] trait to provide default values. This is done, because a default value for a type might not be necessarialy translate into a good default value in a configuration, e.g. the default for all number types is zero, which is not a good default value for the number of threads some process is allowed to use.
Instead [CongenInternal::default] is used instead. The exact default value can be configured using
the #[congen(default)] attribute.
#[congen(default)]useCongenInternal::defaultfor the field#[congen(rust_default)]useDefault::defaultfor the field#[congen(default = <expr>)](custom expression)
In addition #[congen(inner_default = <expr>)] can be used to control the default value of inner types
in Vec, HashMap and Option.
You can parse configuration changes with clap and apply them to an existing config instance.
The most common pattern is:
- Read the current config from disk.
- If the CLI command is
config, parse and apply aCongenChange, then write the updated config back to disk. - Otherwise, run your normal program logic with the config that was read from disk.
congen is focused on step 2: creating and applying strongly typed config changes.
use clap::{Parser, Subcommand};
use congen::{Configuration, CongenClap, load_from_env};
#[derive(Configuration, Debug)]
struct Config {
retries: u32,
#[congen(default)]
token: Option<String>,
}
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Config(CongenClap<Config>),
Run,
}
fn main() {
let cli = Cli::parse_from(["program_name", "config", "token", "unset"]);
let mut config = read_config_from_file("config.toml");
match cli.command {
Commands::Config(change_args) => {
config.apply_change(change_args.into_change());
write_config_to_file("config.toml", &config);
}
Commands::Run => {
let config_change = load_from_env::<Config>("CONFIG").unwrap();
config.apply_change(config_change);
run_program(config);
}
}
}
fn read_config_from_file(_path: &str) -> Config {
Config {
retries: 3,
token: Some("abc".to_string()),
}
}
fn write_config_to_file(_path: &str, _config: &Config) {
// serialize and persist config
}
fn run_program(_config: Config) {
// your actual application logic
}Typical generated commands look like:
config retries set 5config token unsetconfig token use-default
Nested fields and collections are represented through nested subcommands (see workspace examples).
Besides clap-based parsing, you can build a CongenChange from environment variables using congen::env:
env::load_from_env::<Config>("CONFIG")reads fromstd::env::vars_os().
Environment variable names use this shape:
<PREFIX>_<PATH>_<VERB>
Where:
<PATH>is the config field path in screaming snake case (sub.e->SUB_E,myValue->MY_VALUE).<VERB>is one of:SET(value is parsed and applied)UNSET(value is ignored)USE_DEFAULT(also acceptsUSE-DEFAULTandUSEDEFAULT; value is ignored)
This repository contains complete examples:
examples/basic- nested structs,Option, defaults, and basic set/unset behavior.examples/lists-Vec/HashMapoperations, nested collection updates, key handling, andinner_default.examples/primitives- primitive parsing and updates across supported base types.
A changelog exists at CHANGELOG.md