Skip to main content
Version: 0.7.x-dev

Lowering

Once a spec is validated, lowering converts it into a config type. A config type is the runtime form your program uses. Because lowering runs only after the gate, the narrowing conversions inside it never see a bad value.

Defining a config

#[derive(confval::Config)] writes the Lower impl that converts a validated spec into a runtime config:

#[derive(confval::Config)]
#[confval(lower_from = ServerSpec)]
pub struct ServerConfig {
#[confval(lower(from = version, with = i64_to_u32))]
pub version: u32,

#[confval(nested)]
pub limits: Option<LimitsConfig>,

pub ca_file: Option<String>,
}

The Lower trait is:

pub trait Lower<S>: Sized {
fn lower(spec: &S, report: &mut Report) -> Option<Self>;
}

Field rules:

  • No attribute: the field auto-maps via the LowerAuto trait, which strips Located wrappers without narrowing: Located<T> -> T, Option<Located<T>> -> Option<T>, Vec<Located<T>> -> Vec<T>, Located<Vec<Located<T>>> -> Vec<T>, and the optional variant of the last.
  • #[confval(nested)]: the field type implements Lower itself. Works for single, Option, and Vec shapes.
  • #[confval(nested, default)]: a non-optional config field lowered from an Option<Located<S>> spec field. When the source omits the block, S::default() is lowered in its place, so the runtime field is always populated while the spec stays source-faithful (an absent block stays None). This attribute also exists on the spec side, where it fills the omitted block during parsing instead of at lowering. See Optional fields and defaults for the difference.
  • #[confval(lower(from = field, with = fn))]: explicit conversion through a function fn(&SpecField, &mut Report) -> Option<Target>. All narrowing (i64 to u16, string to enum, string to IpNet) goes through these functions. from also accepts a tuple (a, b) when one config field derives from several spec fields.
  • #[confval(spec_only(field, ...))] at the struct level names spec fields that intentionally have no runtime counterpart.

The generated impl destructures the spec exhaustively with no rest pattern. Adding a field to either struct without accounting for it on the other side is a compile error. The two structs therefore stay in agreement.

Narrowing helpers

confval::pipeline::narrow provides ready-made with functions. For integer width changes: i64_to_u16, i64_to_u32, i64_to_u64, i64_to_usize, and opt_ variants for optional fields. They narrow with try_from rather than as. A value that does not fit is reported at its span and lowering fails, so a missing range rule is reported as a located error instead of silently truncating the value. i64_secs_to_duration (and opt_i64_secs_to_duration) route a seconds count through the same checked narrow into a Duration, rejecting a negative value rather than wrapping it. i64_to_f64 widens to f64 for the ratio and rate fields where an as cast cannot be named in a with attribute.

keyword::<T> lowers a validated keyword string into the enum that keyword_enum! generates, reading that enum's TryFrom<&str>. Name it with a turbofish so the derive knows which enum to parse into. The field was validated against the same set the TryFrom accepts, so the conversion does not fail in a running pipeline. The helper reports at the value's span when the keyword_set() check was left out of the Validate impl, or when a hand-rolled keyword set and its enum disagree, a drift keyword_enum! rules out.

keyword_list::<T> does the same for a list field, lowering a Vec<Located<String>> into a Vec<T>. Every element that fails is reported before the call returns, so an operator sees all of them in one run, and a single bad element leaves the whole field unlowered. opt_keyword_list::<T> takes the wrapped optional list, Option<Located<Vec<Located<String>>>>, and returns Some(None) for an absent field. It unwraps that wrapper as well as the Option, which the other opt_ helpers do not, because the wrapped shape adds a Located around the list. Validate a keyword list with check_each so a bad element is reported at its own span during validation rather than through the lowering helper's defensive branch.

use confval::pipeline::narrow;

#[derive(confval::Config)]
#[confval(lower_from = ServerSpec)]
struct ServerConfig {
#[confval(lower(from = port, with = narrow::i64_to_u16))]
port: u16,

#[confval(lower(from = mode, with = narrow::keyword::<LimitMode>))]
mode: LimitMode,
}