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//! [`Reads`]: the fields a declaration reads off a record.
3
4use derive_where::derive_where;
5use toasty::stmt::Path;
6
7/// A model field's typed path, which queries filter and sort on, paired with the function that
8/// reads the field off a loaded record.
9///
10/// [`lens!`](crate::lens!) builds one from a single field name, so the two halves cannot name
11/// different fields:
12///
13/// ```rust
14/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
15/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
16/// # use tablo_core::{TextColumn, lens};
17/// TextColumn::new(lens!(User.name)).searchable();
18/// ```
19///
20/// The filters, the [`Field`](crate::Field) constructors and [`Tenancy`](crate::Tenancy) take a
21/// `Lens` or a plain path; a column needs a `Lens`, since it renders the value it sorts on.
22///
23/// `P` is the type the path names, the field's own type except for a list: Toasty names a
24/// `Vec<T>` field's path `List<T>`.
25#[derive_where(Clone, Debug)]
26pub struct Lens<M, T, P = T> {
27    path: Path<M, P>,
28    #[derive_where(skip(Debug))]
29    read: fn(&M) -> &T,
30}
31
32impl<M, T, P> Lens<M, T, P> {
33    /// Pair `path` with the function reading the same field; [`lens!`](crate::lens!) writes both
34    /// from one field name.
35    #[doc(hidden)]
36    pub fn new(path: impl Into<Path<M, P>>, read: fn(&M) -> &T) -> Self {
37        Self {
38            path: path.into(),
39            read,
40        }
41    }
42
43    /// The field's query path.
44    pub fn path(&self) -> &Path<M, P> {
45        &self.path
46    }
47
48    /// Read the field off `record`.
49    pub fn read<'a>(&self, record: &'a M) -> &'a T {
50        (self.read)(record)
51    }
52
53    /// The function reading the field.
54    pub(crate) fn reader(&self) -> fn(&M) -> &T {
55        self.read
56    }
57}
58
59impl<M, T, P> From<Lens<M, T, P>> for Path<M, P> {
60    fn from(lens: Lens<M, T, P>) -> 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/// ```rust
71/// # use tablo_core::{Lens, lens};
72/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
73/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
74/// # #[derive(Debug, Clone, toasty::Embed)]
75/// # struct Seo { title: String }
76/// # mod blog {
77/// #     #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
78/// #     pub struct Post { #[key] #[auto] pub id: uuid::Uuid, pub title: String, pub seo: super::Seo }
79/// # }
80/// # use blog::Post;
81/// # fn main() {
82/// let name: Lens<User, String> = lens!(User.name);
83/// let seo: Lens<Post, Seo> = lens!(Post.seo); // an embedded value, whole
84/// let title: Lens<Post, String> = lens!(Post.seo.title); // an embedded struct's field
85/// let qualified = lens!(blog::Post.title);
86/// # let _ = (name, seo, title, qualified);
87/// # }
88/// ```
89#[macro_export]
90macro_rules! lens {
91    ($($model:ident)::+ . $($field:ident).+) => {
92        $crate::Lens::new(
93            <$($model)::+>::fields()$(.$field())+,
94            |record: &$($model)::+| &record$(.$field)+,
95        )
96    };
97}
98
99/// The fields of `M` a [`Column`](crate::extend::Column) or a [`Filter`](crate::extend::Filter)
100/// reads, by the root field each path starts at: what
101/// [`field_access`](crate::ResourceDef::field_access) hides it by.
102///
103/// ```rust
104/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
105/// # struct Employee { #[key] #[auto] id: uuid::Uuid, salary: i64, bonus: i64 }
106/// use tablo_core::{ComputedColumn, Reads, lens};
107///
108/// ComputedColumn::new("Pay", |e: &Employee| (e.salary + e.bonus).to_string())
109///     .reading(Reads::of(lens!(Employee.salary)).and(lens!(Employee.bonus)));
110/// ```
111#[derive_where(Clone, Debug, Default)]
112pub struct Reads<M> {
113    fields: Vec<String>,
114    /// Whether some path named no root field: such a read hides under any mask.
115    unresolved: bool,
116    #[derive_where(skip(Debug))]
117    _model: std::marker::PhantomData<fn() -> M>,
118}
119
120impl<M> Reads<M> {
121    /// The root fields the reads name.
122    pub(crate) fn fields(&self) -> &[String] {
123        &self.fields
124    }
125
126    /// Whether some path named no root field, which hides under any mask.
127    pub(crate) fn is_unresolved(&self) -> bool {
128        self.unresolved
129    }
130
131    /// No field: a column or a filter that reads nothing off the row.
132    pub fn nothing() -> Self {
133        Self::default()
134    }
135
136    /// The field `path` reads.
137    pub fn of<T>(path: impl Into<Path<M, T>>) -> Self
138    where
139        M: toasty::schema::Model,
140    {
141        Self::nothing().and(path)
142    }
143
144    /// These fields and the one `path` reads.
145    pub fn and<T>(self, path: impl Into<Path<M, T>>) -> Self
146    where
147        M: toasty::schema::Model,
148    {
149        let path = crate::toasty_compat::model::ModelPath::of(&path.into());
150        match crate::toasty_compat::model::root_field::<M>(&path) {
151            Some(field) => self.named(field),
152            None => Self {
153                unresolved: true,
154                ..self
155            },
156        }
157    }
158
159    /// The fields `predicate` reads, off its expression: a filter's, a tab's or a summary's.
160    pub(crate) fn of_predicate(predicate: &toasty::stmt::Expr<bool>) -> Self
161    where
162        M: toasty::schema::Model,
163    {
164        let (fields, unresolved) = crate::toasty_compat::predicate::root_fields::<M>(predicate);
165        let reads = fields.into_iter().fold(Self::nothing(), Self::named);
166        Self {
167            unresolved,
168            ..reads
169        }
170    }
171
172    /// These fields and the root field `name`.
173    pub(crate) fn named(mut self, name: impl Into<String>) -> Self {
174        let name = name.into();
175        if !self.fields.contains(&name) {
176            self.fields.push(name);
177        }
178        self
179    }
180
181    /// These fields and the ones `other` reads.
182    pub(crate) fn union(&self, other: &Self) -> Self {
183        let mut union = other
184            .fields
185            .iter()
186            .fold(self.clone(), |reads, field| reads.named(field.clone()));
187        union.unresolved |= other.unresolved;
188        union
189    }
190
191    /// Whether it reads any of `fields`; a read naming no field may read them.
192    pub(crate) fn reads_any(&self, fields: &[String]) -> bool {
193        (self.unresolved && !fields.is_empty())
194            || self.fields.iter().any(|field| fields.contains(field))
195    }
196}
197
198impl<M: toasty::schema::Model, T, P> From<Lens<M, T, P>> for Reads<M> {
199    fn from(lens: Lens<M, T, P>) -> Self {
200        Self::of(lens)
201    }
202}
203
204impl<M: toasty::schema::Model, T> From<Path<M, T>> for Reads<M> {
205    fn from(path: Path<M, T>) -> Self {
206        Self::of(path)
207    }
208}
209
210/// Whether a declaration answering `reads` shows to a request that may not view `hidden`. One that
211/// does not say shows: a mount refuses it on a resource restricting a field.
212pub(crate) fn shows<M>(reads: Option<Reads<M>>, hidden: &[String]) -> bool {
213    reads.is_none_or(|reads| !reads.reads_any(hidden))
214}