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}