Refactor dynamic component parsing
This commit is contained in:
+128
-27
@@ -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}),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user