star_toml
A framework for loading, layering, and validating any *.toml configuration file
in Rust — the * in *.toml.
Most config crates parse a file into a struct and stop there. star_toml brings the
Pydantic experience to TOML: a validation engine that
collects every error across the whole config tree at once — each with a precise
location, the offending value, and a machine-matchable code — plus a layered loader that
composes defaults, files, and environment overrides.
Dependencies: serde, toml, thiserror. No async runtime, no crypto, no serde_json.
The headline: Pydantic-grade validation
use ;
Feed it a broken config and you get all the failures, not just the first:
4 validation errors for App
name
must not be empty (got: `""`) [empty]
workers
input must be in range 1..=1024 (got: `0`) [out_of_range]
server.host
must not be empty (got: `""`) [empty]
server.port
input must be in range 1..=65535 (got: `0`) [out_of_range]
Every error is also programmatically matchable:
# use ;
# ;
#
# let app = App;
let report = app.check.unwrap_err;
for e in report.errors
Every error also carries a auto-derived repair hint and a machine-readable severity:
# use ;
# ;
#
# let app = App;
let report = app.check.unwrap_err;
// Van der Aalst conformance score: 0.0 = total failure, 1.0 = perfect
println!;
// Stable variant fingerprint — same error pattern = same ID across runs
println!;
// Object-centric grouping by top-level config section
for in report.by_section
Built-in checks
| Helper | Error code | Use |
|---|---|---|
check_non_empty(field, &str) |
empty |
reject empty strings |
check_range(field, value, lo..=hi) |
out_of_range |
numeric bounds |
check_one_of(field, &str, &[..]) |
not_one_of |
enumerations |
check_predicate(field, cond, code, msg) |
your code | arbitrary domain rules |
check_consistent(field, &[related], cond, code, msg) |
your code | DECLARE cross-field constraints |
with_severity(Severity::Warning, |v| …) |
— | emit non-Error severity |
field(name, |v| …) / index(i, |v| …) |
— | descend into nested structs / arrays |
Van der Aalst innovations
| Feature | API | Description |
|---|---|---|
| Conformance fitness | report.fitness() -> f64 |
Alignment metric: proportion of checks that passed (0.0–1.0) |
| Variant fingerprint | report.variant_id() -> u64 |
FNV-1a hash of error patterns — same failures = same ID |
| Object-centric grouping | report.by_section() |
Errors indexed by top-level config section |
| Severity stratification | Severity::{Advisory,Warning,Error,Fatal} |
Not all failures are equal |
| Repair hints | error.repair_hint() |
Auto-derived minimum fix for each error kind |
| DECLARE constraints | check_consistent(…) |
Cross-field co-existence / response constraints |
Layered loading
Sources merge left-to-right; later layers win. Tables merge key-by-key; arrays and scalars are replaced.
use Loader;
const DEFAULTS: &str = "name = 'my-app'\nport = 8080\n";
let cfg: AppConfig = new
.layer_str // lowest priority
.find_file // walk up from cwd
.layer_file_if_exists // optional user overrides
.env_prefix // APP_PORT=9090 → port = 9090
.load?; // highest priority = env
# Ok::
Environment overrides map APP_SERVER__PORT=9090 → server.port = 9090, coercing the
value to the right TOML scalar type (bool → int → float → string).
Source-relative paths
ConfigFile<T> remembers where the config came from, so relative paths inside the
config resolve correctly regardless of the working directory:
use ;
let cf: = new.find_file.load_file?;
let abs = cf.resolve; // anchored at build.toml's directory
# Ok::
Write-back
use save_file;
save_file?; // creates parent dirs
# Ok::
Loose API surface
| Function | Purpose |
|---|---|
from_str::<T>(s) |
parse a TOML string (with env expansion) |
load_file::<T>(path) |
load + parse a single file |
find_config_file(name, start) |
walk parent dirs for a file |
find_and_load::<T>(name, start) |
the two combined |
to_string(&value) / save_file(&value, path) |
serialize back to TOML |
deep_merge(&mut base, overlay) |
recursive TOML value merge |
expand_env_vars(s) |
${VAR} / $VAR substitution (UTF-8 safe) |
Run the example