use std::borrow::Cow;
use anyhow::Context as _;
use serde_json::Value;
use super::execute_queries::DbConn;
use super::sqlpage_functions::functions::SqlPageFunctionName;
use crate::webserver::http_request_info::ExecutionContext;
use crate::webserver::single_or_vec::SingleOrVec;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum SqlPageExpr<Input> {
Literal(Value),
Variable(VariableRef),
Input(Input),
Call {
function: SqlPageFunctionName,
arguments: Box<[Self]>,
},
Concat {
arguments: Box<[Self]>,
null_behavior: ConcatNullBehavior,
},
Coalesce(Box<[Self]>),
JsonObject(Box<[(Self, Self)]>),
JsonArray(Box<[Self]>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConcatNullBehavior {
IgnoreNull,
PropagateNull,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum NoRowInput {}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct RowInputId(usize);
impl RowInputId {
pub(super) fn new(index: usize) -> Self {
Self(index)
}
pub(super) fn index(&self) -> usize {
self.0
}
}
pub(crate) type StandaloneExpr = SqlPageExpr<NoRowInput>;
pub(crate) type RowExpr = SqlPageExpr<RowInputId>;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VariableRef {
pub name: String,
pub source: VariableSource,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum VariableSource {
Url,
SetOrUrl,
SetOrForm,
}
pub(crate) enum SqlPageValue<'a> {
Null,
Text(Cow<'a, str>),
Json(Cow<'a, Value>),
}
impl<'a> SqlPageValue<'a> {
pub(crate) fn into_function_argument(self) -> Option<Cow<'a, str>> {
match self {
Self::Null => None,
Self::Text(text) => Some(text),
Self::Json(value) => Some(Cow::Owned(value.to_string())),
}
}
pub(crate) fn into_json(self) -> Value {
match self {
Self::Null => Value::Null,
Self::Text(Cow::Borrowed(text)) => Value::String(text.to_owned()),
Self::Text(Cow::Owned(text)) => Value::String(text),
Self::Json(Cow::Borrowed(value)) => value.clone(),
Self::Json(Cow::Owned(value)) => value,
}
}
}
pub(crate) trait ExprInputs<Input> {
fn take(&mut self, input: &Input) -> anyhow::Result<SqlPageValue<'static>>;
}
pub(crate) struct NoInputs;
impl ExprInputs<NoRowInput> for NoInputs {
fn take(&mut self, input: &NoRowInput) -> anyhow::Result<SqlPageValue<'static>> {
match *input {}
}
}
pub(crate) struct RowInputs(Vec<Option<Value>>);
impl RowInputs {
pub(crate) fn new(values: Vec<Value>) -> Self {
Self(values.into_iter().map(Some).collect())
}
}
impl ExprInputs<RowInputId> for RowInputs {
fn take(&mut self, input: &RowInputId) -> anyhow::Result<SqlPageValue<'static>> {
let value = self
.0
.get_mut(input.index())
.and_then(Option::take)
.with_context(|| {
format!(
"Row input {} is missing or was already consumed",
input.index()
)
})?;
Ok(value_to_sqlpage_value(value))
}
}
fn value_to_sqlpage_value(value: Value) -> SqlPageValue<'static> {
match value {
Value::Null => SqlPageValue::Null,
Value::String(text) => SqlPageValue::Text(Cow::Owned(text)),
value => SqlPageValue::Json(Cow::Owned(value)),
}
}
impl VariableRef {
fn evaluate<'a>(&self, request: &'a ExecutionContext) -> SqlPageValue<'a> {
let value = match self.source {
VariableSource::Url => request
.url_params
.get(&self.name)
.map(SingleOrVec::as_json_str),
VariableSource::SetOrForm => {
if let Some(value) = request.set_variables.borrow().get(&self.name) {
return value.as_ref().map_or(SqlPageValue::Null, |value| {
SqlPageValue::Text(Cow::Owned(value.as_json_str().into_owned()))
});
}
request
.post_variables
.get(&self.name)
.map(SingleOrVec::as_json_str)
}
VariableSource::SetOrUrl => {
if let Some(value) = request.set_variables.borrow().get(&self.name) {
return value.as_ref().map_or(SqlPageValue::Null, |value| {
SqlPageValue::Text(Cow::Owned(value.as_json_str().into_owned()))
});
}
let url_value = request.url_params.get(&self.name);
if request.post_variables.contains_key(&self.name) {
if url_value.is_some() {
log::warn!(
"Deprecation warning! There is both a URL parameter named '{}' and a form field named '{}'. SQLPage is using the URL parameter for ${}. Please use :{} to reference the form field explicitly.",
self.name,
self.name,
self.name,
self.name,
);
} else {
log::warn!(
"Deprecation warning! ${} was used to reference a form field value (a POST variable). This now uses only URL parameters. Please use :{} instead.",
self.name,
self.name,
);
}
}
url_value.map(SingleOrVec::as_json_str)
}
};
value.map_or(SqlPageValue::Null, SqlPageValue::Text)
}
}
impl<Input> SqlPageExpr<Input> {
pub(crate) async fn evaluate<'a>(
&'a self,
request: &'a ExecutionContext,
db_connection: &mut DbConn,
inputs: &mut impl ExprInputs<Input>,
) -> anyhow::Result<SqlPageValue<'a>> {
match self {
Self::Literal(value) => Ok(match value {
Value::Null => SqlPageValue::Null,
Value::String(text) => SqlPageValue::Text(Cow::Borrowed(text)),
value => SqlPageValue::Json(Cow::Borrowed(value)),
}),
Self::Variable(variable) => Ok(variable.evaluate(request)),
Self::Input(input) => inputs.take(input).map(SqlPageValue::into_lifetime),
Self::Call {
function,
arguments,
} => {
let mut values = Vec::with_capacity(arguments.len());
for argument in arguments {
values.push(
Box::pin(argument.evaluate(request, db_connection, inputs))
.await?
.into_function_argument(),
);
}
let result = function
.evaluate(request, db_connection, values)
.await
.with_context(|| format!("Error in function call {function}"))?;
Ok(result.map_or(SqlPageValue::Null, SqlPageValue::Text))
}
Self::Concat {
arguments,
null_behavior,
} => {
let mut result = String::new();
for argument in arguments {
let value = Box::pin(argument.evaluate(request, db_connection, inputs)).await?;
match value.into_function_argument() {
Some(value) => result.push_str(&value),
None if *null_behavior == ConcatNullBehavior::PropagateNull => {
return Ok(SqlPageValue::Null);
}
None => {}
}
}
Ok(SqlPageValue::Text(Cow::Owned(result)))
}
Self::Coalesce(arguments) => {
for argument in arguments {
let value = Box::pin(argument.evaluate(request, db_connection, inputs)).await?;
if !matches!(value, SqlPageValue::Null) {
return Ok(value);
}
}
Ok(SqlPageValue::Null)
}
Self::JsonObject(entries) => {
let mut object = serde_json::Map::with_capacity(entries.len());
for (key, value) in entries {
let key = Box::pin(key.evaluate(request, db_connection, inputs))
.await?
.into_function_argument()
.context("JSON object keys cannot be NULL")?
.into_owned();
let value = Box::pin(value.evaluate(request, db_connection, inputs))
.await?
.into_json();
object.insert(key, value);
}
Ok(SqlPageValue::Json(Cow::Owned(Value::Object(object))))
}
Self::JsonArray(elements) => {
let mut array = Vec::with_capacity(elements.len());
for element in elements {
array.push(
Box::pin(element.evaluate(request, db_connection, inputs))
.await?
.into_json(),
);
}
Ok(SqlPageValue::Json(Cow::Owned(Value::Array(array))))
}
}
}
pub(crate) fn contains_function(&self, expected: SqlPageFunctionName) -> bool {
match self {
Self::Call {
function,
arguments,
} => {
*function == expected
|| arguments
.iter()
.any(|argument| argument.contains_function(expected))
}
Self::Concat { arguments, .. }
| Self::Coalesce(arguments)
| Self::JsonArray(arguments) => arguments
.iter()
.any(|argument| argument.contains_function(expected)),
Self::JsonObject(entries) => entries.iter().any(|(key, value)| {
key.contains_function(expected) || value.contains_function(expected)
}),
Self::Literal(_) | Self::Variable(_) | Self::Input(_) => false,
}
}
}
impl SqlPageValue<'static> {
fn into_lifetime<'a>(self) -> SqlPageValue<'a> {
match self {
Self::Null => SqlPageValue::Null,
Self::Text(text) => SqlPageValue::Text(text),
Self::Json(value) => SqlPageValue::Json(value),
}
}
}