use crate::model::Model;
use crate::relation::path::RelationPath;
use std::any::Any;
use std::collections::HashMap;
pub struct JoinedRow<T: Model> {
pub row: T,
relations: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
}
impl<T: Model + std::fmt::Debug> std::fmt::Debug for JoinedRow<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let keys: Vec<&&'static str> = self.relations.keys().collect();
f.debug_struct("JoinedRow")
.field("row", &self.row)
.field("joined_relations", &keys)
.finish()
}
}
impl<T: Model> JoinedRow<T> {
pub fn get<Target: Model + 'static>(&self, path: RelationPath<T, Target>) -> Option<&Target> {
self.relations
.get(path.source_column())
.and_then(|b| b.downcast_ref::<Target>())
}
pub(crate) fn new(
row: T,
relations: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
) -> Self {
Self { row, relations }
}
pub(crate) fn relations_mut(
&mut self,
) -> &mut HashMap<&'static str, Box<dyn Any + Send + Sync>> {
&mut self.relations
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ModelDescriptor;
use crate::relation::path::{RelationKind, RelationPath};
use crate::types::HeerId;
use std::future::Future;
#[derive(Debug, Clone)]
struct Parent;
#[derive(Debug, Clone)]
struct Child;
macro_rules! dummy_model {
($ty:ty, $table:literal) => {
impl crate::model::__sealed::Sealed for $ty {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for $ty {
type Pk = HeerId;
type Fields = ();
fn table_name() -> &'static str {
$table
}
fn pk_value(&self) -> &HeerId {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: HeerId,
) -> impl Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl Future<Output = Result<(), crate::DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
}
};
}
dummy_model!(Parent, "parents");
dummy_model!(Child, "children");
#[test]
fn joined_row_get_returns_child_when_present() {
let row = Parent;
let child = Child;
let mut relations: HashMap<&'static str, Box<dyn Any + Send + Sync>> = HashMap::new();
relations.insert("child_id", Box::new(child) as Box<dyn Any + Send + Sync>);
let jr: JoinedRow<Parent> = JoinedRow::new(row, relations);
let path: RelationPath<Parent, Child> =
RelationPath::new("child_id", "children", RelationKind::ForeignKey);
assert!(jr.get(path).is_some(), "joined child should be present");
}
#[test]
fn joined_row_get_returns_none_when_missing() {
let jr: JoinedRow<Parent> = JoinedRow::new(Parent, HashMap::new());
let path: RelationPath<Parent, Child> =
RelationPath::new("child_id", "children", RelationKind::ForeignKey);
assert!(jr.get(path).is_none());
}
#[test]
fn joined_row_debug_lists_relation_keys() {
let mut relations: HashMap<&'static str, Box<dyn Any + Send + Sync>> = HashMap::new();
relations.insert("child_id", Box::new(Child) as Box<dyn Any + Send + Sync>);
let jr: JoinedRow<Parent> = JoinedRow::new(Parent, relations);
let debug = format!("{jr:?}");
assert!(
debug.contains("child_id"),
"Debug should list the joined relation keys, got: {debug}"
);
}
#[test]
fn joined_row_relations_mut_allows_stitching() {
let mut jr: JoinedRow<Parent> = JoinedRow::new(Parent, HashMap::new());
jr.relations_mut()
.insert("child_id", Box::new(Child) as Box<dyn Any + Send + Sync>);
let path: RelationPath<Parent, Child> =
RelationPath::new("child_id", "children", RelationKind::ForeignKey);
assert!(
jr.get(path).is_some(),
"relations_mut insert should be observable via get"
);
}
}