use crate::model::ModelExt;
use crate::pool::Connection;
use crate::query::QueryBuilder;
use crate::validation::Validate;
impl<M: ModelExt + Validate> QueryBuilder<M> {
pub async fn insert_validated(
&self,
model: &M,
conn: &mut dyn Connection,
) -> Result<u64, crate::DbError> {
model
.validate()
.map_err(|e| crate::DbError::Validation(e.to_string()))?;
let data = model.to_value();
let (sql, params) = self.build_insert(&data);
if sql.is_empty() {
return Err(crate::DbError::InvalidInput(
"empty insert data after validation".to_string(),
));
}
conn.execute_with_params(&sql, ¶ms).await
}
pub async fn update_validated(
&self,
model: &M,
conn: &mut dyn Connection,
) -> Result<u64, crate::DbError> {
model
.validate()
.map_err(|e| crate::DbError::Validation(e.to_string()))?;
let data = model.to_value();
let (sql, params) = self.build_update(&data);
if sql.is_empty() {
return Err(crate::DbError::InvalidInput(
"empty update data after validation".to_string(),
));
}
conn.execute_with_params(&sql, ¶ms).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialect::MySqlDialect;
use crate::model::Model;
use crate::query::QueryBuilder;
use crate::value::Value;
use std::collections::HashMap;
#[derive(Clone, Debug)]
struct TestUser {
id: i64,
email: String,
name: String,
}
impl Model for TestUser {
type PrimaryKey = i64;
fn table_name() -> &'static str {
"users"
}
fn pk(&self) -> Self::PrimaryKey {
self.id
}
fn set_pk(&mut self, pk: Self::PrimaryKey) {
self.id = pk;
}
}
impl ModelExt for TestUser {
fn columns() -> Vec<&'static str> {
vec!["id", "email", "name"]
}
fn fillable() -> Vec<&'static str> {
vec!["email", "name"]
}
fn to_value(&self) -> HashMap<String, Value> {
let mut map = HashMap::new();
map.insert("id".to_string(), Value::I64(self.id));
map.insert("email".to_string(), Value::String(self.email.clone()));
map.insert("name".to_string(), Value::String(self.name.clone()));
map
}
}
impl Validate for TestUser {
fn validate(&self) -> Result<(), crate::validation::ValidationError> {
let results = vec![
crate::validation::rules::validate_email("email", &self.email),
crate::validation::rules::validate_length("name", &self.name, 2, 50),
];
crate::validation::aggregate(results)
}
}
struct MockConnection {
executed: std::sync::Mutex<Vec<String>>,
}
impl MockConnection {
fn new() -> Self {
Self {
executed: std::sync::Mutex::new(Vec::new()),
}
}
}
impl Connection for MockConnection {
fn execute<'a>(
&'a mut self,
sql: &'a str,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<u64, crate::DbError>> + Send + 'a>,
> {
Box::pin(async move {
self.executed.lock().unwrap().push(sql.to_string());
Ok(1)
})
}
fn query<'a>(
&'a mut self,
sql: &'a str,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<crate::pool::QueryRows, crate::DbError>>
+ Send
+ 'a,
>,
> {
let _ = sql;
Box::pin(async { Ok(crate::pool::QueryRows::new()) })
}
fn begin_transaction<'a>(
&'a mut self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
> {
Box::pin(async { Ok(()) })
}
fn commit<'a>(
&'a mut self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
> {
Box::pin(async { Ok(()) })
}
fn rollback<'a>(
&'a mut self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
> {
Box::pin(async { Ok(()) })
}
fn is_connected(&self) -> bool {
true
}
fn ping<'a>(
&'a mut self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
Box::pin(async { true })
}
fn close<'a>(
&'a mut self,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(), crate::DbError>> + Send + 'a>,
> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test]
async fn test_insert_validated_pass() {
let user = TestUser {
id: 1,
email: "test@example.com".to_string(),
name: "Alice".to_string(),
};
let qb = QueryBuilder::<TestUser>::new(Box::new(MySqlDialect));
let mut conn = MockConnection::new();
let result = qb.insert_validated(&user, &mut conn).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
}
#[tokio::test]
async fn test_insert_validated_fail_email() {
let user = TestUser {
id: 1,
email: "not-an-email".to_string(),
name: "Alice".to_string(),
};
let qb = QueryBuilder::<TestUser>::new(Box::new(MySqlDialect));
let mut conn = MockConnection::new();
let result = qb.insert_validated(&user, &mut conn).await;
assert!(matches!(result, Err(crate::DbError::Validation(_))));
assert!(conn.executed.lock().unwrap().is_empty());
}
#[tokio::test]
async fn test_insert_validated_fail_length() {
let user = TestUser {
id: 1,
email: "test@example.com".to_string(),
name: "A".to_string(),
};
let qb = QueryBuilder::<TestUser>::new(Box::new(MySqlDialect));
let mut conn = MockConnection::new();
let result = qb.insert_validated(&user, &mut conn).await;
assert!(matches!(result, Err(crate::DbError::Validation(_))));
assert!(conn.executed.lock().unwrap().is_empty());
}
#[tokio::test]
async fn test_update_validated_pass() {
let user = TestUser {
id: 1,
email: "test@example.com".to_string(),
name: "Alice".to_string(),
};
let qb = QueryBuilder::<TestUser>::new(Box::new(MySqlDialect));
let mut conn = MockConnection::new();
let result = qb.update_validated(&user, &mut conn).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_update_validated_fail() {
let user = TestUser {
id: 1,
email: "bad".to_string(),
name: "Alice".to_string(),
};
let qb = QueryBuilder::<TestUser>::new(Box::new(MySqlDialect));
let mut conn = MockConnection::new();
let result = qb.update_validated(&user, &mut conn).await;
assert!(matches!(result, Err(crate::DbError::Validation(_))));
assert!(conn.executed.lock().unwrap().is_empty());
}
}