use crate::traits::expressive::Expressive;
use crate::{ExprDataSource, Expression};
use std::marker::PhantomData;
use vantage_core::Result;
pub struct AssociatedExpression<'a, DS, T, R>
where
DS: ExprDataSource<T>,
{
expr: Expression<T>,
datasource: &'a DS,
_result: PhantomData<R>,
}
impl<'a, DS, T, R> AssociatedExpression<'a, DS, T, R>
where
DS: ExprDataSource<T>,
{
pub fn new(expr: Expression<T>, datasource: &'a DS) -> Self {
Self {
expr,
datasource,
_result: PhantomData,
}
}
pub async fn get(&self) -> Result<R>
where
R: TryFrom<T>,
R::Error: Into<vantage_core::VantageError>,
{
let raw_result = self.datasource.execute(&self.expr).await?;
R::try_from(raw_result).map_err(Into::into)
}
pub fn expression(&self) -> &Expression<T> {
&self.expr
}
pub fn datasource(&self) -> &'a DS {
self.datasource
}
}
impl<'a, DS, T: Clone, R> Expressive<T> for AssociatedExpression<'a, DS, T, R>
where
DS: ExprDataSource<T>,
{
fn expr(&self) -> Expression<T> {
self.expr.clone()
}
}
impl<'a, DS, T: Clone, R> Clone for AssociatedExpression<'a, DS, T, R>
where
DS: ExprDataSource<T>,
{
fn clone(&self) -> Self {
Self {
expr: self.expr.clone(),
datasource: self.datasource,
_result: PhantomData,
}
}
}
impl<'a, DS, T, R> std::fmt::Debug for AssociatedExpression<'a, DS, T, R>
where
DS: ExprDataSource<T>,
T: std::fmt::Debug + std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AssociatedExpression")
.field("expr", &self.expr.preview())
.field("return_type", &std::any::type_name::<R>())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr;
use crate::mocks::MockBuilder;
use serde_json::Value;
#[derive(Debug, PartialEq)]
struct Email {
pub name: String,
pub domain: String,
}
impl Email {
pub fn new(name: &str, domain: &str) -> Self {
Self {
name: name.to_string(),
domain: domain.to_string(),
}
}
}
impl TryFrom<Value> for Email {
type Error = vantage_core::VantageError;
fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
let email_str = value
.as_str()
.ok_or_else(|| vantage_core::VantageError::other("Expected email string"))?;
let parts: Vec<&str> = email_str.split('@').collect();
if parts.len() != 2 {
return Err(vantage_core::VantageError::other("Invalid email format"));
}
Ok(Email::new(parts[0], parts[1]))
}
}
fn get_authenticated_users_email(
ds: &MockBuilder,
) -> AssociatedExpression<'_, MockBuilder, Value, Email> {
let query = expr!(
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())"
);
ds.associate::<Email>(query)
}
#[tokio::test]
async fn test_email_direct_execution() {
use crate::mocks::mock_builder;
let ds = mock_builder::new()
.on_exact_select(
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())",
serde_json::json!("foo@example.com")
);
let email_associated = get_authenticated_users_email(&ds);
let result = email_associated.get().await.unwrap();
assert_eq!(result.name, "foo");
assert_eq!(result.domain, "example.com");
}
#[tokio::test]
async fn test_email_in_query_composition() {
use crate::mocks::mock_builder;
let ds = mock_builder::new()
.with_flattening()
.on_exact_select(
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())",
serde_json::json!("foo@example.com")
)
.on_exact_select(
"SELECT balance FROM accounts WHERE email = SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())",
serde_json::json!(1250.50)
);
let email_associated = get_authenticated_users_email(&ds);
let balance_query = expr!(
"SELECT balance FROM accounts WHERE email = {}",
(email_associated)
);
let result = ds.execute(&balance_query).await.unwrap();
assert_eq!(result, serde_json::json!(1250.50));
}
#[test]
fn test_associated_expression_debug() {
use crate::mocks::mock_builder;
let ds = mock_builder::new()
.on_exact_select(
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())",
serde_json::json!("test@example.com")
);
let associated = get_authenticated_users_email(&ds);
let debug_str = format!("{:?}", associated);
assert!(debug_str.contains("AssociatedExpression"));
assert!(debug_str.contains("Email"));
assert!(debug_str.contains("SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())"));
}
#[test]
fn test_associated_expression_accessors() {
use crate::mocks::mock_builder;
let ds = mock_builder::new()
.on_exact_select(
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())",
serde_json::json!("user@test.com")
);
let associated = get_authenticated_users_email(&ds);
assert_eq!(
associated.expression().preview(),
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())"
);
assert_eq!(associated.expression().parameters.len(), 0);
assert!(std::ptr::eq(associated.datasource(), &ds));
}
#[test]
fn test_ergonomic_associate_method() {
use crate::mocks::mock_builder;
let ds = mock_builder::new()
.on_exact_select(
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())",
serde_json::json!("test@example.org")
);
let associated = get_authenticated_users_email(&ds);
assert_eq!(
associated.expression().preview(),
"SELECT email FROM users WHERE id = (SELECT user_id FROM sessions WHERE token = current_session())"
);
assert_eq!(associated.expression().parameters.len(), 0);
assert!(std::ptr::eq(associated.datasource(), &ds));
}
}