configulator-rs 0.1.3

A configuration loader that populates a struct from config files, environment variables, and CLI flags
Documentation

Configulator

codecov License GitHub contributors

A simple configuration manager for Rust applications with derive macro support.

Features

  • Supports configuration from multiple sources with clear precedence:
    1. Default values (lowest)
    2. Config files (any serde format via serde_loader)
    3. Environment variables
    4. CLI flags (highest)
  • #[derive(Config)] macro for declarative configuration structs
  • Any serde-compatible file format - YAML, TOML, JSON, with a one-liner
  • Pluggable file format support - bring your own parser via [FileLoader]
  • Nested struct support
  • Vec<T> list fields
  • Custom types - anything implementing FromStr + [Default]
  • Optional validation via the [Validate] trait
  • Boolean CLI flags (--debug sets true, --debug false sets false)

Supported Types

  • All primitive scalars (i8i64, u8u64, f32, f64, bool, String)
  • PathBuf and any other FromStr + Default type
  • Custom enums (with FromStr implementation)
  • Vec<T> for list values
  • Nested structs (must also derive Config)

Usage

Add to your Cargo.toml:

[dependencies]
configulator-rs = "0.1"

[!NOTE] Because the configuration options are expressed as different cases (i.e. http.host in a config file would be HTTP__HOST in environment variables), this library cannot be used for configurations that contain the same field name in different cases.

Derive Attributes

This library uses the #[configulator(...)] attribute with the following keys:

  • name - The config key name (defaults to the field name)
  • default - Default value as a string literal
  • description - Help text shown in CLI --help output
// Field appears in config files, env vars, and CLI flags as "my-name".
#[configulator(name = "my-name")]
my_name: String,

// Field has a description shown in CLI --help
#[configulator(name = "my-name", description = "this text appears in --help")]
my_name: String,

// Field has a default value of 1
#[configulator(name = "my-name", default = "1")]
my_name: u32,

Example

use configulator::{
    CLIFlagOptions, Config, Configulator,
    EnvironmentVariableOptions, FileOptions, Validate,
    serde_loader,
};

#[derive(Config, Default, Debug)]
struct AppConfig {
    #[configulator(name = "host", default = "127.0.0.1", description = "Bind address")]
    host: String,

    #[configulator(name = "port", default = "8080", description = "Listen port")]
    port: u16,

    #[configulator(name = "debug", default = "false", description = "Enable debug mode")]
    debug: bool,

    #[configulator(name = "allowed-origins", default = "localhost,example.com")]
    allowed_origins: Vec<String>,

    #[configulator(name = "database")]
    database: DatabaseConfig,
}

#[derive(Config, Default, Debug)]
struct DatabaseConfig {
    #[configulator(name = "url", default = "postgres://localhost/mydb")]
    url: String,

    #[configulator(name = "max-connections", default = "10")]
    max_connections: u32,
}

impl Validate for AppConfig {
    fn validate(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        if self.port == 0 {
            return Err("port must be non-zero".into());
        }
        Ok(())
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = Configulator::<AppConfig>::new()
        .with_file(FileOptions {
            paths: vec!["config.yaml".into(), "/etc/myapp/config.yaml".into()],
            error_if_not_found: false,
            // Any serde-compatible format works: serde_json, toml, etc.
            loader: serde_loader(|s| serde_yaml_ng::from_str(s)),
        })
        .with_environment_variables(EnvironmentVariableOptions {
            prefix: "MYAPP".into(),
            separator: "__".into(),
        })
        .with_cli_flags(CLIFlagOptions {
            separator: ".".into(),
        })
        .load()?;

    println!("Host: {}", config.host);
    println!("Database URL: {}", config.database.url);
    Ok(())
}

Configuration Sources

Config Files

Configulator is format-agnostic, pass any serde-compatible deserializer via serde_loader, or implement the FileLoader trait for full control. YAML, TOML, JSON, and any other serde format work out of the box.

Provide a list of paths to search. The first file found is used.

// YAML
.with_file(FileOptions {
    paths: vec!["config.yaml".into()],
    error_if_not_found: false,
    loader: serde_loader(|s| serde_yaml_ng::from_str(s)),
})

// TOML
.with_file(FileOptions {
    paths: vec!["config.toml".into()],
    error_if_not_found: false,
    loader: serde_loader(|s| toml::from_str(s)),
})

// JSON
.with_file(FileOptions {
    paths: vec!["config.json".into()],
    error_if_not_found: false,
    loader: serde_loader(|s| serde_json::from_str(s)),
})

The CLI also accepts --config / -c to specify a config file path at runtime (requires calling .with_file() first).

Environment Variables

Environment variables are formed as PREFIX + SEPARATOR + FIELD_NAME (uppercased, dashes become underscores).

.with_environment_variables(EnvironmentVariableOptions {
    prefix: "MYAPP".into(),
    separator: "__".into(),
})

For example, a field named max-connections under a database parent with prefix MYAPP and separator __ would be MYAPP__DATABASE__MAX_CONNECTIONS.

CLI Flags

Nested fields use the separator to form flag names (e.g. --database.host).

.with_cli_flags(CLIFlagOptions {
    separator: ".".into(),
})

Boolean fields work as flags (--debug sets to true, --debug false sets to false). List fields can be repeated (--ports 80 --ports 443).

You can also provide a custom clap::Command to set the app name, version, or add your own flags:

.with_cli_command(clap::Command::new("myapp").version("1.0"))
.with_cli_flags(CLIFlagOptions {
    separator: ".".into(),
})

Validation

Implement the Validate trait and call .load() to validate after loading. Use .load_without_validation() to skip validation.

Feature Flags

Configulator uses feature flags to keep dependencies minimal. All features are enabled by default.

Feature Description Dependencies
file Config file loading (FileOptions, serde_loader, --config flag) serde
cli CLI flag parsing via clap clap
env Environment variable loading -

To opt out of features you don't need:

[dependencies]
configulator-rs = { version = "0.1", default-features = false, features = ["env"] }