diff --git a/src/webserver/database/execute_queries.rs b/src/webserver/database/execute_queries.rs index b831539b..8b31a436 100644 --- a/src/webserver/database/execute_queries.rs +++ b/src/webserver/database/execute_queries.rs @@ -15,8 +15,8 @@ use super::sql::{ use crate::dynamic_component::parse_dynamic_rows; use crate::utils::add_value_to_map; use crate::webserver::database::sql_to_json::row_to_string; -use crate::webserver::http::SingleOrVec; use crate::webserver::http_request_info::RequestInfo; +use crate::webserver::single_or_vec::SingleOrVec; use super::syntax_tree::{extract_req_param, StmtParam}; use super::{error_highlighting::display_db_error, Database, DbItem}; diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 169ebc8b..b7665076 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -4,9 +4,9 @@ use crate::webserver::{ blob_to_data_url::vec_to_data_uri_with_mime, execute_queries::DbConn, sqlpage_functions::url_parameter_deserializer::URLParameters, }, - http::SingleOrVec, http_client::make_http_client, request_variables::ParamMap, + single_or_vec::SingleOrVec, ErrorWithStatus, }; use anyhow::{anyhow, Context}; @@ -571,7 +571,14 @@ async fn run_sql<'a>( .with_context(|| format!("run_sql: invalid path {sql_file_path:?}"))?; let mut tmp_req = if let Some(variables) = variables { let mut tmp_req = request.clone_without_variables(); - let variables: ParamMap = serde_json::from_str(&variables)?; + let variables: ParamMap = serde_json::from_str(&variables).map_err(|err| { + let context = format!( + "run_sql: unable to parse the variables argument (line {}, column {})", + err.line(), + err.column() + ); + anyhow::Error::new(err).context(context) + })?; tmp_req.get_variables = variables; tmp_req } else { diff --git a/src/webserver/database/syntax_tree.rs b/src/webserver/database/syntax_tree.rs index 1558912d..b63aa738 100644 --- a/src/webserver/database/syntax_tree.rs +++ b/src/webserver/database/syntax_tree.rs @@ -16,8 +16,8 @@ use std::str::FromStr; use sqlparser::ast::FunctionArg; -use crate::webserver::http::SingleOrVec; use crate::webserver::http_request_info::RequestInfo; +use crate::webserver::single_or_vec::SingleOrVec; use super::{ execute_queries::DbConn, sql::function_args_to_stmt_params, diff --git a/src/webserver/http.rs b/src/webserver/http.rs index dc63d49d..9468e11e 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -34,8 +34,6 @@ use anyhow::{bail, Context}; use chrono::{DateTime, Utc}; use futures_util::stream::Stream; use futures_util::StreamExt; -use std::borrow::Cow; -use std::mem; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; @@ -226,59 +224,6 @@ async fn render_sql( resp_recv.await.map_err(ErrorInternalServerError) } -#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Clone)] -#[serde(untagged)] -pub enum SingleOrVec { - Single(String), - Vec(Vec), -} - -impl std::fmt::Display for SingleOrVec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SingleOrVec::Single(x) => write!(f, "{x}"), - SingleOrVec::Vec(v) => { - write!(f, "[")?; - let mut it = v.iter(); - if let Some(first) = it.next() { - write!(f, "{first}")?; - } - for item in it { - write!(f, ", {item}")?; - } - write!(f, "]") - } - } - } -} - -impl SingleOrVec { - pub(crate) fn merge(&mut self, other: Self) { - match (self, other) { - (Self::Single(old), Self::Single(new)) => *old = new, - (old, mut new) => { - let mut v = old.take_vec(); - v.extend_from_slice(&new.take_vec()); - *old = Self::Vec(v); - } - } - } - fn take_vec(&mut self) -> Vec { - match self { - SingleOrVec::Single(x) => vec![mem::take(x)], - SingleOrVec::Vec(v) => mem::take(v), - } - } - - #[must_use] - pub fn as_json_str(&self) -> Cow<'_, str> { - match self { - SingleOrVec::Single(x) => Cow::Borrowed(x), - SingleOrVec::Vec(v) => Cow::Owned(serde_json::to_string(v).unwrap()), - } - } -} - async fn process_sql_request( req: &mut ServiceRequest, sql_path: PathBuf, diff --git a/src/webserver/http_request_info.rs b/src/webserver/http_request_info.rs index ff0f3114..8f9cbac1 100644 --- a/src/webserver/http_request_info.rs +++ b/src/webserver/http_request_info.rs @@ -278,8 +278,8 @@ async fn is_file_field_empty( #[cfg(test)] mod test { - use super::super::http::SingleOrVec; use super::*; + use crate::webserver::single_or_vec::SingleOrVec; use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming}; use actix_web::{http::header::ContentType, test::TestRequest}; diff --git a/src/webserver/mod.rs b/src/webserver/mod.rs index 484ad40d..4a70d2a1 100644 --- a/src/webserver/mod.rs +++ b/src/webserver/mod.rs @@ -48,4 +48,5 @@ pub use database::migrations::apply; pub mod oidc; pub mod response_writer; pub mod routing; +mod single_or_vec; mod static_content; diff --git a/src/webserver/request_variables.rs b/src/webserver/request_variables.rs index 0aa9879a..245d8a0d 100644 --- a/src/webserver/request_variables.rs +++ b/src/webserver/request_variables.rs @@ -1,6 +1,6 @@ use std::collections::{hash_map::Entry, HashMap}; -use super::http::SingleOrVec; +use crate::webserver::single_or_vec::SingleOrVec; pub type ParamMap = HashMap; diff --git a/src/webserver/single_or_vec.rs b/src/webserver/single_or_vec.rs new file mode 100644 index 00000000..e5438678 --- /dev/null +++ b/src/webserver/single_or_vec.rs @@ -0,0 +1,124 @@ +use serde::de::Error; +use std::borrow::Cow; +use std::mem; + +#[derive(Debug, serde::Serialize, PartialEq, Clone)] +#[serde(untagged)] +pub enum SingleOrVec { + Single(String), + Vec(Vec), +} + +impl<'de> serde::Deserialize<'de> for SingleOrVec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::String(s) => Ok(SingleOrVec::Single(s)), + serde_json::Value::Array(values) => { + let mut strings = Vec::with_capacity(values.len()); + for (idx, item) in values.into_iter().enumerate() { + match item { + serde_json::Value::String(s) => strings.push(s), + other => { + return Err(D::Error::custom(format!( + "expected an array of strings, but item at index {idx} is {other}" + ))) + } + } + } + Ok(SingleOrVec::Vec(strings)) + } + other => Err(D::Error::custom(format!( + "expected a string or an array of strings, but found {other}" + ))), + } + } +} + +impl std::fmt::Display for SingleOrVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SingleOrVec::Single(x) => write!(f, "{x}"), + SingleOrVec::Vec(v) => { + write!(f, "[")?; + let mut it = v.iter(); + if let Some(first) = it.next() { + write!(f, "{first}")?; + } + for item in it { + write!(f, ", {item}")?; + } + write!(f, "]") + } + } + } +} + +impl SingleOrVec { + pub(crate) fn merge(&mut self, other: Self) { + match (self, other) { + (Self::Single(old), Self::Single(new)) => *old = new, + (old, mut new) => { + let mut v = old.take_vec(); + v.extend_from_slice(&new.take_vec()); + *old = Self::Vec(v); + } + } + } + + fn take_vec(&mut self) -> Vec { + match self { + SingleOrVec::Single(x) => vec![mem::take(x)], + SingleOrVec::Vec(v) => mem::take(v), + } + } + + #[must_use] + pub fn as_json_str(&self) -> Cow<'_, str> { + match self { + SingleOrVec::Single(x) => Cow::Borrowed(x), + SingleOrVec::Vec(v) => Cow::Owned(serde_json::to_string(v).unwrap()), + } + } +} + +#[cfg(test)] +mod single_or_vec_tests { + use super::SingleOrVec; + + #[test] + fn deserializes_string_and_array_values() { + let single: SingleOrVec = serde_json::from_str(r#""hello""#).unwrap(); + assert_eq!(single, SingleOrVec::Single("hello".to_string())); + let array: SingleOrVec = serde_json::from_str(r#"["a","b"]"#).unwrap(); + assert_eq!( + array, + SingleOrVec::Vec(vec!["a".to_string(), "b".to_string()]) + ); + } + + #[test] + fn rejects_non_string_items() { + let err = serde_json::from_str::(r#"["a", 1]"#).unwrap_err(); + assert!( + err.to_string() + .contains("expected an array of strings, but item at index 1 is 1"), + "{err}" + ); + } + + #[test] + fn displays_single_value() { + let single = SingleOrVec::Single("hello".to_string()); + assert_eq!(single.to_string(), "hello"); + } + + #[test] + fn displays_array_values() { + let array = SingleOrVec::Vec(vec!["a".to_string(), "b".to_string()]); + assert_eq!(array.to_string(), "[a, b]"); + } +}