Refactor dynamic component parsing

This commit is contained in:
lovasoa
2024-03-10 20:24:09 +01:00
parent 30d4abbc98
commit 63131646ec
+128 -27
View File
@@ -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<dyn Iterator<Item = DbItem>> {
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<Item = DbItem> {
DynamicComponentIterator {
stack: vec![],
db_item: Some(row),
}
}
fn parse_dynamic_rows_json(mut row: JsonValue, depth: u8) -> Box<dyn Iterator<Item = DbItem>> {
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<anyhow::Result<JsonValue>>,
db_item: Option<DbItem>,
}
impl Iterator for DynamicComponentIterator {
type Item = DbItem;
fn next(&mut self) -> Option<Self::Item> {
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<anyhow::Result<JsonValue>>) {
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<JsonValue> {
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<JsonValue> {
}
}
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<anyhow::Result<JsonValue>> {
dynamic_properties_to_result_vec(properties_obj).map_or_else(
|err| vec![Err(err)],
|vec| vec.into_iter().rev().map(Ok).collect::<Vec<_>>(),
)
}
/// 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<Box<dyn Iterator<Item = JsonValue>>> {
) -> anyhow::Result<Vec<JsonValue>> {
if let JsonValue::String(s) = properties_obj {
properties_obj = serde_json::from_str::<JsonValue>(&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::<Vec<_>>();
assert_eq!(
iter,
vec![
serde_json::json!({"a": 1}),
serde_json::json!({"nested": 2}),
]
);
}
}