From 63131646ec8fa0afee0a77a5948718e889ebf3fd Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 10 Mar 2024 20:24:09 +0100 Subject: [PATCH] Refactor dynamic component parsing --- src/dynamic_component.rs | 155 ++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 27 deletions(-) diff --git a/src/dynamic_component.rs b/src/dynamic_component.rs index f2a97fee..63186428 100644 --- a/src/dynamic_component.rs +++ b/src/dynamic_component.rs @@ -3,37 +3,55 @@ use serde_json::Value as JsonValue; use crate::webserver::database::DbItem; -const MAX_RECURSION_DEPTH: u8 = 127; - -/// the raw query results can include (potentially nested) rows with a 'component' column that has the value 'dynamic'. -/// in that case we need to parse the JSON in the 'properties' column, and emit a row for each value in the resulting json array. -#[must_use] pub fn parse_dynamic_rows(db_item: DbItem) -> Box> { - if let DbItem::Row(row) = db_item { - parse_dynamic_rows_json(row, 0) - } else { - Box::new(std::iter::once(db_item)) +pub fn parse_dynamic_rows(row: DbItem) -> impl Iterator { + DynamicComponentIterator { + stack: vec![], + db_item: Some(row), } } -fn parse_dynamic_rows_json(mut row: JsonValue, depth: u8) -> Box> { - if depth >= MAX_RECURSION_DEPTH { - return Box::new(std::iter::once(DbItem::Error(anyhow::anyhow!( - "Too many nested dynamic components: \n\ - The 'dynamic' component can be used to render another 'dynamic' component, \ - but the recursion cannot exceed {depth} layers." - )))); - } - if let Some(properties) = extract_dynamic_properties(&mut row) { - match dynamic_properties_to_iter(properties) { - Ok(iter) => Box::new(iter.flat_map(move |v| parse_dynamic_rows_json(v, depth + 1))), - Err(e) => Box::new(std::iter::once(DbItem::Error(e))), +struct DynamicComponentIterator { + stack: Vec>, + db_item: Option, +} + +impl Iterator for DynamicComponentIterator { + type Item = DbItem; + + fn next(&mut self) -> Option { + if let Some(db_item) = self.db_item.take() { + if let DbItem::Row(mut row) = db_item { + if let Some(properties) = extract_dynamic_properties(&mut row) { + self.stack = dynamic_properties_to_vec(properties); + } else { + // Most common case: just a regular row. We allocated nothing. + return Some(DbItem::Row(row)); + } + } else { + return Some(db_item); + } + } + expand_dynamic_stack(&mut self.stack); + self.stack.pop().map(|result| match result { + Ok(row) => DbItem::Row(row), + Err(err) => DbItem::Error(err), + }) + } +} + +fn expand_dynamic_stack(stack: &mut Vec>) { + while let Some(Ok(mut next)) = stack.pop() { + if let Some(properties) = extract_dynamic_properties(&mut next) { + stack.extend(dynamic_properties_to_vec(properties)); + } else { + stack.push(Ok(next)); + return; } - } else { - Box::new(std::iter::once(DbItem::Row(row))) } } /// if row.component == 'dynamic', return Some(row.properties), otherwise return None +#[inline] fn extract_dynamic_properties(data: &mut JsonValue) -> Option { let component = data.get("component").and_then(|v| v.as_str()); if component == Some("dynamic") { @@ -44,9 +62,22 @@ fn extract_dynamic_properties(data: &mut JsonValue) -> Option { } } -fn dynamic_properties_to_iter( +/// reverse the order of the vec returned by `dynamic_properties_to_result_vec`, +/// and wrap each element in a Result +fn dynamic_properties_to_vec(properties_obj: JsonValue) -> Vec> { + dynamic_properties_to_result_vec(properties_obj).map_or_else( + |err| vec![Err(err)], + |vec| vec.into_iter().rev().map(Ok).collect::>(), + ) +} + +/// if properties is a string, parse it as JSON and return a vec with the parsed value +/// if properties is an array, return it as is +/// if properties is an object, return it as a single element vec +/// otherwise, return an error +fn dynamic_properties_to_result_vec( mut properties_obj: JsonValue, -) -> anyhow::Result>> { +) -> anyhow::Result> { if let JsonValue::String(s) = properties_obj { properties_obj = serde_json::from_str::(&s).with_context(|| { format!( @@ -56,10 +87,80 @@ fn dynamic_properties_to_iter( })?; } match properties_obj { - obj @ JsonValue::Object(_) => Ok(Box::new(std::iter::once(obj))), - JsonValue::Array(values) => Ok(Box::new(values.into_iter())), + obj @ JsonValue::Object(_) => Ok(vec![obj]), + JsonValue::Array(values) => Ok(values), other => anyhow::bail!( "Dynamic component expected properties of type array or object, got {other} instead." ), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dynamic_properties_to_result_vec() { + let mut properties = JsonValue::String(r#"{"a": 1}"#.to_string()); + assert_eq!( + dynamic_properties_to_result_vec(properties.clone()).unwrap(), + vec![JsonValue::Object( + serde_json::from_str(r#"{"a": 1}"#).unwrap() + )] + ); + + properties = JsonValue::Array(vec![JsonValue::String(r#"{"a": 1}"#.to_string())]); + assert_eq!( + dynamic_properties_to_result_vec(properties.clone()).unwrap(), + vec![JsonValue::String(r#"{"a": 1}"#.to_string())] + ); + + properties = JsonValue::Object(serde_json::from_str(r#"{"a": 1}"#).unwrap()); + assert_eq!( + dynamic_properties_to_result_vec(properties.clone()).unwrap(), + vec![JsonValue::Object( + serde_json::from_str(r#"{"a": 1}"#).unwrap() + )] + ); + + properties = JsonValue::Null; + assert!(dynamic_properties_to_result_vec(properties).is_err()); + } + + #[test] + fn test_dynamic_properties_to_vec() { + let properties = JsonValue::String(r#"{"a": 1}"#.to_string()); + assert_eq!( + dynamic_properties_to_vec(properties.clone()) + .first() + .unwrap() + .as_ref() + .unwrap(), + &serde_json::json!({"a": 1}) + ); + } + + #[test] + fn test_parse_dynamic_rows() { + let row = DbItem::Row(serde_json::json!({ + "component": "dynamic", + "properties": [ + {"a": 1}, + {"component": "dynamic", "properties": {"nested": 2}}, + ] + })); + let iter = parse_dynamic_rows(row) + .map(|item| match item { + DbItem::Row(row) => row, + x => panic!("Expected a row, got {x:?}"), + }) + .collect::>(); + assert_eq!( + iter, + vec![ + serde_json::json!({"a": 1}), + serde_json::json!({"nested": 2}), + ] + ); + } +}