From cf5f15d5b474b751d8a66d701f6fd8fc3c6f5830 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Mon, 29 Apr 2024 18:06:07 -0400 Subject: [PATCH] settings map translation --- src/run/auto_splitter_settings.rs | 37 +--- src/run/mod.rs | 8 +- src/run/parser/livesplit.rs | 318 +++++++++++++++++++++++++----- src/run/saver/livesplit.rs | 235 +++++++++++++++++++--- src/util/xml/helper.rs | 3 - src/util/xml/writer.rs | 5 - 6 files changed, 488 insertions(+), 118 deletions(-) diff --git a/src/run/auto_splitter_settings.rs b/src/run/auto_splitter_settings.rs index 2f273b28..a5d3f08d 100644 --- a/src/run/auto_splitter_settings.rs +++ b/src/run/auto_splitter_settings.rs @@ -6,14 +6,7 @@ use livesplit_auto_splitting::settings; pub struct AutoSplitterSettings { pub version: Version, pub script_path: String, - pub custom_settings: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct CustomSetting { - pub id: String, - pub setting_type: settings::WidgetKind, - pub value: settings::Value, + pub custom_settings: settings::Map, } impl AutoSplitterSettings { @@ -25,31 +18,7 @@ impl AutoSplitterSettings { self.script_path = script_path; } - pub fn add_custom_setting(&mut self, custom_setting: CustomSetting) { - self.custom_settings.push(custom_setting); - } -} - -impl CustomSetting { - pub fn new() -> Self { - Self { - id: String::default(), - setting_type: settings::WidgetKind::Bool { - default_value: false, - }, - value: settings::Value::Bool(false), - } - } - - pub fn set_id(&mut self, id: String) { - self.id = id; - } - - pub fn set_setting_type(&mut self, setting_type: settings::WidgetKind) { - self.setting_type = setting_type; - } - - pub fn set_value(&mut self, value: settings::Value) { - self.value = value + pub fn set_custom_settings(&mut self, custom_settings: settings::Map) { + self.custom_settings = custom_settings; } } diff --git a/src/run/mod.rs b/src/run/mod.rs index 7e148053..d62f33fa 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -81,7 +81,7 @@ pub struct Run { comparison_generators: ComparisonGenerators, auto_splitter_settings: String, #[cfg(feature = "auto-splitting")] - parsed_auto_splitter_settings: AutoSplitterSettings, + parsed_auto_splitter_settings: Option, linked_layout: Option, } @@ -136,7 +136,7 @@ impl Run { comparison_generators: ComparisonGenerators(default_generators()), auto_splitter_settings: String::new(), #[cfg(feature = "auto-splitting")] - parsed_auto_splitter_settings: AutoSplitterSettings::default(), + parsed_auto_splitter_settings: None, linked_layout: None, } } @@ -338,14 +338,14 @@ impl Run { /// Accesses the Auto Splitter Settings. #[inline] #[cfg(feature = "auto-splitting")] - pub fn parsed_auto_splitter_settings(&self) -> &AutoSplitterSettings { + pub fn parsed_auto_splitter_settings(&self) -> &Option { &self.parsed_auto_splitter_settings } /// Accesses the Auto Splitter Settings as mutable. #[inline] #[cfg(feature = "auto-splitting")] - pub fn parsed_auto_splitter_settings_mut(&mut self) -> &mut AutoSplitterSettings { + pub fn parsed_auto_splitter_settings_mut(&mut self) -> &mut Option { &mut self.parsed_auto_splitter_settings } diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs index cf49dc3d..4ed6930d 100644 --- a/src/run/parser/livesplit.rs +++ b/src/run/parser/livesplit.rs @@ -23,7 +23,7 @@ use core::{mem::MaybeUninit, str}; use time::{Date, Duration, PrimitiveDateTime}; #[cfg(feature = "auto-splitting")] use { - crate::run::auto_splitter_settings::{AutoSplitterSettings, CustomSetting}, + crate::run::auto_splitter_settings::AutoSplitterSettings, crate::util::xml::Attributes, livesplit_auto_splitting::settings, }; @@ -438,63 +438,142 @@ fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Ru } } -#[cfg(feature = "auto-splitting")] fn parse_auto_splitter_settings( _version: Version, reader: &mut Reader<'_>, run: &mut Run, ) -> Result<()> { + crate::util::xml::helper::reencode_children(reader, run.auto_splitter_settings_mut()) + .map_err(Into::::into)?; + + #[cfg(feature = "auto-splitting")] + let mut reader = Reader::new(run.auto_splitter_settings()); + + #[cfg(feature = "auto-splitting")] + let mut any_parsed = false; + #[cfg(feature = "auto-splitting")] let mut settings = AutoSplitterSettings::default(); + #[cfg(feature = "auto-splitting")] // The compiler seems to throw a warning that 'attributes' isn't used by default, it actually is though #[allow(unused_variables)] - parse_children(reader, |reader, tag, attributes| match tag.name() { + parse_children(&mut reader, |reader, tag, attributes| match tag.name() { "Version" => type_hint(text(reader, |t| { + any_parsed = true; settings.set_version(parse_version(t.as_ref()).unwrap_or_default()) })), - "ScriptPath" => type_hint(text(reader, |t| settings.set_script_path(t.to_string()))), + "ScriptPath" => type_hint(text(reader, |t| { + any_parsed = true; + settings.set_script_path(t.to_string()) + })), "CustomSettings" => { - parse_children(reader, |reader, _tag, attributes| { - let mut custom_setting = CustomSetting::new(); - - type_hint(parse_attributes(attributes, |k, v| { - match k { - "id" => custom_setting.set_id(v.unescape_str()), - "type" => match v.unescape_str().as_str() { - "bool" => custom_setting.set_setting_type(settings::WidgetKind::Bool { - default_value: false, - }), - _ => custom_setting.set_setting_type(settings::WidgetKind::Bool { - default_value: false, - }), - }, - _ => {} - } - Ok(true) - })) - .ok(); - type_hint(text(reader, |t| { - custom_setting.set_value(settings::Value::Bool( - parse_bool(t.as_ref()).unwrap_or_default(), - )) - })) - .ok(); - settings.add_custom_setting(custom_setting); - Ok::<(), Error>(()) - }) - .ok(); - + any_parsed = true; + settings.set_custom_settings(parse_settings_map(reader)); Ok(()) } _ => Ok(()), }) .ok(); - run.parsed_auto_splitter_settings = settings; + #[cfg(feature = "auto-splitting")] + if any_parsed { + run.parsed_auto_splitter_settings = Some(settings); + } Ok(()) } +#[cfg(feature = "auto-splitting")] +fn parse_settings_map(reader: &mut Reader<'_>) -> settings::Map { + let mut settings_map = settings::Map::new(); + + parse_children(reader, |reader, _tag, attributes| { + if let (Some(id), Some(value)) = parse_settings_entry(reader, attributes) { + settings_map.insert(id.into(), value); + } + Ok::<(), Error>(()) + }) + .ok(); + + settings_map +} + +#[cfg(feature = "auto-splitting")] +fn parse_settings_list(reader: &mut Reader<'_>) -> settings::List { + let mut settings_list = settings::List::new(); + + parse_children(reader, |reader, _tag, attributes| { + if let (_, Some(value)) = parse_settings_entry(reader, attributes) { + settings_list.push(value); + } + Ok::<(), Error>(()) + }) + .ok(); + + settings_list +} + +#[cfg(feature = "auto-splitting")] +fn parse_settings_entry( + reader: &mut Reader<'_>, + attributes: Attributes<'_>, +) -> (Option, Option) { + let mut id = None; + let mut setting_type = None; + let mut string_value = None; + type_hint(parse_attributes(attributes, |k, v| { + match k { + "id" => id = Some(v.unescape_str()), + "type" => setting_type = Some(v.unescape_str()), + "value" => string_value = Some(v.unescape_str()), + _ => {} + } + Ok(true) + })) + .ok(); + let Some(setting_type) = setting_type else { + return (id, None); + }; + let value = match setting_type.as_str() { + "bool" => { + let mut b = bool::default(); + type_hint(text(reader, |t| { + b = parse_bool(t.as_ref()).unwrap_or_default(); + })) + .ok(); + Some(settings::Value::Bool(b)) + } + "i64" => { + let mut i = i64::default(); + type_hint(text(reader, |t| { + i = t.as_ref().parse().unwrap_or_default(); + })) + .ok(); + Some(settings::Value::I64(i)) + } + "f64" => { + let mut f = f64::default(); + type_hint(text(reader, |t| { + f = t.as_ref().parse().unwrap_or_default(); + })) + .ok(); + Some(settings::Value::F64(f)) + } + "string" => { + let mut s = String::default(); + type_hint(text(reader, |t| { + s = t.to_string(); + })) + .ok(); + Some(settings::Value::String(string_value.unwrap_or(s).into())) + } + "map" => Some(settings::Value::Map(parse_settings_map(reader))), + "list" => Some(settings::Value::List(parse_settings_list(reader))), + _ => None, + }; + (id, value) +} + /// Attempts to parse a LiveSplit splits file. pub fn parse(source: &str) -> Result { let mut reader = Reader::new(source); @@ -550,16 +629,7 @@ pub fn parse(source: &str) -> Result { } }) } - "AutoSplitterSettings" => { - #[cfg(not(feature = "auto-splitting"))] - { - let settings = run.auto_splitter_settings_mut(); - crate::util::xml::helper::reencode_children(reader, settings) - .map_err(Into::into) - } - #[cfg(feature = "auto-splitting")] - parse_auto_splitter_settings(version, reader, &mut run) - } + "AutoSplitterSettings" => parse_auto_splitter_settings(version, reader, &mut run), "LayoutPath" => text(reader, |t| { run.set_linked_layout(if t == "?default" { Some(LinkedLayout::Default) @@ -585,6 +655,8 @@ pub fn parse(source: &str) -> Result { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "auto-splitting")] + use livesplit_auto_splitting::settings; #[test] fn time_span_parsing() { @@ -606,4 +678,160 @@ mod tests { parse_time_span("NaN.23:34:56.789").unwrap_err(); parse_time_span("Inf.23:34:56.789").unwrap_err(); } + + #[cfg(feature = "auto-splitting")] + #[test] + fn test_parse_settings() { + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + True + True + True + "# + )), + { + let mut m = settings::Map::new(); + m.insert("start".into(), settings::Value::Bool(true)); + m.insert("split".into(), settings::Value::Bool(true)); + m.insert("remove_loads".into(), settings::Value::Bool(true)); + m + }, + ); + + assert_eq!( + parse_settings_list(&mut Reader::new( + r#""# + )), + { + let mut l = settings::List::new(); + l.push(settings::Value::String("KingsPass".into())); + l + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#""# + )), + { + let mut m = settings::Map::new(); + m.insert( + "splits_0_item".into(), + settings::Value::String("KingsPass".into()), + ); + m + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + + True + + + "# + )), + { + let mut map = settings::Map::new(); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + + map.insert("inner_map".into(), settings::Value::Map(inner_map)); + map + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + + True + + + True + + + + "# + )), + { + let mut map = settings::Map::new(); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + inner_map.insert("recursive".into(), settings::Value::Map(inner_map.clone())); + + map.insert("lolol".into(), settings::Value::Map(inner_map)); + map + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + + True + + + True + + + + "# + )), + { + let mut map = settings::Map::new(); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + inner_map.insert("recursive".into(), settings::Value::Map(inner_map.clone())); + + map.insert("lolol".into(), settings::Value::Map(inner_map)); + map + }, + ); + + assert_eq!( + parse_settings_map(&mut Reader::new( + r#" + True + True + True + + True + + + True + + + + + "# + )), + { + let mut map = settings::Map::new(); + map.insert("level32_bool".into(), settings::Value::Bool(true)); + map.insert("other_setting".into(), settings::Value::Bool(true)); + map.insert("level12_bool".into(), settings::Value::Bool(true)); + + let mut inner_map = settings::Map::new(); + inner_map.insert("first".into(), settings::Value::Bool(true)); + inner_map.insert("second".into(), settings::Value::String("bar".into())); + inner_map.insert("recursive".into(), settings::Value::Map(inner_map.clone())); + + map.insert("lolol".into(), settings::Value::Map(inner_map)); + map.insert( + "okok".into(), + settings::Value::String("hello, you seem to like true!".into()), + ); + map + }, + ); + } } diff --git a/src/run/saver/livesplit.rs b/src/run/saver/livesplit.rs index 80133077..cfab3a44 100644 --- a/src/run/saver/livesplit.rs +++ b/src/run/saver/livesplit.rs @@ -24,6 +24,8 @@ //! livesplit::save_run(&run, IoWrite(writer)).expect("Couldn't save the splits file"); //! ``` +#[cfg(feature = "auto-splitting")] +use crate::run::AutoSplitterSettings; use crate::{ platform::prelude::*, run::LinkedLayout, @@ -310,43 +312,222 @@ pub fn save_run(run: &Run, writer: W) -> fmt::Result { }) })?; - #[cfg(not(feature = "auto-splitting"))] - return writer.tag_with_text_content( - "AutoSplitterSettings", - NO_ATTRIBUTES, - Text::new_escaped(run.auto_splitter_settings()), - ); - #[cfg(feature = "auto-splitting")] - writer.tag_with_content("AutoSplitterSettings", NO_ATTRIBUTES, |writer| { + write_run_auto_splitter_settings(writer, run) + }) +} + +fn write_run_auto_splitter_settings( + writer: &mut Writer, + run: &Run, +) -> fmt::Result { + #[cfg(feature = "auto-splitting")] + if let Some(AutoSplitterSettings { + version, + script_path, + custom_settings, + }) = &run.parsed_auto_splitter_settings + { + return writer.tag_with_content("AutoSplitterSettings", NO_ATTRIBUTES, |writer| { writer.tag_with_text_content( "Version", NO_ATTRIBUTES, - DisplayAlreadyEscaped(&run.parsed_auto_splitter_settings.version), + DisplayAlreadyEscaped(version), )?; writer.tag_with_text_content( "ScriptPath", NO_ATTRIBUTES, - DisplayAlreadyEscaped(&run.parsed_auto_splitter_settings.script_path), + DisplayAlreadyEscaped(script_path), )?; - scoped_iter( - writer, - "CustomSettings", - &run.parsed_auto_splitter_settings.custom_settings, - |writer, custom_setting| { - let value = match custom_setting.value { - settings::Value::Bool(value) => value, - _ => false, - }; - writer.tag_with_text_content( - "Setting", - [("id", custom_setting.id.as_str()), ("type", "bool")], - bool(value), - ) - }, - )?; + write_settings_map(writer, "CustomSettings", vec![], custom_settings)?; Ok(()) - }) + }); + } + + writer.tag_with_text_content( + "AutoSplitterSettings", + NO_ATTRIBUTES, + Text::new_escaped(run.auto_splitter_settings()), + ) +} + +#[cfg(feature = "auto-splitting")] +fn write_settings_map( + writer: &mut Writer, + tag: &str, + attrs: Vec<(&str, &str)>, + map: &settings::Map, +) -> fmt::Result { + writer.tag_with_content(tag, attrs, |writer| { + for (id, value) in map.iter() { + write_settings_entry(writer, vec![("id", id)], value).ok(); + } + Ok(()) }) } + +#[cfg(feature = "auto-splitting")] +fn write_settings_list( + writer: &mut Writer, + tag: &str, + attrs: Vec<(&str, &str)>, + map: &settings::List, +) -> fmt::Result { + writer.tag_with_content(tag, attrs, |writer| { + for value in map.iter() { + write_settings_entry(writer, vec![], value).ok(); + } + Ok(()) + }) +} + +#[cfg(feature = "auto-splitting")] +fn write_settings_entry<'a, W>( + writer: &mut Writer, + mut attrs: Vec<(&str, &'a str)>, + value: &'a settings::Value, +) -> fmt::Result +where + W: fmt::Write, +{ + match value { + settings::Value::Map(m) => { + attrs.push(("type", "map")); + write_settings_map(writer, "Setting", attrs, m) + } + settings::Value::List(l) => { + attrs.push(("type", "list")); + write_settings_list(writer, "Setting", attrs, l) + } + settings::Value::Bool(b) => { + attrs.push(("type", "bool")); + writer.tag_with_text_content("Setting", attrs, bool(*b)) + } + settings::Value::I64(i) => { + attrs.push(("type", "i64")); + writer.tag_with_text_content("Setting", attrs, Text::new_escaped(&i.to_string())) + } + settings::Value::F64(f) => { + attrs.push(("type", "f64")); + writer.tag_with_text_content("Setting", attrs, Text::new_escaped(&f.to_string())) + } + settings::Value::String(s) => { + attrs.push(("type", "string")); + attrs.push(("value", s)); + writer.empty_tag("Setting", attrs) + } + _ => writer.empty_tag("Setting", attrs), + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "auto-splitting")] + use super::*; + #[cfg(feature = "auto-splitting")] + use livesplit_auto_splitting::settings; + + #[cfg(feature = "auto-splitting")] + #[test] + fn test_write_settings() { + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + write_settings_entry(&mut writer, vec![], &settings::Value::Bool(true)).ok(); + s + }, + r#"True"# + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + write_settings_entry( + &mut writer, + vec![("id", "start")], + &settings::Value::Bool(true), + ) + .ok(); + s + }, + r#"True"# + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + let mut m = settings::Map::new(); + m.insert("start".into(), settings::Value::Bool(true)); + m.insert("split".into(), settings::Value::Bool(true)); + m.insert("remove_loads".into(), settings::Value::Bool(true)); + write_settings_map(&mut writer, "CustomSettings", vec![], &m).ok(); + s + }, + format!( + "{}{}{}{}{}", + r#""#, + r#"True"#, + r#"True"#, + r#"True"#, + r#""#, + ) + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + let mut m = settings::Map::new(); + m.insert("first".into(), settings::Value::Bool(true)); + m.insert("second".into(), settings::Value::String("bar".into())); + write_settings_entry( + &mut writer, + vec![("id", "inner_map")], + &settings::Value::Map(m), + ) + .ok(); + s + }, + format!( + "{}{}{}{}", + r#""#, + r#"True"#, + r#""#, + r#""#, + ) + ); + + assert_eq!( + { + let mut s = String::new(); + let mut writer = Writer::new_skip_header(&mut s); + let mut m = settings::Map::new(); + m.insert("first".into(), settings::Value::Bool(true)); + m.insert("second".into(), settings::Value::String("bar".into())); + m.insert("recursive".into(), settings::Value::Map(m.clone())); + write_settings_entry( + &mut writer, + vec![("id", "inner_map")], + &settings::Value::Map(m), + ) + .ok(); + s + }, + format!( + "{}{}{}{}{}{}{}{}", + r#""#, + r#"True"#, + r#""#, + r#""#, + r#"True"#, + r#""#, + r#""#, + r#""#, + ) + ); + } +} diff --git a/src/util/xml/helper.rs b/src/util/xml/helper.rs index 5a45b0df..feae0dc1 100644 --- a/src/util/xml/helper.rs +++ b/src/util/xml/helper.rs @@ -1,10 +1,8 @@ use crate::platform::prelude::*; use alloc::borrow::Cow; -#[cfg(not(feature = "auto-splitting"))] use core::fmt; use core::{mem::MaybeUninit, str}; -#[cfg(not(feature = "auto-splitting"))] use super::Writer; use super::{Attributes, Event, Reader, TagName, Text}; @@ -121,7 +119,6 @@ where } } -#[cfg(not(feature = "auto-splitting"))] pub fn reencode_children(reader: &mut Reader<'_>, target_buf: &mut String) -> Result<(), Error> { let mut writer = Writer::new_skip_header(target_buf); let mut depth = 0usize; diff --git a/src/util/xml/writer.rs b/src/util/xml/writer.rs index 59d78f78..31645ee3 100644 --- a/src/util/xml/writer.rs +++ b/src/util/xml/writer.rs @@ -10,7 +10,6 @@ pub struct Writer { } impl Writer { - #[cfg(not(feature = "auto-splitting"))] pub const fn new_skip_header(sink: T) -> Self { Self { sink } } @@ -93,7 +92,6 @@ impl Writer { }) } - #[cfg(not(feature = "auto-splitting"))] pub fn comment(&mut self, text: impl Value) -> fmt::Result { self.sink.write_str("