Make the crate do what its documentation already claimed
Review from ai-app's session, acted on. **A test that asserted nothing.** `a_missing_tunnel_explains_itself` returned early whenever `wg0` was up -- which it is on this VM and on the host, so it passed everywhere and never once checked the message. The same shape of test that let the file-permissions bug survive three codebases. `netif` now splits the lookup from the decision: `wg_address_among` and `addresses_among` take the interfaces, so the failure is reachable by handing in an empty list rather than by hoping the machine has no tunnel. Four tests where there were two, including that an IPv6-only `wg0` is not an answer. **Two modules the docs promised and the crate did not have.** `certs`, which is the piece where being written twice is worst -- a trust anchor built two ways can be built differently two ways, and the difference reaches a phone as an opaque handshake failure. And `format`, the RON house rules, which were byte-identical in both projects and so the clearest thing in the evidence table. `product` names the certificate and is the whole of what is per-project; the test decodes the DER and looks for it there rather than trusting what was passed in. **The README title still said wg-server-app**, three commits after everything else was renamed. Two judgement calls promoted from silent to written down, both of which would otherwise be inherited rather than chosen: `WG_INTERFACE` is a constant because *these* projects have one tunnel, which is the first thing a third user should expect to change; and `local_addresses` puts the emulator's host alias in every certificate, a SAN for an address the machine does not own. And a review heuristic the day kept proving: when you find a rule stated, grep for its siblings. Three bugs today were the correct rule already written down and applied to one member of a set.
This commit is contained in:
1 parent
592114bfc9
commit
841a3a8372
7 files changed
+1055
-40
No files matched your search
@@ -0,0 +1,156 @@
|
||||
//! Reading and writing the RON both projects' config files are in.
|
||||
//!
|
||||
//! Two things are house rules rather than plain RON, and they are here
|
||||
//! together because they are inverses of each other -- change one and the
|
||||
//! other stops round-tripping. Both projects had this, identically, to the
|
||||
//! byte; that is what made it the first thing worth sharing.
|
||||
//!
|
||||
//! **No outer parentheses.** A file *is* the body of the struct, so
|
||||
//! nothing in it is indented for the sake of a wrapper. RON has no
|
||||
//! implicit top-level struct (`de/mod.rs` requires the `(`), so [`parse`]
|
||||
//! adds it and [`render`] takes it back off. The opening paren is not
|
||||
//! followed by a newline, so a parse error's line number still points at
|
||||
//! the real line.
|
||||
//!
|
||||
//! **`Some` is implicit.** Enabled on the deserializer rather than by a
|
||||
//! `#![enable(implicit_some)]` header every file would have to remember,
|
||||
//! and matched on the writing side by `skip_serializing_if` so nothing
|
||||
//! writes back a `Some(...)` a person didn't type. The two halves only
|
||||
//! round-trip together, which is why a caller's own tests should assert
|
||||
//! the written shape rather than only that it loads.
|
||||
|
||||
/// Reading and writing the RON these files are in.
|
||||
///
|
||||
/// Two things are house rules rather than plain RON, and they are here
|
||||
/// together because they are inverses of each other -- change one and the
|
||||
/// other stops round-tripping.
|
||||
///
|
||||
/// **No outer parentheses.** A file *is* the body of the struct, so nothing
|
||||
/// in it is indented for the sake of a wrapper. RON has no implicit
|
||||
/// top-level struct (`de/mod.rs` requires the `(`), so [`format::parse`]
|
||||
/// adds it and [`format::render`] takes it back off. The opening paren is not followed by a
|
||||
/// newline, so a parse error's line number still points at the real line.
|
||||
///
|
||||
/// **`Some` is implicit.** Enabled on the deserializer rather than by a
|
||||
/// `#![enable(implicit_some)]` header every project file would have to
|
||||
/// remember, and matched on the writing side by `skip_serializing_if` so
|
||||
/// nothing writes back a `Some(...)` a person didn't type.
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
fn options() -> ron::Options {
|
||||
ron::Options::default().with_default_extension(ron::extensions::Extensions::IMPLICIT_SOME)
|
||||
}
|
||||
|
||||
pub fn parse<T: DeserializeOwned>(text: &str) -> Result<T, ron::error::SpannedError> {
|
||||
options().from_str(&format!("({text})"))
|
||||
}
|
||||
|
||||
pub fn render<T: Serialize>(value: &T) -> Result<String, ron::Error> {
|
||||
let pretty = ron::ser::PrettyConfig::new();
|
||||
let text = options().to_string_pretty(value, pretty)?;
|
||||
Ok(unwrap_outer(&text))
|
||||
}
|
||||
|
||||
/// Strips the outer `(`/`)` the writer always emits and removes the
|
||||
/// indent level they cost. Deliberately narrow: it accepts only the
|
||||
/// exact shape `PrettyConfig` produces, and leaves anything else alone
|
||||
/// rather than guessing -- a file with stray parentheses is better than
|
||||
/// one silently mangled. `parse` round-trips either way, since a
|
||||
/// wrapped body parses the same as an unwrapped one re-wrapped.
|
||||
fn unwrap_outer(text: &str) -> String {
|
||||
let Some(body) = text
|
||||
.strip_prefix("(\n")
|
||||
.and_then(|rest| rest.strip_suffix("\n)"))
|
||||
else {
|
||||
return text.to_string();
|
||||
};
|
||||
let mut out: String = body
|
||||
.lines()
|
||||
.map(|line| line.strip_prefix(" ").unwrap_or(line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
out.push('\n');
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
struct Demo {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
/// The house rule both halves depend on: what is written is the *body*
|
||||
/// of the struct, with no outer parentheses and nothing indented for
|
||||
/// them. Asserted rather than trusted, because `render` strips what
|
||||
/// `parse` adds -- if only one ever changed, every file on disk would
|
||||
/// still load and only look wrong.
|
||||
#[test]
|
||||
fn a_file_is_the_body_of_the_struct() {
|
||||
let written = render(&Demo {
|
||||
name: "thing".to_string(),
|
||||
note: Some("why".to_string()),
|
||||
})
|
||||
.expect("render");
|
||||
|
||||
assert!(
|
||||
!written.trim_start().starts_with('('),
|
||||
"outer parens: {written}"
|
||||
);
|
||||
assert!(
|
||||
written.starts_with("name: "),
|
||||
"top level sits at column 0: {written}"
|
||||
);
|
||||
assert_eq!(
|
||||
parse::<Demo>(&written).expect("re-read").name,
|
||||
"thing",
|
||||
"what is written must read back",
|
||||
);
|
||||
}
|
||||
|
||||
/// An optional value is written as itself, never wrapped -- and a
|
||||
/// value nobody set is not written at all, so a file stays readable as
|
||||
/// what was actually chosen.
|
||||
#[test]
|
||||
fn an_optional_value_is_written_as_itself_or_not_at_all() {
|
||||
let with = render(&Demo {
|
||||
name: "a".to_string(),
|
||||
note: Some("b".to_string()),
|
||||
})
|
||||
.expect("render");
|
||||
assert!(
|
||||
with.contains(r#"note: "b""#),
|
||||
"no Some(...) wrapper: {with}"
|
||||
);
|
||||
|
||||
let without = render(&Demo::default()).expect("render");
|
||||
assert!(
|
||||
!without.contains("note"),
|
||||
"an unset value writes nothing: {without}"
|
||||
);
|
||||
|
||||
// And the bare form reads back, which is the other half.
|
||||
assert_eq!(
|
||||
parse::<Demo>("name: \"a\",\nnote: \"b\",\n")
|
||||
.expect("parse")
|
||||
.note
|
||||
.as_deref(),
|
||||
Some("b")
|
||||
);
|
||||
}
|
||||
|
||||
/// A parse error's line number has to point at the real line, which is
|
||||
/// why the opening paren is not followed by a newline.
|
||||
#[test]
|
||||
fn a_parse_error_points_at_the_line_it_is_on() {
|
||||
let err = parse::<Demo>("name: \"a\",\nnote: ,\n").expect_err("malformed");
|
||||
assert_eq!(err.span.start.line, 2, "{err}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user