From df069d03372b9574c20a2a6e408060743515aec1 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Wed, 23 Jul 2025 16:17:53 -0700 Subject: [PATCH] basic test suite for monologue parsing --- Cargo.lock | 9 +- Cargo.toml | 1 + src/bin/trees/mono.rs | 225 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 207 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f1a78a..ee050b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2226,7 +2226,8 @@ dependencies = [ "bevy", "bevy_rapier3d", "chrono", - "itertools 0.13.0", + "indoc", + "itertools 0.14.0", "lipsum", "rand", "serde", @@ -2583,6 +2584,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "inflections" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index fac453a..422c7bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ features = ["wayland", "dynamic_linking"] lipsum = "*" rand = "*" itertools = "*" +indoc = "*" [build-dependencies] walkdir = "*" diff --git a/src/bin/trees/mono.rs b/src/bin/trees/mono.rs index bc23836..46d776f 100644 --- a/src/bin/trees/mono.rs +++ b/src/bin/trees/mono.rs @@ -1,7 +1,7 @@ use super::*; /// A monologue containing a list of optional lines -#[derive(Asset, TypePath, Debug, Deserialize, Default, Clone)] +#[derive(Asset, TypePath, Debug, Deserialize, Default, Clone, PartialEq)] pub(crate) struct Monologue { pub batches: Vec, } @@ -16,8 +16,49 @@ impl Monologue { } } + +#[derive(Debug, Error)] +pub struct MonologueParseError; + +impl std::fmt::Display for MonologueParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Could not parse monologue file") + } +} + +impl TryFrom<&str> for Monologue { + type Error = MonologueParseError; + + fn try_from(value: &str) -> Result { + // Create a blank monologue to populate + let mut monologue = Monologue::default(); + + // Add an initial batch that may end up being empty + monologue.add_batch(MonologueLineBatch::default()); + + // Iterate over raw string lines in the .mono file + for line in value.lines() { + // Break up into batches by --- separator + if line.starts_with("---") { + monologue.add_batch(MonologueLineBatch::default()); + // Skip any empty lines or comments + } else if line.starts_with("#") || line.is_empty() { + // Skip comments and blank lines + // everything else we read as a monologue line + } else { + monologue.batches.last_mut().unwrap().add_line(line.into()); + } + } + + // Clear empty batches + monologue.batches.retain(|batch| !batch.lines.is_empty()); + + Ok(monologue) + } +} + /// A set of possible lines in a monologue -#[derive(Debug, Deserialize, Default, Clone)] +#[derive(Debug, Deserialize, Default, Clone, PartialEq)] pub(crate) struct MonologueLineBatch { pub lines: Vec, } @@ -29,7 +70,7 @@ impl MonologueLineBatch { } /// A single monologue line -#[derive(Debug, Deserialize, Default, Clone)] +#[derive(Debug, Deserialize, Default, Clone, PartialEq)] pub(crate) struct MonologueLine { pub value: String, } @@ -60,6 +101,156 @@ impl From<&str> for MonologueLine { } } +#[cfg(test)] +mod tests { + use super::*; + use indoc::*; + + #[test] + fn empty_monologue() { + const MONO: &str = ""; + + let parsed: Monologue = MONO.try_into().unwrap(); + + let expected = Monologue::default(); + + assert_eq!(parsed, expected); + } + + #[test] + fn complicated_empty_monologue() { + const MONO: &str = indoc! {" + --- + --- + # a comment + --- + --- # more stuff + # another comment + --- + + --- + "}; + + let parsed: Monologue = MONO.try_into().unwrap(); + + let expected = Monologue::default(); + + assert_eq!(parsed, expected); + } + + #[test] + fn basic_monologue() { + const MONO: &str = indoc! {" + --- + hello + --- + world + --- + "}; + + let parsed: Monologue = MONO.try_into().unwrap(); + + let expected = Monologue { + batches: vec![ + MonologueLineBatch { + lines: vec![ + "hello".into() + ], + }, + MonologueLineBatch { + lines: vec![ + "world".into() + ] + } + ] + }; + + assert_eq!(parsed, expected); + } + + #[test] + fn basic_with_comments_monologue() { + const MONO: &str = indoc! {" + # Some stuff before we get started + --- + # I really like this line + hello + --- + # I'm skeptical about this one... + world + # This line needs work + --- + # More notes after the lines + "}; + + let parsed: Monologue = MONO.try_into().unwrap(); + + let expected = Monologue { + batches: vec![ + MonologueLineBatch { + lines: vec![ + "hello".into() + ], + }, + MonologueLineBatch { + lines: vec![ + "world".into() + ] + } + ] + }; + + assert_eq!(parsed, expected); + } + + #[test] + fn batches_monologue() { + const MONO: &str = indoc! {" + a + + b + + c + --- + + d + + e + + --- + + # with comment before... + f + # ...and after + "}; + + let parsed: Monologue = MONO.try_into().unwrap(); + + let expected = Monologue { + batches: vec![ + MonologueLineBatch { + lines: vec![ + "a".into(), "b".into(), "c".into() + ] + }, + MonologueLineBatch { + lines: vec![ + "d".into(), "e".into() + ] + }, + MonologueLineBatch { + lines: vec![ + "f".into() + ] + }, + ] + }; + + assert_eq!(parsed, expected); + } +} + + #[derive(Default)] struct MonologueLoader; @@ -69,6 +260,9 @@ enum MonologueLoaderError { Io(#[from] std::io::Error), #[error("Could not parse utf8")] Utf8(#[from] std::string::FromUtf8Error), + // TODO: Real errors + #[error("Could not as monologue")] + Parse(#[from] MonologueParseError), } impl AssetLoader for MonologueLoader { @@ -86,30 +280,7 @@ impl AssetLoader for MonologueLoader { let raw_string = String::from_utf8(bytes)?; - // Create a blank monologue to populate - let mut monologue = Monologue::default(); - - // Add an initial batch that may end up being empty - monologue.add_batch(MonologueLineBatch::default()); - - // Iterate over raw string lines in the .mono file - for line in raw_string.lines() { - // Break up into batches by --- separator - if line.starts_with("---") { - monologue.add_batch(MonologueLineBatch::default()); - // Skip any empty lines or comments - } else if line.starts_with("#") || line.is_empty() { - // Skip comments and blank lines - // everything else we read as a monologue line - } else { - monologue.batches.last_mut().unwrap().add_line(line.into()); - } - } - - // Clear empty batches - monologue.batches.retain(|batch| !batch.lines.is_empty()); - - Ok(monologue) + Ok(Monologue::try_from(raw_string.as_str())?) } fn extensions(&self) -> &[&str] {