Skip to main content

Crate congen

Crate congen 

Source
Expand description

§congen

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 CongenChange type

Most projects use the derive macros from congen-derive, so you usually do not need to write these trait impls manually.

§Feature overview

  • 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:
    • set
    • unset (for unsettable fields like Option<_> or bool)
    • 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, and OsString
  • clap bridge (CongenClap<T>) that generates subcommands from your configuration description.
  • ValueEnum integration via #[derive(ValueEnumConfiguration)] for enum fields used as config values.

clap support is feature-gated behind the clap crate feature (enabled by default).

§Derive macros

congen re-exports derive macros from congen-derive:

  • Configuration for structs
  • ValueEnumConfiguration for clap::ValueEnum enums (requires the clap feature)

Example:

use congen::Configuration;

#[derive(Configuration, Debug)]
struct Config {
    port: u16,
    #[congen(default)]
    log_file: Option<String>,
}

§Default values

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)] use CongenInternal::default for the field
  • #[congen(rust_default)] use Default::default for 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.

§CLI integration with CongenClap

You can parse configuration changes with clap and apply them to an existing config instance.

§Typical application flow

The most common pattern is:

  1. Read the current config from disk.
  2. If the CLI command is config, parse and apply a CongenChange, then write the updated config back to disk.
  3. 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 5
  • config token unset
  • config token use-default

Nested fields and collections are represented through nested subcommands (see workspace examples).

§Environment variable integration

Besides clap-based parsing, you can build a CongenChange from environment variables using congen::env:

  • env::load_from_env::<Config>("CONFIG") reads from std::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 accepts USE-DEFAULT and USEDEFAULT; value is ignored)

§Workspace examples

This repository contains complete examples:

  • examples/basic - nested structs, Option, defaults, and basic set/unset behavior.
  • examples/lists - Vec/HashMap operations, nested collection updates, key handling, and inner_default.
  • examples/primitives - primitive parsing and updates across supported base types.

§Changelog

A changelog exists at CHANGELOG.md

Re-exports§

pub use clap::CongenClap;
pub use env::load_from_env;

Modules§

clap
Load a CongenChange using clap.
env
Read CongenChange from the environment
internal
Contains Traits, Types and utilities used by the derive macros

Traits§

Configuration
A data structure that allows applying partial changes.

Derive Macros§

Configuration
Derive Configuration for a named struct.
ValueEnumConfiguration