//! 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(text: &str) -> Result { options().from_str(&format!("({text})")) } pub fn render(value: &T) -> Result { 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::>() .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, } /// 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::(&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::("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::("name: \"a\",\nnote: ,\n").expect_err("malformed"); assert_eq!(err.span.start.line, 2, "{err}"); } }