From 1e55d46966f8d2908a7809b306d0a87d88e1bc2d Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 10 Nov 2021 01:02:04 +0100 Subject: implement support for multiline descriptions Key-Value-Stores are not always as easy as every line containing a key-value pair, there is also the variant that a value spans multiple lines and is enclosed in """ (e.g. the description of some mods). This also changes the API slightly, as we want to separate the parsing code from the code that reads the file. This makes it easier to do testing or to re-use the code for data that is already in-memory. Therefore, kvstore::write now takes a writer and kvstore::save provides the functionality that the old kvstore::write had. --- Cargo.lock | 16 +++++ modderbaas/Cargo.toml | 3 + modderbaas/src/error.rs | 3 + modderbaas/src/kvstore.rs | 173 ++++++++++++++++++++++++++++++++++++++++++---- modderbaas/src/world.rs | 2 +- 5 files changed, 182 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0140ae6..86e8fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indoc" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136" +dependencies = [ + "unindent", +] + [[package]] name = "instant" version = "0.1.12" @@ -473,6 +482,7 @@ name = "modderbaas" version = "0.1.0" dependencies = [ "dirs", + "indoc", "itertools", "log", "nix", @@ -1187,6 +1197,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/modderbaas/Cargo.toml b/modderbaas/Cargo.toml index c07ba82..2c2918c 100644 --- a/modderbaas/Cargo.toml +++ b/modderbaas/Cargo.toml @@ -23,3 +23,6 @@ zip = "0.5.13" [target.'cfg(unix)'.dependencies] nix = "0.23.0" + +[dev-dependencies] +indoc = "1.0.3" diff --git a/modderbaas/src/error.rs b/modderbaas/src/error.rs index 670496c..df78c2f 100644 --- a/modderbaas/src/error.rs +++ b/modderbaas/src/error.rs @@ -44,6 +44,9 @@ pub enum Error { /// ContentDB returned more than one fitting mod for the query. #[error("the mod ID '{0}' does not point to a single mod")] AmbiguousModId(String), + /// The Key-Value store has malformed content. + #[error("syntax error in the metadata")] + MalformedKVStore, /// Wrapper for HTTP errors. #[error("underlying HTTP error")] diff --git a/modderbaas/src/kvstore.rs b/modderbaas/src/kvstore.rs index 006bb94..ee02e2f 100644 --- a/modderbaas/src/kvstore.rs +++ b/modderbaas/src/kvstore.rs @@ -7,21 +7,47 @@ //! side. use std::{collections::HashMap, fs, io::Write, path::Path}; -use super::error::Result; +use super::error::{Error, Result}; /// Read the given file as a KVStore. pub fn read>(path: P) -> Result> { - read_inner(path.as_ref()) + let content = fs::read_to_string(path)?; + parse_kv(&content) } -fn read_inner(path: &Path) -> Result> { - let content = fs::read_to_string(path)?; +/// Parse the given data as a KVStore. +pub fn parse_kv(content: &str) -> Result> { + let mut result = HashMap::new(); + let mut current_pair: Option<(String, String)> = None; + for line in content.lines() { + if let Some((current_key, mut current_value)) = current_pair.take() { + if line == r#"""""# { + result.insert(current_key, current_value.trim().into()); + } else { + current_value.push_str(line); + current_value.push('\n'); + current_pair = Some((current_key, current_value)); + } + } else if line.is_empty() { + // Skip empty lines outside of long strings + } else if let [key, value] = line + .splitn(2, '=') + .map(str::trim) + .collect::>() + .as_slice() + { + let (key, value): (String, String) = (String::from(*key), String::from(*value)); + if value == r#"""""# { + current_pair = Some((key, String::new())); + } else { + result.insert(key, value); + } + } else { + return Err(Error::MalformedKVStore); + } + } - Ok(content - .lines() - .map(|line| line.splitn(2, '=').map(str::trim).collect::>()) - .map(|v| (v[0].into(), v[1].into())) - .collect()) + Ok(result) } /// Write the given KVStore back to the file. @@ -32,18 +58,137 @@ fn read_inner(path: &Path) -> Result> { /// 2. All remaining options are saved in alphabetical order. /// /// Note that this function will **override** existing files! -pub fn write>(data: &HashMap, path: P) -> Result<()> { - write_inner(data, path.as_ref()) +pub fn save>(data: &HashMap, path: P) -> Result<()> { + let output = fs::File::create(path)?; + write(data, output) } -fn write_inner(data: &HashMap, path: &Path) -> Result<()> { +/// Write the given KVStore to a [`Write`]r. +/// +/// See [`save`] for information about the key ordering. +pub fn write(data: &HashMap, mut writer: W) -> Result<()> { let mut items = data.iter().collect::>(); items.sort_by_key(|i| (if i.0.starts_with("load_mod_") { 1 } else { 0 }, i.0)); - let mut output = fs::File::create(path)?; for (key, value) in items { - writeln!(output, "{} = {}", key, value)?; + if !value.contains('\n') { + writeln!(writer, "{} = {}", key, value)?; + } else { + writeln!(writer, r#"{0} = """{1}{2}{1}""""#, key, '\n', value)?; + } } Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + + use indoc::indoc; + + #[test] + fn test_read() { + let text = indoc! {r#" + name = climate_api + title = Climate API + author = TestificateMods + release = 10001 + optional_depends = player_monoids, playerphysics, pova + description = """ + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience. + """ + "#}; + let kvstore = parse_kv(text).unwrap(); + let wanted_store = [ + ("name", "climate_api"), + ("title", "Climate API"), + ("author", "TestificateMods"), + ("release", "10001"), + ("optional_depends", "player_monoids, playerphysics, pova"), + ( + "description", + indoc! {" + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience.\ + "}, + ), + ] + .iter() + .map(|&(k, v)| (k.into(), v.into())) + .collect::>(); + assert_eq!(kvstore, wanted_store); + } + + #[test] + fn test_write() { + let mut output = Vec::new(); + let store = [ + ("name", "climate_api"), + ("title", "Climate API"), + ("author", "TestificateMods"), + ("release", "10001"), + ("optional_depends", "player_monoids, playerphysics, pova"), + ( + "description", + indoc! {" + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience.\ + "}, + ), + ] + .iter() + .map(|&(k, v)| (k.into(), v.into())) + .collect::>(); + + write(&store, &mut output).unwrap(); + + let output = std::str::from_utf8(&output).unwrap(); + let expected = indoc! {r#" + author = TestificateMods + description = """ + A powerful engine for weather presets and visual effects. + Use the regional climate to set up different effects for different regions. + Control where your effects are activated based on temperature, humidity, wind, + position, light level or a completely custom activator. + Climate API provides temperature and humidity values on a block-per-block basis + that follow the seasons, day / night cycle and random changes. + Make it rain, change the sky or poison the player - it's up to you. + + Climate API requires additional weather packs in order to function. + Try regional_weather for the best experience. + """ + name = climate_api + optional_depends = player_monoids, playerphysics, pova + release = 10001 + title = Climate API + "#}; + + assert_eq!(output, expected); + } +} diff --git a/modderbaas/src/world.rs b/modderbaas/src/world.rs index 5dce6d0..d51496a 100644 --- a/modderbaas/src/world.rs +++ b/modderbaas/src/world.rs @@ -86,7 +86,7 @@ impl World { conf.entry(key) .and_modify(|e| *e = "true".into()) .or_insert_with(|| "true".into()); - kvstore::write(&conf, &self.path.join("world.mt")) + kvstore::save(&conf, &self.path.join("world.mt")) } } -- cgit v1.2.3