extract and test dynamic component

This commit is contained in:
lovasoa
2024-03-10 14:06:33 +01:00
parent 704a03301a
commit 1cdd2de83d
5 changed files with 206 additions and 54 deletions
+65
View File
@@ -0,0 +1,65 @@
use anyhow::{self, Context as _};
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))
}
}
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))),
}
} else {
Box::new(std::iter::once(DbItem::Row(row)))
}
}
/// if row.component == 'dynamic', return Some(row.properties), otherwise return None
fn extract_dynamic_properties(data: &mut JsonValue) -> Option<JsonValue> {
let component = data.get("component").and_then(|v| v.as_str());
if component == Some("dynamic") {
let properties = data.get_mut("properties").map(JsonValue::take);
Some(properties.unwrap_or_default())
} else {
None
}
}
fn dynamic_properties_to_iter(
mut properties_obj: JsonValue,
) -> anyhow::Result<Box<dyn Iterator<Item = JsonValue>>> {
if let JsonValue::String(s) = properties_obj {
properties_obj = serde_json::from_str::<JsonValue>(&s).with_context(|| {
format!(
"Unable to parse the 'properties' property of the dynamic component as JSON.\n\
Invalid json: {s}"
)
})?;
}
match properties_obj {
obj @ JsonValue::Object(_) => Ok(Box::new(std::iter::once(obj))),
JsonValue::Array(values) => Ok(Box::new(values.into_iter())),
other => anyhow::bail!(
"Dynamic component expected properties of type array or object, got {other} instead."
),
}
}
+1
View File
@@ -4,6 +4,7 @@
extern crate core;
pub mod app_config;
pub mod dynamic_component;
pub mod file_cache;
pub mod filesystem;
pub mod render;
+3 -54
View File
@@ -7,20 +7,18 @@ use std::pin::Pin;
use super::csv_import::run_csv_import;
use super::sql::{ParsedSqlFile, ParsedStatement, StmtWithParams};
use crate::dynamic_component::parse_dynamic_rows;
use crate::webserver::database::sql_pseudofunctions::extract_req_param;
use crate::webserver::database::sql_to_json::row_to_string;
use crate::webserver::http::SingleOrVec;
use crate::webserver::http_request_info::RequestInfo;
use super::sql_pseudofunctions::StmtParam;
use super::{highlight_sql_error, Database, DbItem};
use sqlx::any::{AnyArguments, AnyQueryResult, AnyRow, AnyStatement, AnyTypeInfo};
use sqlx::pool::PoolConnection;
use sqlx::{Any, AnyConnection, Arguments, Either, Executor, Statement};
use serde_json::Value as JsonValue;
use super::sql_pseudofunctions::StmtParam;
use super::{highlight_sql_error, Database, DbItem};
impl Database {
pub(crate) async fn prepare_with(
&self,
@@ -34,55 +32,6 @@ impl Database {
.map_err(|e| highlight_sql_error("Failed to prepare SQL statement", query, e))
}
}
/// 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.
pub fn parse_dynamic_rows(db_item: DbItem) -> Box<dyn Iterator<Item = DbItem>> {
if let DbItem::Row(mut row) = db_item {
if let Some(properties) = extract_dynamic_properties(&mut row) {
match dynamic_properties_to_iter(properties) {
Ok(iter) => Box::new(iter.map(DbItem::Row)),
Err(e) => Box::new(std::iter::once(DbItem::Error(e))),
}
} else {
Box::new(std::iter::once(DbItem::Row(row)))
}
} else {
Box::new(std::iter::once(db_item))
}
}
/// if row.component == 'dynamic', return Some(row.properties), otherwise return None
fn extract_dynamic_properties(data: &mut JsonValue) -> Option<JsonValue> {
let component = data.get("component").and_then(|v| v.as_str());
if component == Some("dynamic") {
let properties = data.get_mut("properties").map(JsonValue::take);
Some(properties.unwrap_or_default())
} else {
None
}
}
fn dynamic_properties_to_iter(
mut properties_obj: JsonValue,
) -> anyhow::Result<Box<dyn Iterator<Item = JsonValue>>> {
if let JsonValue::String(s) = properties_obj {
properties_obj = serde_json::from_str::<JsonValue>(&s).with_context(|| {
format!(
"Unable to parse the 'properties' property of the dynamic component as JSON.\n\
Invalid json: {s}"
)
})?;
}
match properties_obj {
obj @ JsonValue::Object(_) => Ok(Box::new(std::iter::once(obj))),
JsonValue::Array(values) => Ok(Box::new(values.into_iter())),
other => anyhow::bail!(
"Dynamic component expected properties of type array or object, got {other} instead."
),
}
}
pub fn stream_query_results<'a>(
db: &'a Database,
sql_file: &'a ParsedSqlFile,
@@ -0,0 +1,129 @@
select 'dynamic' as component,'{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"dynamic","properties":
{"component":"text", "contents": "It works ! (but it shouldn''t)"}
}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
' as properties;
@@ -0,0 +1,8 @@
-- Checks that we can have a page with a single dynamic component containing multiple children
select 'dynamic' as component,
'[
{"component":"dynamic", "properties": [
{"component":"text"},
{"contents":"It works !", "bold":true}
]}
]' as properties;