Trail Config
Simple Rust library to help with reading (and formatting) values from config files. Supports YAML format (uses serde_yaml_bw library).
Features
- 📖 Simple path-based config value access
- 🔧 Customizable path separators (
/,::, etc.) - 🌍 Environment-specific config files
- 📝 String formatting and interpolation
- ✅ Comprehensive error handling with custom
ConfigErrortype - 📋 Type conversion for strings, numbers, booleans, and sequences
- 🔐 Escape sequence support for keys containing separators
- 🔄 Hot reload support for detecting configuration changes at runtime
Quick Start
use Config;
// Load config.yaml file
let config = default;
// Get values with lenient API (returns empty/None on missing)
let port = config.str; // -> "8080"
let timeout = config.get_int; // -> Some(30)
// Or use strict API for explicit error handling
match config.str_strict
Loading Configuration
Trail Config provides different loading strategies for different use cases:
Production Code (Strict)
Use Config::load_required() when the configuration file must exist:
use Config;
let config = load_required?;
// Will error if file is missing, invalid YAML, or permission denied
Testing/Optional Configs (Lenient)
Use Config::default() when missing config is acceptable:
let config = default; // Never panics, gracefully handles missing config.yaml
Custom Loading
// With custom separator
let config = new?;
// With environment substitution
let config = new?; // Loads config.dev.yaml
// From YAML string
let config = load_yaml?;
API Overview
Trail Config organizes methods into two API styles:
| Goal | Method Style | Returns |
|---|---|---|
| Lenient access (handles missing gracefully) | get(), str(), list(), etc. |
Option<T> or empty defaults |
| Strict access (explicit error handling) | get_strict(), str_strict(), etc. |
Result<T, ConfigError> |
Both styles share the same path syntax and navigate nested YAML using separators (default: /).
Main Methods (Lenient API)
Lenient methods return None or empty values for missing paths or type mismatches.
Reading Values
get(path)→Option<Value>- Get rawserde_yaml::Valuestr(path)→String- Get string representation (empty if missing)list(path)→Vec<String>- Get sequence as vector (empty if missing)contains(path)→bool- Check if path exists
Type Conversion
get_int(path)→Option<i64>- Get integer valueget_float(path)→Option<f64>- Get floating-point valueget_bool(path)→Option<bool>- Get boolean value
Formatting
fmt(format, path)→String- Format multiple values (empty on any error)
Configuration Metadata
get_filename()→&str- Get loaded config filenameenvironment()→Option<&str>- Get environment name (if used)
Hot Reload
reload()→Result<(), ConfigError>- Reload from current filereload_from(filename)→Result<(), ConfigError>- Load from different file
Strict Methods (Error Handling API)
Strict methods return Result<T, ConfigError> for explicit error handling.
get_strict(path)- Get value, fails withPathNotFoundif missingstr_strict(path)- Get string, fails withPathNotFoundif missinglist_strict(path)- Get sequence, fails withPathNotFoundif missingfmt_strict(format, path)- Format values, fails withPathNotFoundorFormatErrorget_int_strict(path)- Get integer, fails withPathNotFoundorFormatErroron type mismatchget_float_strict(path)- Get float, fails withPathNotFoundorFormatErroron type mismatchget_bool_strict(path)- Get boolean, fails withPathNotFoundorFormatErroron type mismatch
Type Conversion
Convert config values to typed Rust values safely:
let config = default;
// Lenient - returns None on missing or type mismatch
let port = config.get_int;
let timeout = config.get_float;
let debug = config.get_bool;
if let Some = port
// Strict - returns error details
match config.get_int_strict
Example config (YAML):
app:
port: 8080
timeout: 30.5
debug: true
String Formatting
Combine multiple config values into a formatted string without separate calls using the fmt() method:
Basic Formatting Example
Instead of loading each value separately:
let host = config.str;
let port = config.str;
let connection = format!; // Separate calls
Use fmt() to combine them in one call:
let connection = config.fmt;
The + in the path tells fmt() to combine multiple attributes at the same level.
How It Works
The fmt() method takes:
- format - A format string with
{}placeholders (one per value) - path - A path ending with attributes joined by
+(e.g.,db/redis/server+port)
It navigates to the parent (database), then extracts and formats the specified attributes (host and port).
Lenient vs Strict Formatting
Both APIs are available:
let config = default;
// Lenient - returns empty string if any value is missing
let connection = config.fmt;
// Strict - returns error if any value is missing
match config.fmt_strict
Multi-Value Formatting
Format more than two values with additional + separators:
// YAML structure
// databasse:
// host: localhost
// port: 5432
// name: myapp_db
// username: admin
// Format all four values
let db_url = config.fmt;
// Result: "postgresql://admin@localhost:5432/myapp_db"
Error Handling
Trail Config uses a custom ConfigError enum for precise error handling:
Error Types
use ConfigError;
// Four error variants:
// - IoError(io::Error) - File I/O errors (missing file, permission denied, etc.)
// - YamlError(String) - YAML parsing errors
// - PathNotFound(String) - Configuration path not found in document
// - FormatError(String) - String formatting or configuration errors
Basic Error Handling
use ;
match load_required
Strict Method Error Handling
use ;
let config = default;
match config.str_strict
// Type conversion with error details
match config.get_int_strict
Hot Reload
Detect and apply configuration changes at runtime without restarting:
let mut config = load_required?;
// Reload from the same file
config.reload?; // Updates content from disk
// Or switch to a different config file
config.reload_from?;
Note: If a reload fails (e.g. the file is temporarily invalid or missing), the existing configuration is preserved unchanged. The error is returned, but the config remains valid and usable.
Server Loop Example
use Config;
use thread;
use Duration;
Escape Sequences
Keys containing the path separator can be accessed using escape sequences.
Syntax
\/- Include literal separator in the key\\- Include literal backslash in the key- Works with any separator:
/,::,->, etc.
Example
Given this YAML with special characters in keys:
database:
"host/port": localhost:5432 # Key contains /
"user\name": admin\user # Key contains \
Access using escape sequences:
let config = load_yaml.unwrap;
// Access key containing separator (/)
let value = config.str; // -> "localhost:5432"
// Access key containing backslash (\)
let value = config.str; // -> "admin\user"
With custom separator:
let config = load_yaml.unwrap;
// Path: a::b\::c::d navigates to keys ["a", "b::c", "d"]
let value = config.str;
Input Validation
Trail Config validates inputs automatically and returns FormatError for invalid configurations:
| Input | Constraint | Error |
|---|---|---|
| Path Separator | Cannot be empty | Returns FormatError |
File Paths (load_required) |
Empty filename explicitly rejected | Returns IoError |
File Paths (new) |
Empty filename passed to OS | Returns IoError |
| Paths | Empty paths safely handled | Returns None or empty |
| Separators (leading/trailing) | Handled gracefully | No error |
| Filename Templates | Must be valid format strings | Returns FormatError |
Examples:
// Empty separator - error
let result = new;
assert!; // FormatError
// load_required rejects empty filename upfront
let result = load_required;
assert!; // IoError (InvalidInput)
// Missing file with load_required - error
let result = load_required;
assert!; // IoError
// Missing file with default - ok, empty config
let config = default;
assert!; // Graceful fallback
Real-World Examples
Web Server Configuration
use Config;
let config = load_required?;
let host = config.str;
let port = config.get_int_strict?;
let ssl = config.get_bool.unwrap_or;
let workers = config.get_int.unwrap_or;
println!;
Environment-Specific Configuration
use Config;
use env;
let env = var.unwrap_or_else;
let config = load_required?;
let db_url = config.str_strict?;
let log_level = config.str;
println!;
Database Connection Pooling
use Config;
let config = default;
let db_config = DatabaseConfig ;
let pool = create_pool?;
Sample YAML:
db:
host: localhost
port: 5432
username: admin
password: secret
pool_size: 20
timeout: 60.0
Feature Flags and Feature Detection
use Config;
let config = default;
if config.get_bool.unwrap_or
if config.get_bool.unwrap_or
let beta_features = config.list;
for feature in beta_features
Sample Configuration File
app:
name: MyApp
port: 8080
timeout: 30.5
debug: false
database:
host: localhost
port: 5432
name: myapp_db
username: admin
password: secret
pool_size: 10
server:
bind: 127.0.0.1
workers: 4
log_level: info
features:
analytics: true
profiling: false
beta:
- new_ui
- advanced_search
License
This project is licensed under the MIT License - see the LICENSE file for details