cnfg

Crates.iocnfg
lib.rscnfg
version0.1.2
created_at2025-09-19 15:22:51.768425+00
updated_at2025-09-21 11:49:26.606891+00
descriptionUnified, validated configuration for Rust
homepagehttps://github.com/tommantonclery/cnfg
repositoryhttps://github.com/tommantonclery/cnfg
max_upload_size
id1846605
size48,964
Thomas Clery (tommantonclery)

documentation

https://docs.rs/cnfg

README

cnfg

cnfg lets you describe your application's configuration once and load it from files, environment variables, and CLI flags with compile-time guarantees. Derive the Cnfg macro on a normal serde struct and the crate handles source precedence, deserialization, validation, and help text for you.

Highlights

  • Define your schema with plain Rust structs and #[derive(Cnfg)]
  • Merge defaults, config files, environment variables, and CLI flags in a predictable order
  • Generate --help output automatically from doc comments and annotations
  • Validate inputs with built-in range/regex/url checks or custom logic
  • Compose nested configs without boilerplate and surface rich error messages

Install

Add the library crate to your Cargo.toml (the derive macro is re-exported, no extra dependency required):

[dependencies]
cnfg = { version = "0.1.1", features = ["yaml", "toml"] }

You can also use cargo add:

cargo add cnfg --features yaml,toml

Feature flags are optional:

Feature Default? Purpose
yaml Enable loading config.yaml / config.yml files
toml Enable loading config.toml files
json Enable loading config.json files

Disable features if you want a smaller dependency tree:

cnfg = { version = "0.1.1", default-features = false, features = ["toml"] }

Define and Load Configuration

Create a normal serde struct, derive Cnfg, and annotate each field with the sources you care about.

use cnfg::{Cnfg, CnfgError};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Cnfg)]
struct AppConfig {
    /// Name used for logging and help output
    #[cnfg(default = "demo-app", cli)]
    name: String,

    /// Toggle verbose logging (`--debug` or `DEBUG=true`)
    #[cnfg(default = false, cli, env = "APP_DEBUG")]
    debug: bool,

    /// Database connection string (required when not using defaults)
    #[cnfg(env = "DATABASE_URL", required)]
    database_url: String,
}

fn main() -> Result<(), CnfgError> {
    let cfg = AppConfig::load()?;
    println!("Loaded config: {cfg:#?}");
    Ok(())
}

The generated loader combines all sources in the following order (later sources win):

  1. Struct defaults & #[cnfg(default = ...)]
  2. Parsed config file (CONFIG_FILE env override or config.{toml,yaml,json})
  3. Environment variables declared with #[cnfg(env = "NAME")]
  4. Command line flags declared with #[cnfg(cli)]

Missing required values surface as CnfgError::Validation with field-qualified error messages.

CLI Help for Free

Doc comments flow into the generated CLI help. Call AppConfig::help() or run your binary with --help to see output like:

Usage:
  <binary> [OPTIONS]

Options:
  --name <value>            Name used for logging and help output [default: demo-app]
  --debug                   Toggle verbose logging (--debug or DEBUG=true)

When users pass --help, cnfg prints the help text and returns CnfgError::HelpPrinted so you can exit gracefully.

Nested Configurations

Break large configs into focused structs. Mark nested fields with #[cnfg(nested)] and use #[serde(default)] when the child implements Default.

#[derive(Debug, Serialize, Deserialize, Cnfg)]
struct Database {
    #[cnfg(default = "localhost", env = "DB_HOST")]
    host: String,

    #[cnfg(default = 5432, cli, validate(range(min = "1024", max = "65535")))]
    port: u16,
}

#[derive(Debug, Serialize, Deserialize, Cnfg)]
struct AppConfig {
    #[serde(default)]
    #[cnfg(nested)]
    database: Database,
}

Nested required fields are tracked automatically (e.g. database.host) and validation errors are surfaced with fully-qualified paths.

Validation Helpers

The derive macro ships with common validators:

#[derive(Debug, Serialize, Deserialize, Cnfg)]
struct Limits {
    #[cnfg(required, validate(range(min = "1", max = "99")))]
    workers: usize,

    #[cnfg(validate(regex = "^[a-z0-9_-]+$"))]
    cluster: String,

    #[cnfg(validate(url))]
    callback: String,
}

For custom checks, implement Validate manually or extend with #[cnfg(validate(custom_fn = "path::to::fn"))] (coming soon).

Environment and Testing Tips

  • Use dotenvy support by dropping a .env file next to your binary—cnfg will load it automatically.
  • In tests, guard environment mutations with a mutex to avoid cross-test interference (see crates/cnfg/tests/nested.rs).
  • Call AppConfig::defaults_json() in unit tests to assert default shapes without touching real files.

Examples

A runnable example lives in examples/tester. It showcases nested configs, CLI help, and validation.

Minimum Supported Rust Version (MSRV)

The crate targets Rust 1.70.0 or newer (for OnceLock). CI and docs assume the 2021 edition.

Status

The library is production-ready for building internal tools and services. Feedback is welcome—open an issue or discussion on GitHub.

License

Licensed under the Apache License, Version 2.0.

Commit count: 6

cargo fmt