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