Templates
A spec type already encodes the whole configuration surface. It names every field, holds every default, and carries the doc comment you wrote on each field. Once you have a spec, you can run parsing backward and turn it into a configuration file. This backward direction is called populate.
to_fields produces a plain configuration file with every default filled in.
to_template produces the same file with each field's documentation rendered as a comment above it, so an operator who opens the file learns what every setting means.
Either one is useful when you build a CLI command that writes a starter config, or when you want to show what the spec resolved to once its defaults were applied.
The crate ships a templates example that parses a two-line config and emits it back as an annotated TOML template.
Run it with:
cargo run -q -p confval --example templates --features derive,color,toml
The source sets only hostname and port.
Populate fills workers and tls from their defaults and fills the whole limits block, and emit renders each field's comment above it:
# The address the server binds to.
hostname = "127.0.0.1"
# The port the server listens on.
port = 8080
# The number of worker threads.
workers = 4
# Whether TLS is enabled.
tls = false
# Request size and mode limits.
[limits]
# The maximum request body size, in megabytes.
max_body_mb = 16
# How limit violations are handled.
mode = "enforce"
Generating a Template
to_template is the method you call to produce an annotated template.
It is generated by #[derive(Spec)].
The prelude exports the ToFields trait that declares it, so spec.to_template() works wherever the prelude is in scope.
It returns a Fields, the same format-neutral field model a frontend produces when it parses a file, so the ordinary emit functions render it.
use confval::format::toml::{emit_toml, parse_toml};
let spec: ServerSpec = parse_toml(&sources, id, &mut report).unwrap();
let template = emit_toml(&spec.to_template())?;
emit_hcl renders the same model as HCL.
emit_kdl renders it as KDL, with each comment as a // line above its node:
use confval::format::kdl::{emit_kdl, parse_kdl};
let spec: ServerSpec = parse_kdl(&sources, id, &mut report).unwrap();
let template = emit_kdl(&spec.to_template())?;
emit_yaml renders it as YAML, with each comment as a # line above its entry.
JSON has no comment syntax, so emit_json renders no doc comments and skips commented entries.
emit_json(&spec.to_template()) therefore produces the same text as emit_json(&spec.to_fields()).
A commented entry stands for a field the source does not set, so the emitted JSON still holds every value the spec
carries, and it shows none of the settings the operator has not written.
Use HCL, TOML, KDL, or YAML when you want an annotated template.
A comment is indented to line up with the field it documents, so a comment inside a block is at the block's indentation:
# The address the server binds to.
hostname = "127.0.0.1"
# The port the server listens on.
port = 8080
# The number of worker threads.
workers = 4
# Whether TLS is enabled.
tls = false
# Request size and mode limits.
limits {
# The maximum request body size, in megabytes.
max_body_mb = 16
# How limit violations are handled.
mode = "enforce"
}
TOML content is flat, so in a TOML template every comment is at column zero.
Emit writes canonical text rather than rewriting a file a person authored.
It drops the comments and layout the field model never held.
A nested struct is written as a [table] in TOML, a block in HCL, or a children node in KDL.
Writing the Comments
A field's comment comes from its Rust doc comment, so you write the documentation once and it serves both the code and the template.
#[derive(confval::Spec)]
struct ServerSpec {
/// The port the server listens on.
port: Located<i64>,
/// Request size and mode limits.
#[confval(nested, default)]
limits: Option<Located<LimitsSpec>>,
}
A multi-line doc comment renders as one # line per source line, and a blank line inside the comment renders as a bare #.
When the template text should read differently from the rustdoc, set it on the field with #[confval(doc = "...")].
That text is used in place of the doc comment.
The Plain Dump
When you want the configuration file without any commentary, call to_fields instead.
It returns the same populated Fields as to_template but with no comments attached, so the emitted file is a clean dump of values.
let text = emit_toml(&spec.to_fields())?;
The two methods share one populated model and differ only in the comment lines, so the plain dump and the annotated template always describe the same configuration.
A spec with a handwritten ToFields builds that model with FieldsBuilder, described in
Writing emitters by hand.
Because the model is the same Fields type parsing produces, anything that reads a parsed field model reads a populated one the same way.
hostname = "127.0.0.1"
port = 8080
workers = 4
tls = false
[limits]
max_body_mb = 16
mode = "enforce"
What Gets Filled
Populate emits an active field only when there is a value to show. The rules follow from what the parser leaves in the spec:
- A required field is always present, so it is always emitted.
- A leaf with an attribute default is emitted with that default, because parsing already filled it when the source omitted the field.
- A repeated block is emitted once per element.
- An optional block is filled only when you mark it. See Marking Optional Blocks.
A block that is present is populated in turn, so a block you wrote but left partial gains its own absent defaults. A block that is filled is populated to full depth, so one call at the root resolves a nested tree of defaults all the way down.
Commented-Out Entries
An absent optional field still exists in the spec.
A template that hid it would leave you unaware the setting is available.
to_template renders each one as a commented-out entry instead, with its doc comment above it, so the template documents every field the spec accepts while activating only the ones that carry a value.
to_fields, the plain dump, emits no commented entries.
Each shape renders a placeholder you overwrite when uncommenting:
- An optional leaf with no default shows a zero value for its type, the empty string,
0,0.0, orfalse. - An optional string list shows an empty list.
- An unmarked optional block shows an empty block, because filling its contents needs an instance only the marker provides.
- An empty repeated block shows one empty element. TOML's array-of-tables syntax keeps the repetition visible. HCL and KDL show a single block.
The marker is each format's own.
TOML and HCL prefix every line with a spaceless #, so an entry stays distinguishable from a # doc comment.
Uncommenting is deleting that one character:
# The PID file path.
#pid_file = ""
#[[svc]]
# The PID file path.
#pid_file = ""
#svc {
#}
YAML uses the same spaceless #, with the marker after the indentation so deleting it leaves the entry at its own
column:
# The PID file path.
#pid_file: ""
#svc:
#- {}
An empty repeated block shows one #- {} element, because uncommenting must leave an empty instance of the right
shape, and a bare - would read as a null element.
KDL uses its native slashdash, a disabled node the parser reads and discards.
Uncommenting is deleting the /-:
// The PID file path.
/-pid_file ""
A commented entry is invisible to every parser, so a template parses to the same configuration with or without its commented entries.
Marking Optional Blocks
An optional block is absent until you write it.
Parsing keeps an absent Option<Located<S>> as None, so a spec read back from a file stays faithful to what the operator wrote.
Populate has to know which absent blocks to fill and which to leave out, because a block the runtime never applies should not appear in a populated view.
The #[confval(nested, default)] marker on an optional nested field is that signal.
A marked block is filled from its type's Default when you populate, and an unmarked optional block is left absent.
#[derive(confval::Spec)]
struct ServerSpec {
hostname: Located<String>,
port: Located<i64>,
#[confval(nested, default)]
limits: Option<Located<LimitsSpec>>,
#[confval(nested)]
telemetry: Option<Located<TelemetrySpec>>,
}
Here limits is filled from LimitsSpec::default() and telemetry is left absent.
A marked block requires its inner type to implement Default.
Deriving that with #[confval(derive_default)] generates the impl from the same attribute defaults, so one declaration drives parsing, Default, and the populated output together.
The marker changes populate only, so parsing still leaves an absent block None and the read path is unchanged.
When Emit Fails
Emit returns a Result, because not every field model can be written faithfully in every format.
On the populate path the risk is small.
Emitting a populated spec to TOML always succeeds.
TOML has a literal for every value populate produces and quotes any key, so emit_toml(&spec.to_template())? cannot fail on a populated model.
Emitting a populated spec to HCL fails only for two numeric values HCL has no literal for.
The first is i64::MIN, which HCL would write as a negation that overflows when it is read back.
The second is a non-finite float, an infinity or a NaN, which HCL has no keyword for.
A spec that holds neither emits to HCL without failing.
Emitting a populated spec to JSON fails only for a non-finite float, an infinity or a NaN, which JSON has no literal
for.
i64::MIN emits, because JSON writes it as a plain integer.
Emitting a populated spec to YAML never fails. YAML 1.2 writes an infinity and a NaN natively, and any key writes as a quoted string.
Format Limitations collects every format's gaps in one place.
Emit can also fail on a Fields that a frontend parsed rather than populated, because a parsed model can carry a name or a value the target format cannot write.
A value with no representation, such as a TOML datetime, fails in any format.
A name that is not a valid identifier fails when you emit HCL, which has no way to quote it, while TOML and KDL quote it without trouble.
HCL also writes a value and a block side by side under one name.
A TOML key names one thing, so emit_toml refuses that pair rather than silently dropping one of the two.
Neither format can write one name twice for plain values, so both emitters refuse that as well.
emit_json and emit_yaml group a repeated name into one member holding a sequence, so a name used twice for plain
values emits.
Both refuse a value beside a same-named block.
The only way either can write that pair is a duplicate key, which loses one of the two members.
Each emit error names the dotted path of the field responsible, so a failure in a large tree points at its location.
A tree assembled by layering can carry unparsed text from an environment variable or a command line flag. That text emits as a string literal, since its type was never decided. A typed reparse of the emitted file therefore reads those leaves as strings.
Detached Spans and the Fixed Point
Every value populate produces carries a detached span, a span with no source location, because the value comes from the spec and not from a file. A parsed value points at the bytes it came from, while a populated value has no bytes to point at, so its span records only that the value was filled.
Equality on a Located value ignores the span, so a populated spec compares equal by value to the same spec parsed back from its own emitted output.
Populate only fills defaults, so running it on a spec that is already complete adds nothing.
Populate is therefore a fixed point.
Populate a spec, emit it, parse it back, and populate again.
The result is the same configuration.