use std::any::Any;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error(
"association `{association}` on `{model}` was accessed but not preloaded; \
add it to the `.preload(...)` set on the finder query"
)]
pub struct NotLoaded {
pub model: &'static str,
pub association: &'static str,
}
impl NotLoaded {
#[must_use]
pub const fn new(model: &'static str, association: &'static str) -> Self {
Self { model, association }
}
}
#[derive(Default)]
pub struct Associations {
map: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
}
impl Associations {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert<T: Any + Send + Sync>(&mut self, key: &'static str, value: T) {
self.map.insert(key, Box::new(value));
}
#[must_use]
pub fn get<T: Any + Send + Sync>(&self, key: &'static str) -> Option<&T> {
self.map.get(key).and_then(|b| b.downcast_ref::<T>())
}
#[must_use]
pub fn get_mut<T: Any + Send + Sync>(&mut self, key: &'static str) -> Option<&mut T> {
self.map.get_mut(key).and_then(|b| b.downcast_mut::<T>())
}
#[must_use]
pub fn contains(&self, key: &'static str) -> bool {
self.map.contains_key(key)
}
#[must_use]
pub fn len(&self) -> usize {
self.map.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
impl std::fmt::Debug for Associations {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut keys: Vec<&&str> = self.map.keys().collect();
keys.sort_unstable();
f.debug_struct("Associations")
.field("loaded", &keys)
.finish()
}
}
#[derive(Debug)]
pub struct Preloaded<T> {
inner: T,
associations: Associations,
}
impl<T> Preloaded<T> {
#[must_use]
pub fn new(inner: T) -> Self {
Self {
inner,
associations: Associations::new(),
}
}
#[must_use]
pub const fn inner(&self) -> &T {
&self.inner
}
#[must_use]
pub fn into_inner(self) -> T {
self.inner
}
#[must_use]
pub const fn associations(&self) -> &Associations {
&self.associations
}
pub const fn associations_mut(&mut self) -> &mut Associations {
&mut self.associations
}
}
impl<T> Deref for Preloaded<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner
}
}
impl<T> DerefMut for Preloaded<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.inner
}
}
impl<T> From<T> for Preloaded<T> {
fn from(inner: T) -> Self {
Self::new(inner)
}
}
impl<T: serde::Serialize> serde::Serialize for Preloaded<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.inner.serialize(serializer)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoPreload;
#[doc(hidden)]
pub trait AutumnPreloadScopeExt {
#[must_use]
fn __autumn_repo_soft_delete_scope() -> bool {
false
}
#[must_use]
fn __autumn_repo_tenant_scope() -> bool {
false
}
}
impl<T: ?Sized> AutumnPreloadScopeExt for T {}
#[doc(hidden)]
pub trait FkKey {
fn autumn_fk_key(&self) -> ::core::option::Option<i64>;
}
impl FkKey for i64 {
fn autumn_fk_key(&self) -> ::core::option::Option<i64> {
::core::option::Option::Some(*self)
}
}
impl FkKey for ::core::option::Option<i64> {
fn autumn_fk_key(&self) -> ::core::option::Option<i64> {
*self
}
}
#[cfg(feature = "db")]
tokio::task_local! {
#[doc(hidden)]
pub static PRELOAD_ACROSS_TENANTS: bool;
}
#[cfg(feature = "db")]
#[doc(hidden)]
#[must_use]
pub fn preload_across_tenants() -> bool {
PRELOAD_ACROSS_TENANTS.try_with(|v| *v).unwrap_or(false)
}
#[cfg(feature = "db")]
#[macro_export]
macro_rules! impl_preloadable_leaf {
($ty:ty) => {
impl $ty {
#[doc(hidden)]
pub fn __autumn_preload_retain(
rows: ::std::vec::Vec<Self>,
) -> $crate::AutumnResult<::std::vec::Vec<Self>> {
::core::result::Result::Ok(rows)
}
#[doc(hidden)]
pub fn __autumn_preload_keep(
row: Self,
) -> $crate::AutumnResult<::core::option::Option<Self>> {
::core::result::Result::Ok(::core::option::Option::Some(row))
}
}
impl $crate::preload::Preloadable for $ty {
type Spec = $crate::preload::NoPreload;
fn load_associations<'__a>(
_records: &'__a mut [$crate::preload::Preloaded<Self>],
_spec: &'__a Self::Spec,
_conn: &'__a mut $crate::RuntimeConnection,
) -> $crate::preload::PreloadFuture<'__a> {
::std::boxed::Box::pin(async move { ::core::result::Result::Ok(()) })
}
}
};
}
#[cfg(feature = "db")]
pub use db_support::{PreloadFuture, Preloadable};
#[cfg(feature = "db")]
mod db_support {
use super::Preloaded;
use crate::AutumnResult;
use std::future::Future;
use std::pin::Pin;
pub type PreloadFuture<'a> = Pin<Box<dyn Future<Output = AutumnResult<()>> + Send + 'a>>;
pub trait Preloadable: Sized + Send + Sync + 'static {
type Spec: Default + Send + Sync + 'static;
fn load_associations<'a>(
records: &'a mut [Preloaded<Self>],
spec: &'a Self::Spec,
conn: &'a mut crate::db::RuntimeConnection,
) -> PreloadFuture<'a>;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[derive(Debug, PartialEq)]
struct User {
id: i64,
name: String,
}
#[derive(Debug, PartialEq)]
struct Post {
id: i64,
author_id: i64,
title: String,
}
#[derive(Debug, PartialEq)]
struct Comment {
id: i64,
post_id: i64,
body: String,
}
#[test]
fn deref_exposes_inner_fields() {
let post = Preloaded::new(Post {
id: 1,
author_id: 7,
title: "hello".into(),
});
assert_eq!(post.id, 1);
assert_eq!(post.title, "hello");
}
#[test]
fn belongs_to_happy_path_returns_loaded_parent() {
let mut post = Preloaded::new(Post {
id: 1,
author_id: 7,
title: "t".into(),
});
let author = Arc::new(Preloaded::new(User {
id: 7,
name: "ada".into(),
}));
post.associations_mut()
.insert::<Option<Arc<Preloaded<User>>>>("author", Some(author));
let got = post
.associations()
.get::<Option<Arc<Preloaded<User>>>>("author")
.expect("author preloaded")
.as_ref()
.expect("author present");
assert_eq!(got.name, "ada");
}
#[test]
fn belongs_to_missing_parent_is_some_none() {
let mut post = Preloaded::new(Post {
id: 1,
author_id: 99,
title: "t".into(),
});
post.associations_mut()
.insert::<Option<Arc<Preloaded<User>>>>("author", None);
assert!(post.associations().contains("author"));
let got = post
.associations()
.get::<Option<Arc<Preloaded<User>>>>("author")
.unwrap();
assert!(got.is_none());
}
#[test]
fn has_many_groups_children() {
let mut post = Preloaded::new(Post {
id: 1,
author_id: 7,
title: "t".into(),
});
let comments = vec![
Preloaded::new(Comment {
id: 10,
post_id: 1,
body: "a".into(),
}),
Preloaded::new(Comment {
id: 11,
post_id: 1,
body: "b".into(),
}),
];
post.associations_mut()
.insert::<Vec<Preloaded<Comment>>>("comments", comments);
let got = post
.associations()
.get::<Vec<Preloaded<Comment>>>("comments")
.unwrap();
assert_eq!(got.len(), 2);
assert_eq!(got[0].body, "a");
}
#[test]
fn has_many_empty_children_is_empty_vec() {
let mut post = Preloaded::new(Post {
id: 1,
author_id: 7,
title: "t".into(),
});
post.associations_mut()
.insert::<Vec<Preloaded<Comment>>>("comments", Vec::new());
let got = post
.associations()
.get::<Vec<Preloaded<Comment>>>("comments")
.unwrap();
assert!(got.is_empty());
}
#[test]
fn not_preloaded_key_is_absent() {
let post = Preloaded::new(Post {
id: 1,
author_id: 7,
title: "t".into(),
});
assert!(!post.associations().contains("author"));
assert!(
post.associations()
.get::<Option<Arc<Preloaded<User>>>>("author")
.is_none()
);
}
#[test]
fn not_loaded_error_carries_model_and_association() {
let err = NotLoaded::new("Post", "author");
assert_eq!(err.model, "Post");
assert_eq!(err.association, "author");
let msg = err.to_string();
assert!(msg.contains("Post"));
assert!(msg.contains("author"));
assert!(msg.contains("not preloaded"));
}
#[test]
fn shared_parent_via_arc_is_cheap_to_clone() {
let author = Arc::new(Preloaded::new(User {
id: 7,
name: "ada".into(),
}));
let mut p1 = Preloaded::new(Post {
id: 1,
author_id: 7,
title: "t1".into(),
});
let mut p2 = Preloaded::new(Post {
id: 2,
author_id: 7,
title: "t2".into(),
});
p1.associations_mut()
.insert::<Option<Arc<Preloaded<User>>>>("author", Some(Arc::clone(&author)));
p2.associations_mut()
.insert::<Option<Arc<Preloaded<User>>>>("author", Some(author));
let a1 = p1
.associations()
.get::<Option<Arc<Preloaded<User>>>>("author")
.unwrap()
.as_ref()
.unwrap();
let a2 = p2
.associations()
.get::<Option<Arc<Preloaded<User>>>>("author")
.unwrap()
.as_ref()
.unwrap();
assert!(Arc::ptr_eq(a1, a2));
}
}