Skip to main content

tablo_core/resource/
relation.rs

1//! Relation tables: a record page's related rows through the related resource's own list table.
2
3use std::{any::TypeId, sync::Arc};
4
5use toasty::stmt::{Expr, IntoExpr, Path};
6
7use super::Resource;
8use crate::{
9    form::FormScalar,
10    toasty_compat::{
11        model::{self, ModelPath},
12        pk,
13    },
14};
15
16/// One relation of a parent resource's records.
17///
18/// Declared by [`ResourceDef::relation`](super::ResourceDef::relation); the related resource
19/// must be registered on the same panel, which keys the relation by its slug:
20///
21/// ```text
22/// ResourceDef::new().relation(Relation::has_many::<CommentResource>(Comment::fields().post_id()))
23/// ```
24pub struct Relation<P> {
25    pub(crate) child: TypeId,
26    pub(crate) child_name: &'static str,
27    pub(crate) label: Option<String>,
28    pub(crate) foreign_key: String,
29    bind: BindFn<P>,
30    /// Why `foreign_key` binds no column, when it does not.
31    pub(crate) misdeclared: Option<crate::DeclarationErrorKind>,
32}
33
34/// An owner's rows filter on the child.
35type BindFn<P> = Arc<dyn Fn(&P) -> (Expr<bool>, String) + Send + Sync>;
36
37/// A foreign-key column type referencing a primary key of type `K`: `K` itself, or `Option<K>`
38/// for an optional reference. Sealed: these are the only two.
39pub trait ForeignKey<K>:
40    sealed::Sealed<K> + IntoExpr<Self> + FormScalar + Send + Sync + 'static
41{
42}
43
44impl<K> ForeignKey<K> for K where K: IntoExpr<K> + FormScalar + Send + Sync + 'static {}
45
46impl<K> ForeignKey<K> for Option<K> where
47    Option<K>: IntoExpr<Self> + FormScalar + Send + Sync + 'static
48{
49}
50
51mod sealed {
52    pub trait Sealed<K> {}
53
54    impl<K> Sealed<K> for K {}
55
56    impl<K> Sealed<K> for Option<K> {}
57}
58
59impl<P> Relation<P>
60where
61    P: toasty::schema::Model + IntoExpr<P> + 'static,
62{
63    /// Declare a `has_many` child resource whose `foreign_key` holds the owner's primary key, a
64    /// single column.
65    ///
66    /// ```text
67    /// Relation::has_many::<CommentResource>(Comment::fields().post_id())
68    /// ```
69    pub fn has_many<C>(foreign_key: Path<C::Model, impl ForeignKey<P::PrimaryKey>>) -> Self
70    where
71        C: Resource,
72    {
73        Self::bound_by::<C, _>(foreign_key)
74    }
75
76    fn bound_by<C, T>(foreign_key: Path<C::Model, T>) -> Self
77    where
78        C: Resource,
79        T: ForeignKey<P::PrimaryKey>,
80    {
81        // A foreign key is one column of the child: it binds against the child's model alone.
82        let (column, misdeclared) = match model::field::<C::Model>(&ModelPath::of(&foreign_key)) {
83            Ok(field) => (field.name, None),
84            Err(error) => (String::new(), Some(error)),
85        };
86        Self {
87            child: TypeId::of::<C>(),
88            child_name: std::any::type_name::<C>(),
89            label: None,
90            foreign_key: column,
91            misdeclared,
92            bind: Arc::new(move |owner| {
93                let scope = foreign_key.clone().eq(pk::pk_expr::<P, T>(owner));
94                (scope, pk::pk_text(owner))
95            }),
96        }
97    }
98}
99
100impl<P> Relation<P> {
101    /// Titles the section `label` instead of the related resource's plural label.
102    #[must_use]
103    pub fn label(mut self, label: impl Into<String>) -> Self {
104        self.label = Some(label.into());
105        self
106    }
107
108    /// The child's rows that belong to `owner`, and the owner's primary key as text.
109    pub(crate) fn bind(&self, owner: &P) -> (Expr<bool>, String) {
110        (self.bind)(owner)
111    }
112}
113
114impl<P> std::fmt::Debug for Relation<P> {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct("Relation")
117            .field("child", &self.child_name)
118            .field("label", &self.label)
119            .field("foreign_key", &self.foreign_key)
120            .finish_non_exhaustive()
121    }
122}