use sea_orm::error::SqlErr;
use sea_orm::{DbErr, RuntimeErr, SqlxError};
use crate::validation::error::ValidationError;
#[derive(Clone)]
struct ConstraintEntry {
pg_name: String,
sqlite_key: Option<String>,
field: String,
message: String,
}
#[derive(Clone, Default)]
pub struct ConstraintMap {
entries: Vec<ConstraintEntry>,
}
impl ConstraintMap {
pub fn new() -> Self {
Self::default()
}
pub fn on(
mut self,
pg_constraint: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
self.entries.push(ConstraintEntry {
pg_name: pg_constraint.into(),
sqlite_key: None,
field: field.into(),
message: message.into(),
});
self
}
pub fn sqlite(mut self, table_col: impl Into<String>) -> Self {
if let Some(last) = self.entries.last_mut() {
last.sqlite_key = Some(table_col.into());
}
self
}
pub fn try_map(&self, err: DbErr) -> Result<ValidationError, DbErr> {
if !matches!(err.sql_err(), Some(SqlErr::UniqueConstraintViolation(_))) {
return Err(err);
}
let pg_name: Option<String> = match &err {
DbErr::Exec(RuntimeErr::SqlxError(SqlxError::Database(e)))
| DbErr::Query(RuntimeErr::SqlxError(SqlxError::Database(e))) => {
e.constraint().map(ToOwned::to_owned)
}
_ => None,
};
let sqlite_key: Option<String> = match err.sql_err() {
Some(SqlErr::UniqueConstraintViolation(msg)) => {
msg.split(": ").nth(1).map(|s| s.trim().to_owned())
}
_ => None,
};
for entry in &self.entries {
let pg_hit = pg_name
.as_deref()
.map(|c| c == entry.pg_name)
.unwrap_or(false);
let sqlite_hit = sqlite_key
.as_deref()
.zip(entry.sqlite_key.as_deref())
.map(|(k, r)| k == r)
.unwrap_or(false);
if pg_hit || sqlite_hit {
let mut ve = ValidationError::new();
ve.add(&entry.field, &entry.message);
return Ok(ve);
}
}
Err(err)
}
}
pub trait MapConstraintExt<T> {
fn map_constraint(
self,
map: &ConstraintMap,
data: &serde_json::Value,
url: impl Into<String>,
) -> Result<T, crate::http::action::ActionError>;
}
impl<T> MapConstraintExt<T> for Result<T, DbErr> {
fn map_constraint(
self,
map: &ConstraintMap,
data: &serde_json::Value,
url: impl Into<String>,
) -> Result<T, crate::http::action::ActionError> {
self.map_err(|err| match map.try_map(err) {
Ok(ve) => ve.with_old_input(data).into_action_error(url),
Err(original) => crate::http::action::ActionError::from(original),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_unique_dberr_passes_through_unchanged() {
let map = ConstraintMap::new()
.on("some_constraint", "field", "message")
.sqlite("t.col");
let err = DbErr::Custom("boom".to_string());
match map.try_map(err) {
Err(DbErr::Custom(msg)) => assert_eq!(msg, "boom"),
other => panic!("expected Err(DbErr::Custom(\"boom\")), got {other:?}"),
}
}
#[test]
fn empty_map_passes_through_any_dberr_unchanged() {
let map = ConstraintMap::new();
let err = DbErr::Custom("any error".to_string());
match map.try_map(err) {
Err(DbErr::Custom(msg)) => assert_eq!(msg, "any error"),
other => panic!("expected Err(DbErr::Custom), got {other:?}"),
}
}
#[test]
fn builder_chains_on_and_sqlite_without_panic() {
let map = ConstraintMap::new()
.on("a_constraint", "field_a", "msg a")
.sqlite("table_a.col_a")
.on("b_constraint", "field_b", "msg b");
let err = DbErr::Custom("test".to_string());
assert!(map.try_map(err).is_err(), "non-UNIQUE must fall through");
let map2 = map.clone();
let err2 = DbErr::Custom("test2".to_string());
assert!(map2.try_map(err2).is_err());
}
#[test]
fn sqlite_no_op_without_prior_on() {
let map = ConstraintMap::new().sqlite("table.col");
assert!(map.entries.is_empty());
}
}