Skip to content

Wasabi375/ConGen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

congen

Crates.io docs.rs Dependency status

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

About

Derive config generators in rust

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors