//! The configuration reference on the documentation site describes every //! field of the configuration's schema. //! //! The schema is `proto/config.proto`; the reference is //! `docs/content/docs/reference/configuration.md`, with a section for each //! message and enum (`## Peer`) that names each of its fields or values in //! backquotes. A field added to the schema and not to the reference fails //! here, so that operators can find every setting the daemon has. use std::collections::BTreeMap; use std::path::Path; /// The messages and enums of a `.proto` file, each with its fields or /// values, in the order they are declared. fn declarations(proto: &str) -> BTreeMap> { let mut found = BTreeMap::new(); let mut current: Option<(String, Vec)> = None; for line in proto.lines() { let line = line.trim(); if line.starts_with("//") || line.is_empty() { continue; } let words: Vec<&str> = line.split_whitespace().collect(); match words.as_slice() { ["message" | "enum", name, "{"] => current = Some((name.to_string(), Vec::new())), ["}"] => { if let Some((name, members)) = current.take() { found.insert(name, members); } } // `repeated Peer peers = 4;`, `string name = 1;`, `PEER = 3;` _ => { let Some((_, members)) = current.as_mut() else { continue; }; let Some(at) = words.iter().position(|word| *word == "=") else { continue; }; let name = words[at - 1]; // An enum's zero value is what is left out; there is // nothing to configure. if !name.ends_with("_UNSPECIFIED") { members.push(name.to_string()); } } } } found } /// The text of each `## ` section of a Markdown page, by its heading. fn sections(page: &str) -> BTreeMap { let mut found = BTreeMap::new(); let mut current: Option<(String, String)> = None; for line in page.lines() { if let Some(heading) = line.strip_prefix("## ") { if let Some((name, text)) = current.take() { found.insert(name, text); } current = Some((heading.trim().to_string(), String::new())); } else if let Some((_, text)) = current.as_mut() { text.push_str(line); text.push('\n'); } } if let Some((name, text)) = current { found.insert(name, text); } found } #[test] fn the_configuration_reference_has_every_field() { let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let proto = std::fs::read_to_string(crate_dir.join("proto/config.proto")).unwrap(); let reference_path = crate_dir.join("../../docs/content/docs/reference/configuration.md"); let reference = std::fs::read_to_string(&reference_path).unwrap(); let declared = declarations(&proto); assert!(declared.contains_key("BgpConfig") && declared.contains_key("Peer")); let documented = sections(&reference); let mut missing = Vec::new(); for (name, members) in &declared { let Some(section) = documented.get(name) else { missing.push(format!("a section `## {name}`")); continue; }; for member in members { if !section.contains(&format!("`{member}`")) { missing.push(format!("`{member}` under `## {name}`")); } } } assert!( missing.is_empty(), "{} does not describe:\n {}", reference_path.display(), missing.join("\n ") ); }