Crate tryphon

Crate tryphon 

Source
Expand description

§Tryphon

A type-safe Rust library for loading configuration from environment variables using derive macros.

§Quick Start

use tryphon::{Config, ErrorPrintMode, Secret};

#[derive(Debug, Config)]
struct AppConfig {
    #[env("DATABASE_URL")]
    database_url: String,

    #[env("API_KEY")]
    api_key: Secret<String>,

    #[env("PORT")]
    #[default(8080)]
    port: u16,
}

match AppConfig::load() {
    Ok(config) => {
        println!("Server starting on port {}", config.port);
        println!("Hash of API_KEY: {:?}", config.api_key.hashed());
    }
    Err(e) => {
        eprintln!("{}", e.pretty_print(ErrorPrintMode::Table));
    }
}

§Attributes

§#[env("VAR_NAME")]

Specifies which environment variable to read for this field. You can provide multiple #[env] attributes to create a fallback chain - they will be tried in order.

#[derive(Config)]
struct AppConfig {
    #[env("APP_PORT")]
    #[env("PORT")]  // Fallback if APP_PORT is not set
    port: u16,
}

§#[default(value)]

Provides a default value to use if no environment variable is set.

#[derive(Config)]
struct ServerConfig {
    #[env("HOST")]
    #[default("localhost")]
    host: String,

    #[env("PORT")]
    #[default(8080)]
    port: u16,
}

§#[config]

Marks a field as a nested configuration that should be loaded recursively. The field type must also implement Config.

#[derive(Config)]
struct DatabaseConfig {
    #[env("DB_HOST")]
    host: String,
}

#[derive(Config)]
struct AppConfig {
    #[config]  // Load nested config
    database: DatabaseConfig,
}

§Usage Examples

§Basic Configuration

Use the #[env] attribute to specify which environment variable to read:

use tryphon::Config;

#[derive(Config)]
struct AppConfig {
    #[env("APP_NAME")]
    name: String,

    #[env("MAX_CONNECTIONS")]
    max_connections: u32,
}

§Optional Values

Use Option<T> for values that may not be set:

#[derive(Config)]
struct AppConfig {
    #[env("LOG_LEVEL")]
    log_level: Option<String>,  // None if environment variable not set

    #[env("DEBUG_MODE")]
    debug: Option<bool>,
}

§Secret Masking

Use Secret<T> to prevent sensitive values from appearing in logs:

#[derive(Config)]
struct AppConfig {
    #[env("DB_PASSWORD")]
    password: Secret<String>,

    #[env("API_TOKEN")]
    api_token: Secret<String>,
}

let config = AppConfig::load().unwrap();

§Enum Configurations

Use enums to handle different deployment scenarios. The library will try each variant until one loads successfully:

#[derive(Config)]
enum DatabaseConfig {
    Postgres {
        #[env("POSTGRES_URL")]
        url: String,
    },
    Sqlite {
        #[env("SQLITE_PATH")]
        path: String,
    },
}

§Custom Type Decoders

§Using the Derive Macro

For simple enums with unit variants, use the #[derive(ConfigValueDecoder)] macro:

use tryphon::{ConfigValueDecoder, Config};

#[derive(Debug, ConfigValueDecoder)]
enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
}

#[derive(Config)]
struct AppConfig {
    #[env("LOG_LEVEL")]
    log_level: LogLevel,
}

The derive macro also works for newtype structs (single-field structs):

use tryphon::{ConfigValueDecoder, Config};

#[derive(Debug, ConfigValueDecoder)]
struct Port(u16);

#[derive(Debug, ConfigValueDecoder)]
struct ApiKey(String);

#[derive(Config)]
struct AppConfig {
    #[env("PORT")]
    port: Port,

    #[env("API_KEY")]
    api_key: ApiKey,
}

§Manual Implementation

For more complex types, implement the ConfigValueDecoder trait:

use tryphon::{ConfigValueDecoder, Config};

#[derive(Debug)]
struct Percentage(f64);

impl ConfigValueDecoder for Percentage {
    fn decode(raw: String) -> Result<Self, String> {
        let value: f64 = raw.parse()
            .map_err(|e| format!("Failed to parse percentage: {}", e))?;

        if value < 0.0 || value > 100.0 {
            return Err("Percentage must be between 0 and 100".to_string());
        }

        Ok(Percentage(value))
    }
}

§Supported Types

Tryphon includes built-in decoders for:

  • Primitives: String, bool, char
  • Integers: u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
  • Floats: f32, f64
  • Non-zero integers: NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize
  • Network types: IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6
  • Path types: PathBuf
  • Wrappers: Option<T>, Secret<T> (for any T that implements ConfigValueDecoder)

§Error Handling

Tryphon collects all configuration errors and returns them together, making it easy to see all issues at once. The ConfigError type provides a pretty_print method with two formatting modes:

§List Mode (Compact)

use tryphon::{Config, ErrorPrintMode};

#[derive(Config)]
struct AppConfig {
    #[env("REQUIRED_VAR")]
    required: String,
}

match AppConfig::load() {
    Ok(config) => { /* use config */ }
    Err(e) => {
        // Compact list format, suitable for logs
        eprintln!("{}", e.pretty_print(ErrorPrintMode::List));
        // Output:
        // Found 1 configuration error(s):
        // Missing value for field 'required', tried env vars: REQUIRED_VAR
    }
}

§Table Mode (Structured)

use tryphon::{Config, ErrorPrintMode};

#[derive(Config)]
struct AppConfig {
    #[env("PORT")]
    port: u16,
}

match AppConfig::load() {
    Ok(config) => { /* use config */ }
    Err(e) => {
        // ASCII table format, ideal for terminal output
        eprintln!("{}", e.pretty_print(ErrorPrintMode::Table));
        // Output:
        // ┌────────────┬────────────────────────┬─────────────────────────┐
        // │ Field Name │ Environment Variables  │ Error Details           │
        // ├────────────┼────────────────────────┼─────────────────────────┤
        // │ port       │ PORT                   │ invalid digit found...  │
        // └────────────┴────────────────────────┴─────────────────────────┘
    }
}

You can also access individual errors programmatically:

match AppConfig::load() {
    Ok(config) => { /* use config */ }
    Err(e) => {
        for error in &e.field_errors {
            eprintln!("  - {:?}", error);
        }
    }
}

Error types include:

§Testing with EnvOverrides

When testing configuration loading, you typically need to set environment variables. However, environment variables are global to the process, which makes tests interfere with each other when running in parallel.

Tryphon provides the EnvOverrides type to solve this problem. It uses thread-local storage to override environment variables per-thread:

use tryphon::{Config, EnvOverrides};

#[derive(Config)]
struct TestConfig {
    #[env("DATABASE_URL")]
    database_url: String,
}

#[test]
fn test_config_loading() {
    let mut overrides = EnvOverrides::init()
      .set("DATABASE_URL", "postgres://test-db");

    let config = TestConfig::load().unwrap();
    assert_eq!(config.database_url, "postgres://test-db");

}

You can also use env_vars annotation:

use tryphon::{Config, EnvOverrides};

#[derive(Config, Debug)]
struct TestConfig {
    #[env("FOO")]
    foo: String,

    #[env("BAZ")]
    baz: String,
}

#[test]
#[env_vars(FOO = "bar", BAZ = "qux")]
fn test_implicit_overrides() {
    let config = TestConfig::load().expect("Failed to load test config");

    assert_eq!(config.foo, "bar");
    assert_eq!(config.baz, "qux");
}

With EnvOverrides tests can run concurrently without conflicts. Overrides are removed when the EnvOverrides instance is dropped. Original environment variables are not changed.

See the env_overrides module documentation for more details.

Re-exports§

pub use config::*;
pub use config_error::*;
pub use config_field_error::*;
pub use config_value_decoder::*;
pub use env_overrides::*;
pub use error_print_mode::*;
pub use secret::*;

Modules§

config
config_error
config_field_error
config_value_decoder
decoders
Built-in implementations of ConfigValueDecoder for common types.
env_overrides
Environment variable overrides for testing.
error_print_mode
secret

Functions§

read_env
Reads an environment variable with support for thread-local test overrides.

Attribute Macros§

env_vars
Attribute macro that sets up environment variable overrides for test functions.

Derive Macros§

Config
Derives the Config trait for a struct or enum to enable loading configuration from environment variables.
ConfigValueDecoder
Derives the ConfigValueDecoder trait for simple enums without fields.