Skip to main content
Version: 0.7.0

Representations

Sometimes you need to see what your service loaded, which may differ from the file on disk. This is especially true when you are examining a running service's configuration state.

Three representations of one loaded spec are available:

  1. The source view shows the configuration exactly as the operator wrote it, with no defaults applied.
  2. The populated view shows the configuration the service resolved to, with every default filled.
  3. The runtime view shows the typed values the program uses.

The representations example prints all three from one spec.

Run it with:

cargo run -q -p confval --example representations --features derive,serde,toml

The source sets only mode, leaving max_body_mb to its default, so the views differ exactly where a default fills a gap:

+ Source view (what was set):
mode = "log"

+ Populated view (after defaults):
max_body_mb = 16
mode = "log"

+ Runtime view (what runs):
{
"max_body_mb": 16,
"mode": "log"
}

The Source View

to_source_fields returns a Fields holding only the fields the source set. It is generated by #[derive(Spec)]. The prelude exports the ToFields trait that declares it, so spec.to_source_fields() works wherever the prelude is in scope. The result is the same format-neutral field model a frontend produces, so the ordinary emit functions render it in any format.

use confval::format::toml::{emit_toml, parse_toml};

let spec: LimitsSpec = parse_toml(&sources, id, &mut report).unwrap();
let source = emit_toml(&spec.to_source_fields())?;

The view is decided one field at a time by whether the field's span is attached. Parsing gives every value it reads a real span into the source file, and every filled default carries a detached sentinel span instead. The source view keeps the attached values and drops the detached ones, so a default never appears as though the operator wrote it.

Each value the view keeps carries its real source span, so a tool that wants to report where a value came from still has the location. A block the operator wrote with every inner field left to its default renders as an empty block, because the block itself was written but nothing inside it was.

One shape cannot keep the distinction. A bare Vec<Located<String>> list holds no span of its own, so an empty list the operator wrote is indistinguishable from an absent one, and both are dropped. When the difference matters, use the wrapped form, Option<Located<Vec<Located<String>>>>, which keeps the list's own span and so survives the source view even when empty.

The Populated View

The populated view is the plain populate dump. to_fields fills every default the source omitted and the emitters render it.

let populated = emit_toml(&spec.to_fields())?;

Where the source view answers what was set, the populated view answers what the service resolved to, so a field the operator left out appears here with its default value.

The Runtime View

The runtime view is the lowered config serialized with serde. A lowered config holds plain runtime types, so deriving serde::Serialize on the config struct is the whole mechanism.

#[derive(confval::Config, serde::Serialize)]
#[confval(lower_from = LimitsSpec)]
struct LimitsConfig {
#[confval(lower(from = max_body_mb, with = narrow::i64_to_u16))]
max_body_mb: u16,
#[confval(lower(from = mode, with = narrow::keyword::<Mode>))]
mode: Mode,
}

A keyword_enum! type serializes as its keyword string rather than its Rust variant name, so the runtime view spells a mode "log" exactly as the config file and the other two views do. This impl is behind confval's serde feature, so it appears only when you enable serde.

Why a Separate Walk

The populated view and the source view read the same spec, but neither can produce the other. The populate walk fills defaults and detaches every span, so it has no record of what the source set. The source walk reads the spec's spans directly, which is where the set-or-defaulted distinction lives. to_source_fields is therefore its own walk rather than a filter over the populated model. It is a required method on ToFields, because no default body could answer the question without reporting defaults as operator-written.

A spec with a handwritten ToFields writes the source walk itself. Build it with FieldsBuilder, which takes the walk as a parameter and applies this rule per field, as Writing emitters by hand describes.