1"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([["9177"],{58886(e,n,s){s.r(n),s.d(n,{metadata:()=>d,default:()=>h,frontMatter:()=>i,contentTitle:()=>l,toc:()=>a,assets:()=>c});var d=JSON.parse('{"id":"guide/parsing","title":"Parsing","description":"When you define a configuration for your application, the first step is to parse a file into a spec type.","source":"@site/versioned_docs/version-0.11.0/guide/parsing.md","sourceDirName":"guide","slug":"/guide/parsing","permalink":"/confval/docs/0.11.0/guide/parsing","draft":false,"unlisted":false,"editUrl":"https://github.com/ethanhann/confval/tree/main/docs/versioned_docs/version-0.11.0/guide/parsing.md","tags":[],"version":"0.11.0","sidebarPosition":1,"frontMatter":{"sidebar_position":1},"sidebar":"tutorialSidebar","previous":{"title":"The Pipeline Contract","permalink":"/confval/docs/0.11.0/pipeline"},"next":{"title":"Validation","permalink":"/confval/docs/0.11.0/guide/validation"}}'),t=s(74848),r=s(28453);let i={sidebar_position:1},l="Parsing",c={},a=[{value:"A first parse",id:"a-first-parse",level:2},{value:"Located values",id:"located-values",level:2},{value:"Defining a spec",id:"defining-a-spec",level:2},{value:"Field types",id:"field-types",level:3},{value:"Optional fields and defaults",id:"optional-fields-and-defaults",level:3},{value:"Deriving <code>Default</code> from the attribute defaults",id:"deriving-default-from-the-attribute-defaults",level:3}
1,{value:"Nested structs",id:"nested-structs",level:3},{value:"Maps",id:"maps",level:3},{value:"Unknown fields",id:"unknown-fields",level:3},{value:"What the derive does not handle",id:"what-the-derive-does-not-handle",level:3},{value:"Parsing a file",id:"parsing-a-file",level:2},{value:"Nesting equivalences",id:"nesting-equivalences",level:3},{value:"Writing back to text",id:"writing-back-to-text",level:3},{value:"Single-element list coercion",id:"single-element-list-coercion",level:3},{value:"Format-specific behavior",id:"format-specific-behavior",level:3},{value:"HCL and TOML",id:"hcl-and-toml",level:4},{value:"KDL",id:"kdl",level:4},{value:"JSON",id:"json",level:4},{value:"YAML",id:"yaml",level:4},{value:"Writing parsers by hand",id:"writing-parsers-by-hand",level:2},{value:"The field model",id:"the-field-model",level:3},{value:"Helpers",id:"helpers",level:3},{value:"Writing emitters by hand",id:"writing-emitters-by-hand",level:2}];function o(e){let n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"parsing",children:"Parsing"})}),"\n",(0,t.jsx)(n.p,{children:"When you define a configuration for your application, the first step is to parse a file into a spec type.\nA spec type is a plain Rust struct whose fields are the settings you expect."}),"\n",(0,t.jsx)(n.p,{children:'"Spec" is short for "specification."\nThe collection of spec types is the specification for your application\'s operator-facing configuration surface.'}),"\n",(0,t.jsxs)(n.p,{children:["Parsing checks structure only.\nIt determines whether each field is present and has the right type.\nWhat the values mean is left to ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/validation",children:"validation"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"a-first-parse",children:"A first parse"}),"\n",(0,t.jsx)(n.p,{children:"Define a spec as a struct, then parse a file into it with the frontend for the format you enabled."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:'use confval::prelude::*;\n\n#[derive(confval::Spec)]\nstruct ServerSpec {\n hostname: Located<String>,\n port: Located<i64>,\n}\n\nlet text = r#"hostname = "127.0.0.1"\nport = 8080\n"#;\n\nlet mut sources = SourceMap::new();\nlet mut report = Report::new();\nlet id = sources.add("server.hcl", text);\n\nlet spec: Option<ServerSpec> = confval::format::hcl::parse_hcl(&sources, id, &mut report);\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Every field is wrapped in a ",(0,t.jsx)(n.code,{children:"Located<T>"}),", which pairs the value with its source span.\nThe parse reports a missing field, a wrong type, or an unknown field, each at its span.\nWhat the values mean is left to ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/validation",children:"validation"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"located-values",children:"Located values"}),"\n",(0,t.jsxs)(n.p,{children:["Every field on a spec is wrapped in a ",(0,t.jsx)(n.code,{children:"Located<T>"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"pub struct Located<T> {\n pub value: T,\n pub span: Span,\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["A ",(0,t.jsx)(n.code,{children:"Located<T>"})," contains a span.\nA span is a byte range in the configuration file.\nIt records where the value came from, so a later error can point at the line and column.\n",(0,t.jsx)(n.code,{children:"Span"})," and the ",(0,t.jsx)(n.code,{children:"SourceMap"})," that resolves it are covered under ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/diagnostics#spans-and-source",children:"Diagnostics"}),"."]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Behavior"}),(0,t.jsx)(n.th,{children:"Detail"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Value-only equality"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"PartialEq"}),", ",(0,t.jsx)(n.code,{children:"Eq"}),", and ",(0,t.jsx)(n.code,{children:"Hash"})," ignore the span. Two configs with the same values compare equal regardless of formatting."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"Deref"})," to ",(0,t.jsx)(n.code,{children:"T"})]}),(0,t.jsx)(n.td,{children:"Method calls pass through to the inner value."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Located::detached(value)"})}),(0,t.jsx)(n.td,{children:"Produces a value with a sentinel span. Use it to build a spec in code (tests, builders, generated templates) with no source file behind it."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Default"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"detached(T::default())"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"serde"}),(0,t.jsxs)(n.td,{children:["With the ",(0,t.jsx)(n.code,{children:"serde"})," feature, ",(0,t.jsx)(n.code,{children:"Located<T>"})," serializes transparently as ",(0,t.jsx)(n.code,{children:"T"})," and deserializes detached."]})]})]})]}),"\n",(0,t.jsx)(n.h2,{id:"defining-a-spec",children:"Defining a spec"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"#[derive(confval::Spec)]"})," writes the parser for a struct.\nParsing is purely structural, so the macro never embeds semantic rules."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"#[derive(confval::Spec)]\npub struct ServerSpec {\n pub version: Located<i64>,\n pub threads: Option<Located<i64>>,\n\n #[confval(nested)]\n pub limits: Option<Located<LimitsSpec>>,\n\n #[confval(default = 30)]\n pub refresh_interval_seconds: Located<i64>,\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"field-types",children:"Field types"}),"\n",(0,t.jsx)(n.p,{children:"A field's type tells the parser how to read it."}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Scalars"}),": ",(0,t.jsx)(n.code,{children:"Located<String>"}),", ",(0,t.jsx)(n.code,{children:"Located<i64>"}),", ",(0,t.jsx)(n.code,{children:"Located<f64>"}),", ",(0,t.jsx)(n.code,{children:"Located<bool>"}),", and ",(0,t.jsx)(n.code,{children:"Located<PathBuf>"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Lists of strings"}),": ",(0,t.jsx)(n.code,{children:"Vec<Located<String>>"}),", or ",(0,t.jsx)(n.code,{children:"Option<Located<Vec<Located<String>>>>"})," for an optional list."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Nested structs"}),": another ",(0,t.jsx)(n.code,{children:"Spec"})," type marked with ",(0,t.jsx)(n.code,{children:"#[confval(nested)]"}),", described below."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Maps"}),": an open-ended, string-keyed map, ",(0,t.jsx)(n.code,{children:"BTreeMap<String, Located<String>>"})," marked with ",(0,t.jsx)(n.code,{children:"#[confval(map)]"}),", described below."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"optional-fields-and-defaults",children:"Optional fields and defaults"}),"\n",(0,t.jsxs)(n.p,{children:["Every field is required by default.\nLeave a required field out of the file and the parser reports a ",(0,t.jsx)(n.code,{children:"missing field"})," error against the block it belongs to."]}),"\n",(0,t.jsx)(n.p,{children:"Two things make a field optional:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"Option<...>"})," on the type turns an absent field into ",(0,t.jsx)(n.code,{children:"None"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"#[confval(default)]"})," or ",(0,t.jsx)(n.code,{children:"#[confval(default = expr)]"})," fills an absent field instead of reporting it."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["A bare ",(0,t.jsx)(n.code,{children:"#[confval(default)]"})," uses the field type's ",(0,t.jsx)(n.code,{children:"Default"}),".\nThe ",(0,t.jsx)(n.code,{children:"default = expr"})," form uses ",(0,t.jsx)(n.code,{children:"expr"})," instead.\nFor example, ",(0,t.jsx)(n.code,{children:"#[confval(default = 30)]"})," gives the field the value ",(0,t.jsx)(n.code,{children:"30"})," when the file leaves it out.\nA filled-in value carries a detached span, because no source text stands behind it."]}),"\n",(0,t.jsx)(n.p,{children:"Which form a field accepts depends on its shape."}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Field shape"}),(0,t.jsx)(n.th,{children:(0,t.jsx)(n.code,{children:"#[confval(default)]"})}),(0,t.jsx)(n.th,{children:(0,t.jsx)(n.code,{children:"#[confval(default = expr)]"})})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Located<T>"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"T::default()"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"expr"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Option<Located<T>>"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Some(T::default())"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Some(expr)"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Vec<Located<String>>"})}),(0,t.jsx)(n.td,{children:"empty list"}),(0,t.jsx)(n.td,{children:"compile error"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"BTreeMap<...>"})," with ",(0,t.jsx)(n.code,{children:"#[confval(map)]"})]}),(0,t.jsx)(n.td,{children:"empty map"}),(0,t.jsx)(n.td,{children:"compile error"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"Located<S>"})," with ",(0,t.jsx)(n.code,{children:"#[confval(nested)]"})]}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"S::default()"})}),(0,t.jsx)(n.td,{children:"compile error"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"Option<Located<S>>"})," with ",(0,t.jsx)(n.code,{children:"#[confval(nested)]"})]}),(0,t.jsx)(n.td,{children:"compile error"}),(0,t.jsx)(n.td,{children:"compile error"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"Vec<Located<S>>"})," with ",(0,t.jsx)(n.code,{children:"#[confval(nested)]"})]}),(0,t.jsx)(n.td,{children:"compile error"}),(0,t.jsx)(n.td,{children:"compile error"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Option<Located<Vec<Located<String>>>>"})}),(0,t.jsx)(n.td,{children:"compile error"}),(0,t.jsx)(n.td,{children:"compile error"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Combining ",(0,t.jsx)(n.code,{children:"Option"})," with a default means the field is never ",(0,t.jsx)(n.code,{children:"None"})," for an absent value.\nThe default fills it in.\nLeave the default off when you need the ",(0,t.jsx)(n.code,{children:"Option"})," to report what the source omitted."]}),"\n",(0,t.jsxs)(n.p,{children:["An optional nested block rejects a default because an absent block already yields ",(0,t.jsx)(n.code,{children:"None"}),".\nA nested list rejects one because a list of blocks is zero-or-more already."]}),"\n",(0,t.jsxs)(n.p,{children:["A string list accepts only the bare form, where the default is the empty list.\nThere is no ",(0,t.jsx)(n.code,{children:"default = expr"})," for a list."]}),"\n",(0,t.jsx)(n.admonition,{type:"caution",children:(0,t.jsxs)(n.p,{children:["The attribute ",(0,t.jsx)(n.code,{children:"#[confval(nested, default)]"}
1)," also exists on the config side, where it means something different.\nOn a spec it fills the omitted block during parsing.\nThe spec itself holds the default.\nOn a config it leaves the spec field ",(0,t.jsx)(n.code,{children:"None"})," and lowers ",(0,t.jsx)(n.code,{children:"S::default()"})," in its place.\nThe spec stays faithful to the source and only the runtime value is filled in.\nThe two are independent.\nOne setting can use either, both, or neither.\nSee ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/lowering#defining-a-config",children:"Lowering"}),"."]})}),"\n",(0,t.jsxs)(n.h3,{id:"deriving-default-from-the-attribute-defaults",children:["Deriving ",(0,t.jsx)(n.code,{children:"Default"})," from the attribute defaults"]}),"\n",(0,t.jsxs)(n.p,{children:["The attribute default fills a field the file omits.\nWhen the whole block is omitted, the config side supplies it through ",(0,t.jsx)(n.code,{children:"#[confval(nested, default)]"}),", which lowers ",(0,t.jsx)(n.code,{children:"S::default()"}),".\nThe spec type therefore needs a ",(0,t.jsx)(n.code,{children:"Default"})," impl.\nWriting that impl by hand repeats the attribute defaults, and nothing keeps the two in agreement."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"#[confval(derive_default)]"})," on the struct generates the ",(0,t.jsx)(n.code,{children:"Default"})," impl from the attribute defaults.\nEach default is declared once."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:'#[derive(confval::Spec)]\n#[confval(derive_default)]\nstruct LimitsSpec {\n #[confval(default = 16)]\n max_body_mb: Located<i64>,\n #[confval(default = "enforce".to_string())]\n mode: Located<String>,\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["The standard ",(0,t.jsx)(n.code,{children:"#[derive(Default)]"})," fills each field with ",(0,t.jsx)(n.code,{children:"T::default()"}),".\nA ",(0,t.jsx)(n.code,{children:"Located<String>"})," becomes empty and a ",(0,t.jsx)(n.code,{children:"Located<i64>"})," becomes zero.\n",(0,t.jsx)(n.code,{children:"#[confval(derive_default)]"})," fills each field from its declared ",(0,t.jsx)(n.code,{children:"#[confval(default)]"})," instead.\nIt refuses a field that declares no default rather than inventing a value."]}),"\n",(0,t.jsxs)(n.p,{children:["Use ",(0,t.jsx)(n.code,{children:"#[confval(derive_default)]"})," rather than ",(0,t.jsx)(n.code,{children:"#[derive(Default)]"})," on a spec.\nThe standard derive fills an undeclared field with ",(0,t.jsx)(n.code,{children:"T::default()"})," without reporting it.\nThe value for an absent block and the value for a field the source omits can then drift apart.\n",(0,t.jsx)(n.code,{children:"#[confval(derive_default)]"})," keeps those two values the same."]}),"\n",(0,t.jsxs)(n.p,{children:["The value it generates for a field is the value the parser fills when that field is absent.\nA field the parser would report as missing has no value to derive, so it is a compile error.\nA non-optional ",(0,t.jsx)(n.code,{children:"Located<T>"})," or ",(0,t.jsx)(n.code,{children:"Located<S>"})," with no default, and a ",(0,t.jsx)(n.code,{children:"Vec<Located<String>>"})," with no default, each need a ",(0,t.jsx)(n.code,{children:"#[confval(default)]"})," or a handwritten ",(0,t.jsx)(n.code,{children:"impl Default"}),".\nAn ",(0,t.jsx)(n.code,{children:"Option"})," field and a nested list default on their own, because the parser already fills them when they are absent."]}),"\n",(0,t.jsxs)(n.p,{children:["The attribute is opt-in and additive.\nA type that keeps its handwritten ",(0,t.jsx)(n.code,{children:"impl Default"})," is unaffected."]}
1),"\n",(0,t.jsx)(n.h3,{id:"nested-structs",children:"Nested structs"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"#[confval(nested)]"})," tells the parser to read a field with its own ",(0,t.jsx)(n.code,{children:"Spec"})," type instead of as a scalar.\nIt works three ways:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["a single struct, ",(0,t.jsx)(n.code,{children:"Located<T>"})]}),"\n",(0,t.jsxs)(n.li,{children:["an optional struct, ",(0,t.jsx)(n.code,{children:"Option<Located<T>>"})]}),"\n",(0,t.jsxs)(n.li,{children:["a list of structs, ",(0,t.jsx)(n.code,{children:"Vec<Located<T>>"}),", which reads a block that may repeat"]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"maps",children:"Maps"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"#[confval(map)]"})," reads a field as an open-ended, string-keyed map, ",(0,t.jsx)(n.code,{children:"BTreeMap<String, Located<String>>"}),".\nUse it for a setting whose keys are not known ahead of time, such as HTTP request headers or URL templates."]}),"\n",(0,t.jsxs)(n.p,{children:["The keys are open, so the parser reports no unknown field inside the map.\nA duplicate key is an error.\nEach value keeps its span, so a ",(0,t.jsx)(n.code,{children:"Validate"})," impl can report a bad entry at the entry.\nAn operator writes the map as a block or as an inline map, and both read the same."]}),"\n",(0,t.jsxs)(n.p,{children:["A bare ",(0,t.jsx)(n.code,{children:"#[confval(map, default)]"})," reads an absent map as empty.\nOn the config side the map lowers to a plain ",(0,t.jsx)(n.code,{children:"HashMap<String, String>"})," or ",(0,t.jsx)(n.code,{children:"BTreeMap<String, String>"})," with no lowering function, because the two ",(0,t.jsx)(n.code,{children:"LowerAuto"})," impls drop each value's span."]}),"\n",(0,t.jsx)(n.p,{children:"Only a string-keyed map with string values is supported.\nA map of another value type, or a map of nested structs, needs a handwritten parser."}),"\n",(0,t.jsx)(n.h3,{id:"unknown-fields",children:"Unknown fields"}),"\n",(0,t.jsx)(n.p,{children:"The parser reports a setting that the spec struct does not declare as an unknown field error.\nThere is no lenient mode that ignores extra settings."}),"\n",(0,t.jsx)(n.p,{children:"An agent editing a configuration file can add a setting the spec does not declare.\nThe strict parse reports that setting instead of ignoring it."}),"\n",(0,t.jsx)(n.h3,{id:"what-the-derive-does-not-handle",children:"What the derive does not handle"}),"\n",(0,t.jsx)(n.p,{children:"The derive handles plain structs only.\nIt cannot express an enum."}),"\n",(0,t.jsxs)(n.p,{children:["That is rarely a problem, because a field with a discrete set of values is a ",(0,t.jsx)(n.code,{children:"Located<String>"})," in a spec by convention rather than an enum.\n",(0,t.jsx)(n.a,{href:"#writing-parsers-by-hand",children:"Writing parsers by hand"})," covers that convention, along with the shapes that do need a handwritten parser."]}),"\n",(0,t.jsx)(n.h2,{id:"parsing-a-file",children:"Parsing a file"}),"\n",(0,t.jsx)(n.p,{children:"To parse, call the frontend for the format you enabled:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Entry point"}),(0,t.jsx)(n.th,{children:"Feature"}),(0,t.jsx)(n.th,{children:"Backed by"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"confval::format::hcl::parse_hcl"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"hcl"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"hcl-edit"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"confval::format::toml::parse_toml"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"toml"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"toml_edit"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"confval::format::kdl::parse_kdl"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"kdl"})}
1),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"kdl"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"confval::format::json::parse_json"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"json"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"jsonc-parser"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"confval::format::yaml::parse_yaml"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"yaml"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"saphyr-parser"})})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Each takes a ",(0,t.jsx)(n.code,{children:"SourceMap"}),", a ",(0,t.jsx)(n.code,{children:"SourceId"}),", and a ",(0,t.jsx)(n.code,{children:"&mut Report"}),", and returns your spec as an ",(0,t.jsx)(n.code,{children:"Option"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"let spec: Option<ServerSpec> = confval::format::hcl::parse_hcl(&sources, id, &mut report);\n"})}),"\n",(0,t.jsx)(n.p,{children:"The result is the same whichever format you read.\nValidation and lowering never depend on which frontend ran."}),"\n",(0,t.jsx)(n.h3,{id:"nesting-equivalences",children:"Nesting equivalences"}),"\n",(0,t.jsx)(n.p,{children:"Every format has more than one way to write a nested block.\nBoth forms parse into the same thing."}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Format"}),(0,t.jsx)(n.th,{children:"Block form"}),(0,t.jsx)(n.th,{children:"Object form"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"HCL"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"bind { port = 8080 }"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"bind = { port = 8080 }"})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"TOML"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"[bind]"})," section"]}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"bind = { port = 8080 }"})," inline table"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"KDL"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"bind { port 8080 }"})," children block"]}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"bind port=8080"})," properties on one node"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"JSON"}),(0,t.jsx)(n.td,{children:"(one form only)"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:'"bind": { "port": 8080 }'})})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"YAML"}),(0,t.jsx)(n.td,{children:"block mapping"}),(0,t.jsxs)(n.td,{children:["flow mapping ",(0,t.jsx)(n.code,{children:"bind: { port: 8080 }"})]})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"For example, these two documents fill the same spec:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-hcl",children:'hostname = "127.0.0.1"\nport = 8080\nallow = ["10.0.0.0/8", "192.168.0.0/16"]\n\nbind {\n port = 8080\n}\n'})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-toml",children:'hostname = "127.0.0.1"\nport = 8080\nallow = ["10.0.0.0/8", "192.168.0.0/16"]\n\n[bind]\nport = 8080\n'})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"let spec: Option<ServerSpec> = confval::format::toml::parse_toml(&sources, id, &mut report);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"writing-back-to-text",children:"Writing back to text"}),"\n",(0,t.jsx)(n.p,{children:"Write the model back out with the emitter for the format you want:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"let text = confval::format::hcl::emit_hcl(&spec.to_fields())?;\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The emitter for each format is named ",(0,t.jsx)(n.code,{children:"emit_hcl"}),", ",(0,t.jsx)(n.code,{children:"emit_toml"}),", ",(0,t.jsx)(n.code,{children:"emit_kdl"}),", ",(0,t.jsx)(n.code,{children:"emit_json"}),", or ",(0,t.jsx)(n.code,{children:"emit_yaml"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"single-element-list-coercion",children:"Single-element list coercion"}),"\n",(0,t.jsx)(n.p,{children:"A list field also accepts a single str
1ing as a one-element list, in every format.\nEvery frontend applies the same rule.\nOne configuration reads the same way whichever frontend parsed it."}),"\n",(0,t.jsx)(n.h3,{id:"format-specific-behavior",children:"Format-specific behavior"}),"\n",(0,t.jsx)(n.p,{children:"Each frontend handles edge cases according to the rules of its format.\nThe tables below collect the behaviors you are most likely to encounter."}),"\n",(0,t.jsx)(n.h4,{id:"hcl-and-toml",children:"HCL and TOML"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Behavior"}),(0,t.jsx)(n.th,{children:"Detail"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Duplicate attribute"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"hcl-edit"})," and ",(0,t.jsx)(n.code,{children:"toml_edit"})," reject a duplicate attribute key during parsing. It is a syntax error."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Repeated block"}),(0,t.jsx)(n.td,{children:"Parses. confval reports it with a related span pointing at the first occurrence."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"KDL-style label"}),(0,t.jsxs)(n.td,{children:["HCL supports a native block label: ",(0,t.jsx)(n.code,{children:'upstream "api" { ... }'}),". The label fills the child field the spec marks with ",(0,t.jsx)(n.code,{children:"#[confval(label)]"}),"."]})]})]})]}),"\n",(0,t.jsx)(n.h4,{id:"kdl",children:"KDL"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Behavior"}),(0,t.jsx)(n.th,{children:"Detail"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Grammar"}),(0,t.jsx)(n.td,{children:"KDL 2.0 only."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"List"}),(0,t.jsxs)(n.td,{children:["Repeated arguments on one node (",(0,t.jsx)(n.code,{children:'allow "a" "b"'}),"), or repeated same-named nodes. A bare node is an empty list."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Repeated scalar node"}),(0,t.jsxs)(n.td,{children:["A list when the field is a list. A ",(0,t.jsx)(n.code,{children:"duplicate field"})," error when the field is single-valued."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Block label"}),(0,t.jsxs)(n.td,{children:["A block node's first string argument is its native label (",(0,t.jsx)(n.code,{children:'upstream "api" { ... }'}),"). It fills the child field marked with ",(0,t.jsx)(n.code,{children:"#[confval(label)]"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Non-string label"}),(0,t.jsx)(n.td,{children:"Reported. An argument past the first is also reported."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Bare node where scalar expected"}),(0,t.jsxs)(n.td,{children:["Reports ",(0,t.jsx)(n.code,{children:"expected string, found array"}),", because a bare node means an empty list."]})]})]})]}),"\n",(0,t.jsx)(n.h4,{id:"json",children:"JSON"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Behavior"}),(0,t.jsx)(n.th,{children:"Detail"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Strict mode"}),(0,t.jsx)(n.td,{children:"Strict JSON only. Comments, trailing commas, unquoted property names, missing commas, single-quoted strings, hexadecimal numbers, and numbers with a unary plus are each a syntax error."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Document root"}),(0,t.jsxs)(n.td,{children:["Must be an object. Any other root reports ",(0,t.jsx)(n.code,{children:"expected an object at the document root"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Number classification"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"1"})," is an integer. ",(0,t.jsx)(n.code,{children:"1.0"})," and ",(0,t.jsx)(n.code,{children:"1e3"})," are floats."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Oversized integer"}),(0,t.jsxs)(n.td,{children:["An integer beyond ",(0,t.jsx)(n.code,{children:"i64"})," range reports ",(0,t.jsx)(n.code,{children:"expected integer, found oversized integer"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Oversized number"}),(0,t.jsxs)(n.td,{children:["A magnitude beyond ",(0,t.jsx)(n.code,{children:"f64"})," (such as ",(0,t.jsx)(n.code,{children:"1e999"}),") reports ",(0,t.jsx)(n.code,{children:"expected number, found oversized number"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"null"})}),(0,t.jsxs)(n.td,{children:["Reports ",(0,t.jsx)(n.code,{children:"expected string, found null"}),". Omit the member when you want an optional setting left unset."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Duplicate key"}),(0,t.jsxs)(n.td,{children:["A list when the field is a list. A ",(0,t.jsx)(n.code,{children:"duplicate field"})," error when the field is single-valued."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Type mismatch"}),(0,t.jsxs)(n.td,{children:["A scalar where a nested object is expected reports ",(0,t.jsx)(n.code,{children:"expected block, found string"}),". The expected side of a mismatch is shared across formats."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Comments"}),(0,t.jsxs)(n.td,{children:["JSON has no comment syntax. Emitted JSON carries no comments. ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/templates#generating-a-template",children:"Templates"})," covers what that means for a template."]})]})]})]}),"\n",(0,t.jsx)(n.h4,{id:"yaml",children:"YAML"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Behavior"}),(0,t.jsx)(n.th,{children:"Detail"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Document root"}),(0,t.jsxs)(n.td,{children:["Must be a mapping. Any other root reports ",(0,t.jsx)(n.code,{children:"expected a mapping at the document root"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Empty document"}),(0,t.jsx)(n.td,{children:"An empty file, a whitespace-only file, and a file of comments each parse as a configuration that sets nothing, the way an empty TOML or HCL file does."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Multiple documents"}),(0,t.jsxs)(n.td,{children:["A second document reports ",(0,t.jsx)(n.code,{children:"expected a single document"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Scalar resolution"}),(0,t.jsxs)(n.td,{children:["A plain scalar resolves through the YAML 1.2 core schema. A quoted, literal, or folded scalar is a string whatever its text. ",(0,t.jsx)(n.code,{children:"port: 8080"})," is an integer. ",(0,t.jsx)(n.code,{children:'port: "8080"'})," is a string."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"1.1 literals"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"yes"}),", ",(0,t.jsx)(n.code,{children:"no"}),", ",(0,t.jsx)(n.code,{children:"on"}),", and ",(0,t.jsx)(n.code,{children:"off"})," are not in the 1.2 schema. ",(0,t.jsx)(n.code,{children:"country: no"})," is the string ",(0,t.jsx)(n.code,{children:"no"}),", not a boolean. ",(0,t.jsx)(n.code,{children:"-.nan"}
1),", uppercase or signed base prefixes (",(0,t.jsx)(n.code,{children:"0X1F"}),", ",(0,t.jsx)(n.code,{children:"-0x10"}),"), and underscored numbers (",(0,t.jsx)(n.code,{children:"1_000"}),") are also strings."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Oversized integer"}),(0,t.jsxs)(n.td,{children:["An integer beyond ",(0,t.jsx)(n.code,{children:"i64"})," range reports ",(0,t.jsx)(n.code,{children:"expected integer, found oversized integer"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Oversized number"}),(0,t.jsxs)(n.td,{children:["A decimal that overflows ",(0,t.jsx)(n.code,{children:"f64"})," reports ",(0,t.jsx)(n.code,{children:"expected number, found oversized number"}),". ",(0,t.jsx)(n.code,{children:".inf"})," written by an operator is the only infinity the model holds from YAML."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"null"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"null"}),", ",(0,t.jsx)(n.code,{children:"~"}),", or a key with no value reports ",(0,t.jsx)(n.code,{children:"expected string, found null"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Duplicate key"}),(0,t.jsxs)(n.td,{children:["A list when the field is a list. A ",(0,t.jsx)(n.code,{children:"duplicate field"})," error when the field is single-valued."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Alias"}),(0,t.jsxs)(n.td,{children:["Not expanded. Reports ",(0,t.jsx)(n.code,{children:"expected string, found alias"})," at the alias."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Anchor"}),(0,t.jsx)(n.td,{children:"Read through. The anchored node is ordinary data."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:["Merge key ",(0,t.jsx)(n.code,{children:"<<"})]}),(0,t.jsxs)(n.td,{children:["An ordinary key. A spec that does not declare it reports ",(0,t.jsx)(n.code,{children:"unknown field: <<"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Core schema tags"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"!!str 8080"})," is the string ",(0,t.jsx)(n.code,{children:"8080"}),". The non-specific ",(0,t.jsx)(n.code,{children:"!"})," resolves the same way on a scalar. A core scalar tag whose text it cannot read (",(0,t.jsx)(n.code,{children:"!!int foo"}),"), a core tag on the wrong node kind (",(0,t.jsx)(n.code,{children:"!!int {a: 1}"}),"), and any tag outside the core schema each report ",(0,t.jsx)(n.code,{children:"expected string, found tagged value"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Non-scalar key"}),(0,t.jsxs)(n.td,{children:["A mapping, sequence, or explicit ",(0,t.jsx)(n.code,{children:"? *alias"})," key reports ",(0,t.jsx)(n.code,{children:"expected a scalar key"}),". The entry is skipped so later errors are not hidden. An alias as a plain key (",(0,t.jsx)(n.code,{children:"*a: 1"}),") is a syntax error. A scalar key reads as its text whatever the schema would resolve it to. ",(0,t.jsx)(n.code,{children:"8080:"})," names the field ",(0,t.jsx)(n.code,{children:"8080"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Type mismatch"}),(0,t.jsxs)(n.td,{children:["A scalar where a nested mapping is expected reports ",(0,t.jsx)(n.code,{children:"expected block, found string"}),". The expected side of a mismatch is shared across formats."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Emitted style"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"emit_yaml"})," produces block-style YAML with two-space indentation, values before nested mappings, and a trailing newline. Every string emits double-quoted, so a value the schema would otherwise resolve (",(0,t.jsx)(n.code,{children:"no"}),", ",(0,t.jsx)(n.code,{children:"123"}),", ",(0,t.jsx)(n.code,{children:"null"}),") reads back as the string it was."]})]})]})]}),"\n",(0,t.jsx)(n.admonition,{type:"note",children:(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"hcl-edit"})," rejects duplicate attribute keys while parsing, and TOML rejects a duplicate key the same way.\nA repeated block parses, and confval reports it with a related span pointing at the first occurrence.\nJSON and YAML both permit the same key twice.\nA duplicate key parses and the spec's declared shape decides what it means."]})}),"\n",(0,t.jsx)(n.h2,{id:"writing-parsers-by-hand",children:"Writing parsers by hand"}),"\n",(0,t.jsxs)(n.p,{children:["Sometimes a block's remaining fields depend on the value of a discriminator field.\nThe ",(0,t.jsx)(n.code,{children:"Spec"})," derive cannot express that shape, so you write the parser yourself."]}
1),"\n",(0,t.jsx)(n.p,{children:"By convention, confval reduces a spec's fields to primitive types wherever it can.\nA spec holds the most broadly typed form of a field.\nLowering narrows that value to a more specific type once validation has run."}),"\n",(0,t.jsxs)(n.p,{children:["A discrete set of values follows the same pattern rather than becoming an enum in the spec.\nTake a ",(0,t.jsx)(n.code,{children:"mode"}),' field that accepts "red", "green", or "blue" as an example.\nThe spec holds a ',(0,t.jsx)(n.code,{children:"Located<String>"}),".\nValidation checks it against a ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/validation#keywordset",children:"KeywordSet"}),".\nLowering converts the string to an enum."]}),"\n",(0,t.jsx)(n.p,{children:"Handwritten parsers cover the shapes that pattern cannot express.\nThe clearest case is a block whose remaining fields depend on a discriminator.\nThe parser reads the discriminator first and dispatches on it."}),"\n",(0,t.jsxs)(n.p,{children:["A parser is an implementation of confval's ",(0,t.jsx)(n.code,{children:"FromFields"})," trait:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"pub trait FromFields: Sized {\n fn from_fields(fields: &Fields, report: &mut Report) -> Option<Self>;\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"An implementation parses every field before deciding what to return.\nParsing all of them first keeps one bad field from hiding the problems in the others."}),"\n",(0,t.jsxs)(n.p,{children:["Report each problem as you find it.\nThen return ",(0,t.jsx)(n.code,{children:"None"})," when a field failed and the value cannot be built.\nThe ",(0,t.jsx)(n.code,{children:"None"})," itself carries no reason.\nWhatever explains the failure must already be in the report."]}),"\n",(0,t.jsxs)(n.p,{children:["A ",(0,t.jsx)(n.code,{children:"Fields"})," built by ",(0,t.jsx)(n.code,{children:"to_template"})," can also hold commented-out entries, fields whose ",(0,t.jsx)(n.code,{children:"commented"})," flag is set.\nSuch a field reads as absent.\n",(0,t.jsx)(n.code,{children:"Fields::get"})," and ",(0,t.jsx)(n.code,{children:"Fields::has"})," skip one for you.\nIf you iterate with ",(0,t.jsx)(n.code,{children:"Fields::iter"}),", check the flag and skip the field the way the generated walk does."]}),"\n",(0,t.jsx)(n.h3,{id:"the-field-model",children:"The field model"}),"\n",(0,t.jsxs)(n.p,{children:["A handwritten parser reads ",(0,t.jsx)(n.code,{children:"Fields"}),", confval's format-neutral view of one level of structure.\nA frontend builds it, and from there nothing knows which format the text was."]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Role"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Fields"})}),(0,t.jsx)(n.td,{children:"One level: the named entries of a body, table, or inline object, plus the span a missing-field error points at."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Field"})}),(0,t.jsxs)(n.td,{children:["One entry: its name, the span of the name, the span of the whole entry, and a ",(0,t.jsx)(n.code,{children:"FieldKind"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"FieldKind"})}),(0,t.jsxs)(n.td,{children:["Either ",(0,t.jsx)(n.code,{children:"Value"})," for an attribute (",(0,t.jsx)(n.code,{children:"name = value"}),") or ",(0,t.jsx)(n.code,{children:"Block"})," for a block (",(0,t.jsx)(n.code,{children:"name { ... }"})," in HCL, ",(0,t.jsx)(n.code,{children:"[name]"}),' in TOML). The split lets a diagnostic say "found block" rather than "found object".']})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Value"})}),(0,t.jsxs)(n.td,{children:["A span plus a ",(0,t.jsx)(n.code,{children:"ValueKind"}),": a ",(0,t.jsx)(n.code,{children:"Scalar"}),", a ",(0,t.jsx)(n.code,{children:"Seq"})," (a list), a ",(0,t.jsx)(n.code,{children:"Map"})," (nested fields), or ",(0,t.jsx)(n.code,{children:"Other"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Scalar"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"String"}),", ",(0,t.jsx)(n.code,{children:"Int(i64)"}),", ",(0,t.jsx)(n.code,{children:"Float(f64)"}),", ",(0,t.jsx)(n.code,{children:"Bool"}),", or ",(0,t.jsx)(n.code,{children:"Unparsed"}),". Integers and floats stay distinct so a format that separates them (like TOML's ",(0,t.jsx)(n.code,{children:"1"})," and ",(0,t.jsx)(n.code,{children:"1.0"}),") round-trips faithfully."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Unparsed(String)"})}),(0,t.jsxs)(n.td,{children:["The raw text of a value from a source that only carries strings, such as an environment variable or a command line flag. The leaf parsers coerce it to the type they expect. No file frontend produces it. A quoted string in a file stays a ",(0,t.jsx)(n.code,{children:"String"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Other(label)"})}),(0,t.jsxs)(n.td,{children:["A value that exists in the file but falls outside the model, such as an HCL template or a TOML datetime. It surfaces as a type mismatch named by the label. For example, ",(0,t.jsx)(n.code,{children:"expected string, found datetime"}),". ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/format-limitations",children:"Format Limitations"})," lists every one."]})]})]})]}),"\n",(0,t.jsx)(n.h3,{id:"helpers",children:"Helpers"}),"\n",(0,t.jsx)(n.p,{children:"confval ships helpers so a handwritten parser reports the same way the derive does."}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Leaf parsers"})," turn one ",(0,t.jsx)(n.code,{children:"Field"})," into a ",(0,t.jsx)(n.code,{children:"Located"})," value, reporting a typed error on mismatch:"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Helper"}),(0,t.jsx)(n.th,{children:"Parses"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_string_field"})}),(0,t.jsx)(n.td,{children:"a string"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_int_field"})}),(0,t.jsxs)(n.td,{children:["an ",(0,t.jsx)(n.code,{children:"i64"})]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_float_field"})}),(0,t.jsxs)(n.td,{children:["an ",(0,t.jsx)(n.code,{children:"f64"})]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_bool_field"})}),(0,t.jsx)(n.td,{children:"a boolean"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_path_field"})}),(0,t.jsxs)(n.td,{children:["a ",(0,t.jsx)(n.code,{children:"PathBuf"})]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_string_list_field"})}
1),(0,t.jsx)(n.td,{children:"an array of strings"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Structural parsers"})," recurse through ",(0,t.jsx)(n.code,{children:"FromFields"}),":"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Helper"}),(0,t.jsx)(n.th,{children:"Parses"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_struct_field"})}),(0,t.jsx)(n.td,{children:"one nested struct from a block or a map value"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_single_struct"})}),(0,t.jsxs)(n.td,{children:["same as ",(0,t.jsx)(n.code,{children:"parse_struct_field"}),", with duplicate reporting"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_struct_list_field"})}),(0,t.jsxs)(n.td,{children:["repeated blocks or a sequence of maps, into a ",(0,t.jsx)(n.code,{children:"Vec"})]})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Occurrence helpers"})," decide what a repeated field means:"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Helper"}),(0,t.jsx)(n.th,{children:"Behavior"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"first_occurrence"})}),(0,t.jsx)(n.td,{children:"Records the first occurrence of a leaf field. Reports a later one as a duplicate."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_single_struct"})}),(0,t.jsx)(n.td,{children:"The same guard around a nested block."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"parse_string_list_occurrence"})}),(0,t.jsx)(n.td,{children:"Accumulates a list field's occurrences into one list, in document order."})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["The derive wraps every leaf arm it generates in ",(0,t.jsx)(n.code,{children:"first_occurrence"}),".\nA derived spec therefore reports a repeated field.\nA handwritten parser that assigns its slot directly takes the last value instead, with no diagnostic."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Reporting helpers"})," keep messages uniform: ",(0,t.jsx)(n.code,{children:"report_unknown_field"}),", ",(0,t.jsx)(n.code,{children:"report_missing_field"}),", ",(0,t.jsx)(n.code,{children:"report_duplicate_field"}),".\nUnknown fields are always errors.\nThere is no lenient mode."]}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"handwritten"})," example calls these helpers against a tagged enum and a handwritten root.\nThe test at ",(0,t.jsx)(n.code,{children:"crates/confval/tests/handwritten_parity.rs"})," writes one spec both ways and asserts that the two write walks render the same text and agree on every span."]}),"\n",(0,t.jsx)(n.h2,{id:"writing-emitters-by-hand",children:"Writing emitters by hand"}),"\n",(0,t.jsxs)(n.p,{children:["A type with a handwritten ",(0,t.jsx)(n.code,{children:"FromFields"})," needs a handwritten ",(0,t.jsx)(n.code,{children:"ToFields"})," too, because the derive generates one only for the types it parses.\n",(0,t.jsx)(n.code,{children:"ToFields"})," has two required walks.\n",(0,t.jsx)(n.code,{children:"to_fields"})," emits every field with its span detached.\nThis is the populated view and the template.\n",(0,t.jsx)(n.code,{children:"to_source_fields"})," emits only the fields the source set, keeps their spans, and recurses into children with ",(0,t.jsx)(n.code,{children:"to_source_fields"})," rather than ",(0,t.jsx)(n.code,{children:"to_fields"}),".\nThat output is the ",(0,t.jsx)(n.a,{href:"/confval/docs/0.11.0/guide/representations",children:"source view"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Writing both by hand means writing the field list twice and reproducing that difference on every line.\n",(0,t.jsx)(n.code,{children:"FieldsBuilder"})," takes the walk as a parameter instead, so you list the fields once:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:'use confval::format::{Fields, FieldsBuilder, ToFields, Walk};
1\n\nimpl Server {\n fn build(&self, walk: Walk) -> Fields {\n FieldsBuilder::new(walk)\n .leaf("hostname", &self.hostname)\n .leaf_opt("port", self.port.as_ref())\n .string_list("tags", &self.tags)\n .block("limits", &self.limits)\n .finish()\n }\n}\n\nimpl ToFields for Server {\n fn to_fields(&self) -> Fields {\n self.build(Walk::Populated)\n }\n\n fn to_source_fields(&self) -> Fields {\n self.build(Walk::Source)\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Each method takes the ",(0,t.jsx)(n.code,{children:"Located"})," rather than the value inside it, so the builder has the span each walk needs."]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Method"}),(0,t.jsx)(n.th,{children:(0,t.jsx)(n.code,{children:"Walk::Populated"})}),(0,t.jsx)(n.th,{children:(0,t.jsx)(n.code,{children:"Walk::Source"})})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"leaf"}),", ",(0,t.jsx)(n.code,{children:"leaf_opt"})]}),(0,t.jsx)(n.td,{children:"emits detached"}),(0,t.jsx)(n.td,{children:"emits with its span, or omits a detached one"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string_list"})}),(0,t.jsx)(n.td,{children:"emits every element detached"}),(0,t.jsx)(n.td,{children:"emits the elements that carry a span, or omits the field"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string_list_opt"})}),(0,t.jsx)(n.td,{children:"emits detached when present"}),(0,t.jsx)(n.td,{children:"emits when the wrapper carries a span, elements included"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"block"}),", ",(0,t.jsx)(n.code,{children:"block_opt"}),", ",(0,t.jsx)(n.code,{children:"block_list"})]}),(0,t.jsxs)(n.td,{children:["recurses with ",(0,t.jsx)(n.code,{children:"to_fields"})]}),(0,t.jsxs)(n.td,{children:["recurses with ",(0,t.jsx)(n.code,{children:"to_source_fields"})," when the block carries a span"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"block_opt_default"})}),(0,t.jsxs)(n.td,{children:["fills an absent block from ",(0,t.jsx)(n.code,{children:"S::default()"})]}),(0,t.jsx)(n.td,{children:"omits an absent block"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"literal_string"})}),(0,t.jsx)(n.td,{children:"emits detached"}),(0,t.jsx)(n.td,{children:"emits detached"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"block_opt_default"})," is the counterpart of ",(0,t.jsx)(n.code,{children:"#[confval(nested, default)]"}),".\nIts populated walk shows the values the program will run with even for a block the operator never wrote.\nUse ",(0,t.jsx)(n.code,{children:"block_opt"})," for an optional block with no default.\nBoth walks omit an absent block."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"leaf"})," accepts the same types a derived spec field accepts, through the sealed ",(0,t.jsx)(n.code,{children:"Leaf"})," trait: ",(0,t.jsx)(n.code,{children:"String"}),", ",(0,t.jsx)(n.code,{children:"i64"}),", ",(0,t.jsx)(n.code,{children:"f64"}),", ",(0,t.jsx)(n.code,{children:"bool"}),", and ",(0,t.jsx)(n.code,{children:"PathBuf"}),".\nNo crate outside confval implements it, so the list can grow in a minor release.\nA path emits as a string, the one lossy conversion, matching what the derive generates."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"literal_string"})," is for a field your impl supplies rather than reads.\nThat field is the discriminator on a tagged enum:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:'match self {\n TlsSpec::Manual { cert, key } => builder\n .literal_string("mode", "manual")\n .leaf("cert", cert)\n .leaf("key", key),\n TlsSpec::Acme { domains } => builder\n .literal_string("mode", "acme")\n .string_list("domains", domains),\n};\n'})}),"\n",(0,t.jsx)(n.p,{children:"Both walks emit it, because a source view that dropped the tag would not reparse."}),"\n",(0,t.jsxs)(n.p,{children:["The builder does not cover every shape a spec can hold.\nA string-keyed map has a derive form, ",(0,t.jsx)(n.code,{children:"#[confval(map)]"}),", so it needs no handwritten walk.\nThe builder has no method for one.\nBuild such a field directly with ",(0,t.jsx)(n.code,{children:"Field::detached_value"})," or ",(0,t.jsx)(n.code,{children:"Field::detached_block"}),".\nLocate it with ",(0,t.jsx)(n.code,{children:"at"})," when it carries a span, then ",(0,t.jsx)(n.code,{children:"push"})," it into the builder:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-rust",children:"let field = Field::detached_value(name, value).at(span);\nbuilder.push(field);\n"})}),"\n",(0,t.jsx)(n.p,{children:"The walk does not reach a pushed field.\nYou decide what it carries.\nOn a source walk that includes deciding whether the source set it.\nThe builder still shapes every other field of the type."}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"at"})," sets the field's span and its source.\nAn attribute's value takes the same span.\nA block's nested level keeps its own source and enclosing span.\nA sequence's elements keep the spans they were built with."]}),"\n",(0,t.jsxs)(n.p,{children:["A handwritten spec type also implements ",(0,t.jsx)(n.code,{children:"Validate"})," and ",(0,t.jsx)(n.code,{children:"ValidateNested"}),".\n",(0,t.jsx)(n.code,{children:"Validate"})," holds its rules.\n",(0,t.jsx)(n.code,{children:"ValidateNested"}
1)," holds the descent into its children, the traversal the derive would have written from the struct definition.\nThe ",(0,t.jsx)(n.code,{children:"Self: ValidateNested"})," bound on ",(0,t.jsx)(n.code,{children:"validate_all"})," makes omitting the traversal a compile error rather than a silently skipped subtree.\nA type in a required nested slot also needs ",(0,t.jsx)(n.code,{children:"Default"}),", because the generated parser fills an absent block with it before reporting the block missing."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"to_template"})," defaults to ",(0,t.jsx)(n.code,{children:"to_fields"})," for a handwritten impl.\nThat fallback recurses with ",(0,t.jsx)(n.code,{children:"to_fields"}),", so doc comments stop at the first handwritten node and never reach anything below it.\nA derived block nested under a handwritten one renders without its comments.\nThe ",(0,t.jsx)(n.code,{children:"handwritten"})," example prints both sides of that boundary."]})]})}function h(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(o,{...e})}):o(e)}},28453(e,n,s){s.d(n,{R:()=>i,x:()=>l});var d=s(96540);let t={},r=d.createContext(t);function i(e){let n=d.useContext(r);return d.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),d.createElement(r.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.