use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub server: ServerConfig,
pub database: DatabaseConfig,
#[serde(default)]
pub logging: LoggingConfig,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
#[serde(default = "default_max_connections")]
pub max_connections: u32,
}
#[derive(Debug, Default, Deserialize)]
pub struct LoggingConfig {
#[serde(default)]
pub level: LogLevel,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Debug,
#[default]
Info,
Warn,
Error,
}
fn default_port() -> u16 {
8080
}
fn default_max_connections() -> u32 {
10
}
impl Config {
pub fn validate(&self) -> Result<(), String> {
if self.database.url.trim().is_empty() {
return Err("database.url must not be empty".into());
}
if self.database.max_connections == 0 {
return Err("database.max_connections must be greater than 0".into());
}
Ok(())
}
}
use std::path::Path;
use thiserror::Error;
use crate::config::Config;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read config file `{path}`: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("failed to parse TOML: {0}")]
Parse(#[from] toml::de::Error),
#[error("invalid configuration: {0}")]
Validation(String),
}
impl Config {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Config, ConfigError> {
let path = path.as_ref();
let raw = std::fs::read_to_string(path).map_err(|source| ConfigError::Io {
path: path.display().to_string(),
source,
})?;
let mut config: Config = toml::from_str(&raw)?;
apply_env_overrides(&mut config);
config.validate().map_err(ConfigError::Validation)?;
Ok(config)
}
}
fn apply_env_overrides(config: &mut Config) {
if let Ok(port) = std::env::var("APP__SERVER__PORT") {
if let Ok(port) = port.parse() {
config.server.port = port;
}
}
if let Ok(url) = std::env::var("APP__DATABASE__URL") {
config.database.url = url;
}
}
mod config;
mod loader;
use config::Config;
fn main() {
let path = std::env::args()
.nth(1)
.unwrap_or_else(|| "config.toml".to_string());
let config = match Config::load(&path) {
Ok(config) => config,
Err(err) => {
eprintln!("configuration error: {err}");
std::process::exit(1);
}
};
println!(
"starting server on {}:{} (log level: {:?})",
config.server.host, config.server.port, config.logging.level
);
println!(
"database pool size: {}",
config.database.max_connections
);
}
This snippet models the common task of loading application configuration from a TOML file into strongly-typed Rust structs, layering environment overrides on top, and validating the result before the program uses it. The value of splitting it into files is that the config schema stays a pure data description while loading, merging, and error handling live in a dedicated loader.
In config.rs, each section of the file becomes its own struct: ServerConfig, DatabaseConfig, and LoggingConfig, aggregated by the top-level Config. #[derive(Deserialize)] lets serde map TOML tables onto these types by field name, so a [server] table populates ServerConfig. Optional keys use #[serde(default = "...")] pointing at small functions like default_port, which means a missing key falls back to a sensible value instead of failing. #[serde(deny_unknown_fields)] on Config turns typos in section names into hard errors rather than silently ignored config — a deliberate trade-off favoring strictness. The LogLevel enum with #[serde(rename_all = "lowercase")] shows how a constrained set of string values maps to a real enum, so invalid levels are rejected at parse time.
The validate method covers rules serde cannot express, such as max_connections being non-zero; this separation keeps deserialization about shape and validation about semantics.
In loader.rs, ConfigError uses thiserror to unify IO, parse, and validation failures into one type with #[from] conversions, so ? propagates cleanly. Config::load reads the file, calls toml::from_str, applies apply_env_overrides to let variables like APP__SERVER__PORT win over file values, then runs validate. Reading the file separately from parsing produces clearer error messages that name the path.
In main.rs, the entry point simply calls Config::load and prints a friendly message on failure, exiting non-zero. This pattern is worth reaching for whenever config correctness matters at startup: failing fast with a precise error beats discovering a bad port or empty database URL deep in a request handler. The main pitfall is over-strictness locking out valid future keys, which deny_unknown_fields should be applied to consciously.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.