Skip to main content

tablo_core/column/relation/
lens.rs

1//! [`RelationLens`] and [`relation!`](crate::relation!): a relation field named once, paired with
2//! its include and its accessors.
3
4use derive_where::derive_where;
5use toasty::stmt::Path;
6
7use crate::{DeclarationErrorKind, column::Includes, toasty_compat::model};
8
9/// A relation field's include, which loads it, paired with the function that reads the loaded
10/// relation off a record.
11///
12/// [`relation!`](crate::relation!) builds one from a single field name, so the column that reads
13/// the relation always declares its include:
14///
15/// ```rust
16/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
17/// # struct Author { #[key] #[auto] id: uuid::Uuid, name: String }
18/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
19/// # struct Post {
20/// #     #[key] #[auto] id: uuid::Uuid,
21/// #     author_id: uuid::Uuid,
22/// #     #[belongs_to(key = author_id, references = id)]
23/// #     author: toasty::Deferred<Author>,
24/// # }
25/// # use tablo_core::{RelationColumn, relation};
26/// RelationColumn::unscoped(relation!(Post.author), |a: &Author| a.name.clone());
27/// ```
28#[derive_where(Clone, Debug)]
29pub struct RelationLens<M, R> {
30    includes: Includes<M>,
31    /// The relation field's name, or why the path names no single field.
32    field: Result<String, DeclarationErrorKind>,
33    /// The relation field, and the foreign keys of a `belongs_to`: what showing it reads.
34    reads: Vec<String>,
35    #[derive_where(skip(Debug))]
36    pub(super) read: fn(&M) -> &R,
37}
38
39impl<M, R> RelationLens<M, R>
40where
41    M: toasty::schema::Model,
42{
43    /// Pair the relation `path` with the function reading the same field;
44    /// [`relation!`](crate::relation!) writes both from one field name.
45    #[doc(hidden)]
46    pub fn new<T>(path: impl Into<Path<M, T>>, read: fn(&M) -> &R) -> Self {
47        let path = path.into();
48        let info = model::field::<M>(&model::ModelPath::of(&path));
49        let reads = match &info {
50            Ok(field) => std::iter::once(field.name.clone())
51                .chain(
52                    model::belongs_to::<M>(field.index)
53                        .map(|relation| relation.foreign_keys)
54                        .unwrap_or_default(),
55                )
56                .collect(),
57            Err(_) => Vec::new(),
58        };
59        Self {
60            includes: Includes::new().with(path),
61            field: info.map(|f| f.name),
62            reads,
63            read,
64        }
65    }
66}
67
68impl<M, R> RelationLens<M, R> {
69    /// The include that loads the relation.
70    pub fn includes(&self) -> &Includes<M> {
71        &self.includes
72    }
73
74    /// Read the relation field off `record`.
75    pub fn read<'a>(&self, record: &'a M) -> &'a R {
76        (self.read)(record)
77    }
78
79    /// The fields showing the relation reads.
80    pub(crate) fn reads(&self) -> crate::lens::Reads<M> {
81        self.reads
82            .iter()
83            .fold(crate::lens::Reads::nothing(), |reads, field| {
84                reads.named(field.clone())
85            })
86    }
87
88    /// The field's name and label, or why the path names no single field.
89    pub(crate) fn field(&self) -> Result<&str, DeclarationErrorKind> {
90        self.field.as_deref().map_err(Clone::clone)
91    }
92}
93
94/// Build a [`RelationLens`] from a model and one of its relation fields.
95///
96/// `relation!(Post.author)` is the include `Post::fields().author()` paired with
97/// `|post| &post.author`.
98///
99/// ```rust
100/// # use tablo_core::{RelationLens, relation};
101/// # use toasty::Deferred;
102/// # mod blog {
103/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
104/// #     pub struct Author { #[key] #[auto] pub id: uuid::Uuid, pub name: String }
105/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
106/// #     pub struct Post {
107/// #         #[key] #[auto] pub id: uuid::Uuid,
108/// #         pub author_id: uuid::Uuid,
109/// #         #[belongs_to(key = author_id, references = id)]
110/// #         pub author: toasty::Deferred<Author>,
111/// #         #[has_many]
112/// #         pub comments: toasty::Deferred<Vec<Comment>>,
113/// #     }
114/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
115/// #     pub struct Comment {
116/// #         #[key] #[auto] pub id: uuid::Uuid,
117/// #         #[index]
118/// #         pub post_id: uuid::Uuid,
119/// #         #[belongs_to(key = post_id, references = id)]
120/// #         pub post: toasty::Deferred<Post>,
121/// #     }
122/// # }
123/// # use blog::{Author, Comment, Post};
124/// # fn main() {
125/// let author: RelationLens<Post, Deferred<Author>> = relation!(Post.author);
126/// let comments: RelationLens<Post, Deferred<Vec<Comment>>> = relation!(Post.comments);
127/// let qualified = relation!(blog::Post.author);
128/// # let _ = (author, comments, qualified);
129/// # }
130/// ```
131#[macro_export]
132macro_rules! relation {
133    ($($model:ident)::+ . $field:ident) => {
134        $crate::RelationLens::new(
135            <$($model)::+>::fields().$field(),
136            |record: &$($model)::+| &record.$field,
137        )
138    };
139}