Skip to main content

Crate configulator

Crate configulator 

Source
Expand description

§Configulator

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

Supports configuration from multiple sources with clear precedence:

  1. Default values (lowest priority)
  2. Config files (any serde format via serde_loader)
  3. Environment variables
  4. CLI flags (highest priority)

§Features

  • #[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)

§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.

§Quick Start

use configulator::{Config, Configulator, Validate};

#[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,
}

fn main() {
    let config: AppConfig = Configulator::new()
        .load_without_validation()
        .expect("failed to load config");
    println!("{config:?}");
}

§Configuration Sources

Enable as many or as few sources as you need via the builder:

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)),
        })
        // Env vars: MYAPP__HOST, MYAPP__DATABASE__MAX_CONNECTIONS, etc.
        .with_environment_variables(EnvironmentVariableOptions {
            prefix: "MYAPP".into(),
            separator: "__".into(),
        })
        // CLI flags: --host, --database.url, --debug, etc.
        .with_cli_flags(CLIFlagOptions {
            separator: ".".into(),
        })
        // .load() validates; use .load_without_validation() to skip
        .load()?;

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

§Derive Attributes

Fields are annotated with #[configulator(...)] using these keys:

KeyDescription
nameConfig key name (defaults to the field name)
defaultDefault value as a string literal
descriptionHelp 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,

§Supported Types

  • All primitive scalars (i8i64, u8u64, f32, f64, bool, String)
  • PathBuf and any other FromStr + Default type
  • Custom enums (implement FromStr + Default)
  • Vec<T> for list values (comma-separated defaults, repeated CLI flags)
  • Nested structs (must also derive Config)

§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.

FeatureDescriptionDependencies
fileConfig file loading (FileOptions, serde_loader, --config flag)serde
cliCLI flag parsing via clapclap
envEnvironment variable loading-

To opt out of features you don’t need:

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

Structs§

CLIFlagOptionscli
Options for loading configuration from CLI flags.
Configulator
Builder for loading configuration from multiple sources into a typed struct.
EnvironmentVariableOptionsenv
Options for loading configuration from environment variables.
FileOptionsfile
Options for loading configuration from a file.

Enums§

ConfigulatorError
Errors that can occur during configuration loading.

Traits§

FileLoaderfile
Trait for parsing file contents into configuration values.
Validate
Trait for user-defined config validation.

Functions§

serde_loaderfile
Create a FileLoader from any serde-compatible deserializer.

Derive Macros§

Config
Derive macro that generates ConfigFields and FromValueMap implementations for a struct.