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.
20pub struct Lens<M, T> {
21    path: Path<M, T>,
22    read: fn(&M) -> &T,
23}
24
25impl<M, T> Lens<M, T> {
26    /// Pair `path` with the function reading the same field; [`lens!`](crate::lens!) writes both
27    /// from one field name.
28    #[doc(hidden)]
29    pub fn new(path: Path<M, T>, read: fn(&M) -> &T) -> Self {
30        Self { path, read }
31    }
32
33    /// The field's query path.
34    pub fn path(&self) -> &Path<M, T> {
35        &self.path
36    }
37
38    /// Read the field off `record`.
39    pub fn read<'a>(&self, record: &'a M) -> &'a T {
40        (self.read)(record)
41    }
42}
43
44impl<M, T> Clone for Lens<M, T> {
45    fn clone(&self) -> Self {
46        Self {
47            path: self.path.clone(),
48            read: self.read,
49        }
50    }
51}
52
53impl<M, T> std::fmt::Debug for Lens<M, T> {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_tuple("Lens").field(&self.path).finish()
56    }
57}
58
59impl<M, T> From<Lens<M, T>> for Path<M, T> {
60    fn from(lens: Lens<M, T>) -> Self {
61        lens.path
62    }
63}
64
65/// Build a [`Lens`] from a model and a field, or a chain of embedded fields.
66///
67/// `lens!(User.name)` is the path `User::fields().name()` paired with `|user| &user.name`. A
68/// chain through a relation does not compile: a relation's records are not part of the row.
69///
70/// ```text
71/// lens!(User.name)          // Lens<User, String>
72/// lens!(Post.seo.title)     // an embedded struct's field
73/// lens!(crate::blog::Post.title)
74/// ```
75#[macro_export]
76macro_rules! lens {
77    ($($model:ident)::+ . $($field:ident).+) => {
78        $crate::Lens::new(
79            <$($model)::+>::fields()$(.$field())+,
80            |record: &$($model)::+| &record$(.$field)+,
81        )
82    };
83}
84
85#[cfg(test)]
86mod tests;