Why there is no get_string("foo.bar")¶
Most configuration libraries hand you a bag of values and a set of accessors:
get_string("http.host"), get_int("http.port"), a Sub("http") to scope into a
section. rtb-config has none of that, and the omission is deliberate.
Config<C> is a generic container over a struct you declare and derive
serde::Deserialize on. Configuration is reached by field access, like any other
Rust value.
What the dynamic version costs¶
A string key is not checked by anything. get_string("http.hosst") compiles, runs,
and returns an empty string or a default — at which point the tool connects to the
wrong place, or to nowhere, with no error anywhere near the mistake. Rename a key in
the config file and every call site that names it keeps compiling.
The type is not checked either. Asking for a string where the file holds an integer is a runtime question, answered differently by every library: some coerce, some return a zero value, some panic. In each case the failure surfaces where the value is used, which can be a long way from where it was loaded.
In a dynamically typed language that is a fair trade — there is no compiler to enlist. In Rust it is throwing away the language's main advantage.
What the typed version buys¶
Declaring the shape once means the compiler checks every use of it:
- A misspelled field is a compile error, at every call site, immediately.
- A type mismatch is a compile error, not a surprise at runtime.
- Renaming a field is a refactor the compiler drives, and
cargo checkfinds every place that has to change. - Nested access is
cfg.http.port, with completion and go-to-definition, rather than a string that no tool can follow.
Bad input still fails at runtime — that is unavoidable, since the file is written by a human after the binary was built — but it fails once, at load, with a message naming the key and the layer it came from. Not later, at the point of use, as a zero value.
What this costs you¶
Being honest about the other side of the trade:
- The shape is fixed at compile time. A tool that must accept keys it does not
know about — plugin configuration, arbitrary user-defined sections — cannot express
that as named fields. The escape hatch is a
HashMap<String, serde_json::Value>field, which is the dynamic bag again, scoped to where it is genuinely needed. - There is no generic inspection API. "Print every configured key and value" is
not something the crate can do for you; it is
serde::Serializeon your own struct. - Every consumer needs the type. A helper that works over "some configuration"
has to be generic over
Cor take the concrete struct.
For a CLI or a service — the things this toolkit builds — the shape is known at compile time, and those costs do not bite.
Why the Go toolkit does it differently¶
The Go sibling of this crate exposes a dynamic Containable interface with
GetString("foo.bar") accessors, and that is not an inconsistency anyone intends to
fix. Go's reflection-based struct handling and its lack of generics at the time made
the dynamic bag the natural idiom there; Rust's derive macros and monomorphisation
make the typed container the natural idiom here.
The two toolkits aim at the same outcomes — layered precedence, an explicit reload, one error surface — and reach them through whatever each language does well. A port that carried the Go accessor API into Rust would have been faithful to the implementation and wrong about the point of it.
Next¶
- How layering and precedence work
- Add layered configuration to a Rust CLI — the typed container in use