Skip to main content

tablo_core/
lens.rs

1//! [`Lens`]: a model field's typed path paired with the reader of its value, built by [`lens!`].
2
3use toasty::stmt::Path;
4
5/// A model field's typed path, which queries filter and sort on, paired with the function that
6/// reads the field off a loaded record.
7///
8/// [`lens!`](crate::lens!) builds one from a single field name, so the two halves cannot name
9/// different fields:
10///
11/// ```rust
12/// # #[derive(Debug, Clone, toasty::Model)]
13/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
14/// # use tablo_core::{TextColumn, lens};
15/// TextColumn::new(lens!(User.name)).searchable();
16/// ```
17///
18/// The filters, the [`Field`](crate::Field) constructors and [`Tenancy`](crate::Tenancy) take a
19/// `Lens` or a plain path; a column needs a `Lens`, since it renders the value it sorts on.
20///
21/// `P` is the type the path names, the field's own type except for a list: Toasty names a
22/// `Vec<T>` field's path `List<T>`.
23pub struct Lens<M, T, P = T> {
24    path: Path<M, P>,
25    read: fn(&M) -> &T,
26}
27
28impl<M, T, P> Lens<M, T, P> {
29    /// Pair `path` with the function reading the same field; [`lens!`](crate::lens!) writes both
30    /// from one field name.
31    #[doc(hidden)]
32    pub fn new(path: impl Into<Path<M, P>>, read: fn(&M) -> &T) -> Self {
33        Self {
34            path: path.into(),
35            read,
36        }
37    }
38
39    /// The field's query path.
40    pub fn path(&self) -> &Path<M, P> {
41        &self.path
42    }
43
44    /// Read the field off `record`.
45    pub fn read<'a>(&self, record: &'a M) -> &'a T {
46        (self.read)(record)
47    }
48}
49
50impl<M, T, P> Clone for Lens<M, T, P> {
51    fn clone(&self) -> Self {
52        Self {
53            path: self.path.clone(),
54            read: self.read,
55        }
56    }
57}
58
59impl<M, T, P> std::fmt::Debug for Lens<M, T, P> {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_tuple("Lens").field(&self.path).finish()
62    }
63}
64
65impl<M, T, P> From<Lens<M, T, P>> for Path<M, P> {
66    fn from(lens: Lens<M, T, P>) -> Self {
67        lens.path
68    }
69}
70
71/// Build a [`Lens`] from a model and a field, or a chain of embedded fields.
72///
73/// `lens!(User.name)` is the path `User::fields().name()` paired with `|user| &user.name`. A
74/// chain through a relation does not compile: a relation's records are not part of the row.
75///
76/// ```rust
77/// # use tablo_core::{Lens, lens};
78/// # #[derive(Debug, Clone, toasty::Model)]
79/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
80/// # #[derive(Debug, Clone, toasty::Embed)]
81/// # struct Seo { title: String }
82/// # mod blog {
83/// #     #[derive(Debug, Clone, toasty::Model)]
84/// #     pub struct Post { #[key] #[auto] pub id: uuid::Uuid, pub title: String, pub seo: super::Seo }
85/// # }
86/// # use blog::Post;
87/// # fn main() {
88/// let name: Lens<User, String> = lens!(User.name);
89/// let seo: Lens<Post, Seo> = lens!(Post.seo); // an embedded value, whole
90/// let title: Lens<Post, String> = lens!(Post.seo.title); // an embedded struct's field
91/// let qualified = lens!(crate::blog::Post.title);
92/// # let _ = (name, seo, title, qualified);
93/// # }
94/// ```
95#[macro_export]
96macro_rules! lens {
97    ($($model:ident)::+ . $($field:ident).+) => {
98        $crate::Lens::new(
99            <$($model)::+>::fields()$(.$field())+,
100            |record: &$($model)::+| &record$(.$field)+,
101        )
102    };
103}