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, 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, f.label)),
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, &str), DeclarationErrorKind> {
90        self.field
91            .as_ref()
92            .map(|(name, label)| (name.as_str(), label.as_str()))
93            .map_err(Clone::clone)
94    }
95}
96
97/// Build a [`RelationLens`] from a model and one of its relation fields.
98///
99/// `relation!(Post.author)` is the include `Post::fields().author()` paired with
100/// `|post| &post.author`.
101///
102/// ```rust
103/// # use tablo_core::{RelationLens, relation};
104/// # use toasty::Deferred;
105/// # mod blog {
106/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
107/// #     pub struct Author { #[key] #[auto] pub id: uuid::Uuid, pub name: String }
108/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
109/// #     pub struct Post {
110/// #         #[key] #[auto] pub id: uuid::Uuid,
111/// #         pub author_id: uuid::Uuid,
112/// #         #[belongs_to(key = author_id, references = id)]
113/// #         pub author: toasty::Deferred<Author>,
114/// #         #[has_many]
115/// #         pub comments: toasty::Deferred<Vec<Comment>>,
116/// #     }
117/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
118/// #     pub struct Comment {
119/// #         #[key] #[auto] pub id: uuid::Uuid,
120/// #         #[index]
121/// #         pub post_id: uuid::Uuid,
122/// #         #[belongs_to(key = post_id, references = id)]
123/// #         pub post: toasty::Deferred<Post>,
124/// #     }
125/// # }
126/// # use blog::{Author, Comment, Post};
127/// # fn main() {
128/// let author: RelationLens<Post, Deferred<Author>> = relation!(Post.author);
129/// let comments: RelationLens<Post, Deferred<Vec<Comment>>> = relation!(Post.comments);
130/// let qualified = relation!(blog::Post.author);
131/// # let _ = (author, comments, qualified);
132/// # }
133/// ```
134#[macro_export]
135macro_rules! relation {
136    ($($model:ident)::+ . $field:ident) => {
137        $crate::RelationLens::new(
138            <$($model)::+>::fields().$field(),
139            |record: &$($model)::+| &record.$field,
140        )
141    };
142}