use sea_query::{Alias, Expr};
use super::Predicate;
use super::model::{HydrateRelated, Model};
use super::queryset::{Manager, QuerySet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReverseError {
NoForeignKey {
child: &'static str,
parent_table: &'static str,
},
Ambiguous {
child: &'static str,
parent_table: &'static str,
candidates: Vec<&'static str>,
},
UnknownColumn { child: &'static str, column: String },
NotAForeignKey {
child: &'static str,
column: String,
parent_table: &'static str,
},
NonI64Pk { parent: &'static str },
}
impl std::fmt::Display for ReverseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReverseError::NoForeignKey {
child,
parent_table,
} => write!(
f,
"umbral::orm::reverse: `{child}` has no foreign key to `{parent_table}` \
— there is no reverse relation to follow"
),
ReverseError::Ambiguous {
child,
parent_table,
candidates,
} => write!(
f,
"umbral::orm::reverse: `{child}` has multiple foreign keys to `{parent_table}` \
({}). Disambiguate with `reverse_via::<{child}>(\"<column>\")`",
candidates.join(", ")
),
ReverseError::UnknownColumn { child, column } => write!(
f,
"umbral::orm::reverse_via: `{child}` has no column `{column}`"
),
ReverseError::NotAForeignKey {
child,
column,
parent_table,
} => write!(
f,
"umbral::orm::reverse_via: column `{column}` on `{child}` is not a foreign key \
to `{parent_table}`"
),
ReverseError::NonI64Pk { parent } => write!(
f,
"umbral::orm::reverse: `{parent}` primary key could not be bound into the \
reverse relation predicate"
),
}
}
}
impl std::error::Error for ReverseError {}
pub trait ReverseRelations: Model + HydrateRelated {
fn reverse<C: Model + HydrateRelated>(&self) -> Result<QuerySet<C>, ReverseError> {
let fk_col = discover_single_fk::<Self, C>()?;
self.reverse_on::<C>(fk_col)
}
fn reverse_via<C: Model + HydrateRelated>(
&self,
fk_col: &str,
) -> Result<QuerySet<C>, ReverseError> {
let spec = C::FIELDS.iter().find(|f| f.name == fk_col).ok_or_else(|| {
ReverseError::UnknownColumn {
child: C::NAME,
column: fk_col.to_string(),
}
})?;
if spec.fk_target != Some(Self::TABLE) {
return Err(ReverseError::NotAForeignKey {
child: C::NAME,
column: fk_col.to_string(),
parent_table: Self::TABLE,
});
}
self.reverse_on::<C>(spec.name)
}
#[doc(hidden)]
fn reverse_on<C: Model + HydrateRelated>(
&self,
fk_col: &'static str,
) -> Result<QuerySet<C>, ReverseError> {
let pk = self
.pk_as_json()
.ok_or(ReverseError::NonI64Pk { parent: Self::NAME })?;
let spec = C::FIELDS.iter().find(|f| f.name == fk_col).ok_or_else(|| {
ReverseError::UnknownColumn {
child: C::NAME,
column: fk_col.to_string(),
}
})?;
let parent_pk_ty = Self::FIELDS.iter().find(|f| f.primary_key).map(|f| f.ty);
let pk_value =
crate::orm::write::json_to_sea_value(spec.ty, &pk, false, fk_col, parent_pk_ty)
.map_err(|_| ReverseError::NonI64Pk { parent: Self::NAME })?;
let predicate: Predicate<C> = Predicate::new(Expr::col(Alias::new(fk_col)).eq(pk_value));
Ok(Manager::<C>::new().filter(predicate))
}
}
impl<T: Model + HydrateRelated> ReverseRelations for T {}
fn discover_single_fk<P: Model, C: Model>() -> Result<&'static str, ReverseError> {
let candidates: Vec<&'static str> = C::FIELDS
.iter()
.filter(|f| f.fk_target == Some(P::TABLE))
.map(|f| f.name)
.collect();
match candidates.len() {
1 => Ok(candidates[0]),
0 => Err(ReverseError::NoForeignKey {
child: C::NAME,
parent_table: P::TABLE,
}),
_ => Err(ReverseError::Ambiguous {
child: C::NAME,
parent_table: P::TABLE,
candidates,
}),
}
}