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 anyTthat implementsConfigValueDecoder)
§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:
ConfigFieldError::MissingValue- Required environment variable not setConfigFieldError::ParsingError- Failed to parse value into target typeConfigFieldError::Nested- Error in nested configurationConfigFieldError::Other- Custom error messages
§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
ConfigValueDecoderfor 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
Configtrait for a struct or enum to enable loading configuration from environment variables. - Config
Value Decoder - Derives the
ConfigValueDecodertrait for simple enums without fields.