From 1cdd2de83d41b343a2301727466bbe8801c8f7f4 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sun, 10 Mar 2024 14:06:33 +0100 Subject: [PATCH] extract and test dynamic component --- src/dynamic_component.rs | 65 +++++++++ src/lib.rs | 1 + src/webserver/database/execute_queries.rs | 57 +------- ...ror_too_many_nested_dynamic_components.sql | 129 ++++++++++++++++++ .../it_works_dynamic_nested.sql | 8 ++ 5 files changed, 206 insertions(+), 54 deletions(-) create mode 100644 src/dynamic_component.rs create mode 100644 tests/sql_test_files/error_too_many_nested_dynamic_components.sql create mode 100644 tests/sql_test_files/it_works_dynamic_nested.sql diff --git a/src/dynamic_component.rs b/src/dynamic_component.rs new file mode 100644 index 00000000..f2a97fee --- /dev/null +++ b/src/dynamic_component.rs @@ -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> { + 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> { + 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 { + 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>> { + if let JsonValue::String(s) = properties_obj { + properties_obj = serde_json::from_str::(&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." + ), + } +} diff --git a/src/lib.rs b/src/lib.rs index 38845be9..9c3e5166 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/webserver/database/execute_queries.rs b/src/webserver/database/execute_queries.rs index 08869210..1a4df801 100644 --- a/src/webserver/database/execute_queries.rs +++ b/src/webserver/database/execute_queries.rs @@ -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> { - 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 { - 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>> { - if let JsonValue::String(s) = properties_obj { - properties_obj = serde_json::from_str::(&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, diff --git a/tests/sql_test_files/error_too_many_nested_dynamic_components.sql b/tests/sql_test_files/error_too_many_nested_dynamic_components.sql new file mode 100644 index 00000000..c0608b23 --- /dev/null +++ b/tests/sql_test_files/error_too_many_nested_dynamic_components.sql @@ -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; \ No newline at end of file diff --git a/tests/sql_test_files/it_works_dynamic_nested.sql b/tests/sql_test_files/it_works_dynamic_nested.sql new file mode 100644 index 00000000..4c3c0fb8 --- /dev/null +++ b/tests/sql_test_files/it_works_dynamic_nested.sql @@ -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; \ No newline at end of file