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 /// These fields and the root field `name`.
160 pub(crate) fn named(mut self, name: impl Into<String>) -> Self {
161 let name = name.into();
162 if !self.fields.contains(&name) {
163 self.fields.push(name);
164 }
165 self
166 }
167
168 /// These fields and the ones `other` reads.
169 pub(crate) fn union(&self, other: &Self) -> Self {
170 let mut union = other
171 .fields
172 .iter()
173 .fold(self.clone(), |reads, field| reads.named(field.clone()));
174 union.unresolved |= other.unresolved;
175 union
176 }
177
178 /// Whether it reads any of `fields`; a read naming no field may read them.
179 pub(crate) fn reads_any(&self, fields: &[String]) -> bool {
180 (self.unresolved && !fields.is_empty())
181 || self.fields.iter().any(|field| fields.contains(field))
182 }
183}
184
185impl<M: toasty::schema::Model, T, P> From<Lens<M, T, P>> for Reads<M> {
186 fn from(lens: Lens<M, T, P>) -> Self {
187 Self::of(lens)
188 }
189}
190
191impl<M: toasty::schema::Model, T> From<Path<M, T>> for Reads<M> {
192 fn from(path: Path<M, T>) -> Self {
193 Self::of(path)
194 }
195}
196
197/// Whether a declaration answering `reads` shows to a request that may not view `hidden`. One that
198/// does not say shows: a mount refuses it on a resource restricting a field.
199pub(crate) fn shows<M>(reads: Option<Reads<M>>, hidden: &[String]) -> bool {
200 reads.is_none_or(|reads| !reads.reads_any(hidden))
201}