1use std::sync::Arc;
5
6use topcoat::context::Cx;
7
8use super::{Mounted, Resource, is_mounted, is_restricted_model, model_rules, restricts_any};
9use crate::{
10 DeclarationError, DeclarationErrorKind, FieldAccessFault, Site,
11 column::{BoxColumn, Related},
12 form::FormField,
13 schema::{FormMask, Schema},
14 toasty_compat::{model, pk},
15};
16
17type Rule = Arc<dyn Fn(&Cx) -> bool + Send + Sync>;
19
20#[derive(Clone, Default)]
41pub struct FieldAccess {
42 view: Option<Rule>,
43 edit: Option<Rule>,
44}
45
46impl FieldAccess {
47 pub fn new() -> Self {
50 Self::default()
51 }
52
53 #[must_use]
55 pub fn view(mut self, rule: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
56 self.view = Some(Arc::new(rule));
57 self
58 }
59
60 #[must_use]
62 pub fn edit(mut self, rule: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
63 self.edit = Some(Arc::new(rule));
64 self
65 }
66
67 fn views(&self, cx: &Cx) -> bool {
69 self.view.as_ref().is_none_or(|rule| rule(cx))
70 }
71
72 fn edits(&self, cx: &Cx) -> bool {
74 self.views(cx) && self.edit.as_ref().is_none_or(|rule| rule(cx))
75 }
76}
77
78impl std::fmt::Debug for FieldAccess {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("FieldAccess")
81 .field("view", &self.view.is_some())
82 .field("edit", &self.edit.is_some())
83 .finish()
84 }
85}
86
87#[derive(Debug, Clone)]
89pub(crate) struct FieldRule {
90 field: Result<String, (String, DeclarationErrorKind)>,
92 covers: Vec<String>,
94 inverse: bool,
96 access: FieldAccess,
97}
98
99impl FieldRule {
100 pub(crate) fn new<M, T>(path: &toasty::stmt::Path<M, T>, access: FieldAccess) -> Self
102 where
103 M: toasty::schema::Model,
104 {
105 let path = model::ModelPath::of(path);
106 let field =
107 model::field::<M>(&path).map_err(|error| (format!("{:?}", path.steps()), error));
108 let covers = match &field {
109 Ok(field) => paired::<M>(field),
110 Err(_) => Vec::new(),
111 };
112 let inverse = field
113 .as_ref()
114 .is_ok_and(|field| model::is_inverse::<M>(&field.name));
115 Self {
116 field: field.map(|field| field.name),
117 covers,
118 inverse,
119 access,
120 }
121 }
122
123 pub(crate) fn restricts_view(&self) -> bool {
125 self.access.view.is_some()
126 }
127}
128
129fn paired<M: toasty::schema::Model>(field: &model::FieldInfo) -> Vec<String> {
132 let mut covers = vec![field.name.clone()];
133 for relation in model::belongs_tos::<M>() {
134 let pairs = relation.name == field.name || relation.foreign_keys.contains(&field.name);
135 if pairs {
136 covers.push(relation.name);
137 covers.extend(relation.foreign_keys);
138 }
139 }
140 covers.sort();
141 covers.dedup();
142 covers
143}
144
145#[derive(Debug, Clone, Default)]
147pub(crate) struct FieldMask {
148 hidden: Vec<String>,
149 locked: Vec<String>,
150}
151
152impl FieldMask {
153 pub(crate) fn of(cx: &Cx, rules: &[FieldRule]) -> Self {
155 let mut mask = Self::default();
156 for rule in rules {
157 if !rule.access.views(cx) {
158 mask.hidden.extend(rule.covers.iter().cloned());
159 } else if !rule.access.edits(cx) {
160 mask.locked.extend(rule.covers.iter().cloned());
161 }
162 }
163 mask
164 }
165
166 pub(crate) fn hidden(&self) -> &[String] {
168 &self.hidden
169 }
170
171 pub(crate) fn form<K>(&self, fields: &[FormField<K>]) -> FormMask {
173 let keys = |names: &[String]| -> Vec<String> {
174 fields
175 .iter()
176 .filter(|field| names.iter().any(|name| name == field.name))
177 .flat_map(|field| field.keys.iter().cloned())
178 .collect()
179 };
180 FormMask::new(keys(&self.hidden), keys(&self.locked))
181 }
182}
183
184pub(crate) fn check<R: Resource>(
188 cx: &Cx,
189 declared: &Mounted<R>,
190 errors: &mut Vec<DeclarationError>,
191) {
192 let model = model_rules(
193 cx,
194 <R::Model as toasty::schema::Model>::id(),
195 &declared.field_rules,
196 );
197 let key_fields = pk::key_names::<R::Model>();
198 let mut seen = Vec::new();
199 for rule in &declared.field_rules {
200 let site = |field: &str| Site::FieldAccess(field.to_string());
201 let fault = |fault| DeclarationErrorKind::FieldAccess { fault };
202 let field = match &rule.field {
203 Ok(field) => field,
204 Err((placeholder, error)) => {
205 errors.push(DeclarationError::of::<R>(site(placeholder), error.clone()));
206 continue;
207 }
208 };
209 if seen.contains(&field) {
210 errors.push(DeclarationError::of::<R>(
211 site(field),
212 fault(FieldAccessFault::Duplicate),
213 ));
214 }
215 seen.push(field);
216 if rule.inverse {
217 errors.push(DeclarationError::of::<R>(
218 site(field),
219 fault(FieldAccessFault::Inverse),
220 ));
221 continue;
222 }
223 let trash = declared.soft_delete.as_ref();
224 if trash.is_some_and(|trash| trash.field().is_ok_and(|(_, name)| name == field)) {
225 errors.push(DeclarationError::of::<R>(
226 site(field),
227 fault(FieldAccessFault::SoftDelete),
228 ));
229 }
230 if rule.restricts_view() && key_fields.contains(field) {
231 errors.push(DeclarationError::of::<R>(
232 site(field),
233 fault(FieldAccessFault::RecordKey),
234 ));
235 }
236 }
237 let hiding = model.iter().filter(|rule| rule.restricts_view());
241 let title_hidden = declared.record_title_field().is_some_and(|title| {
242 hiding
243 .clone()
244 .any(|rule| rule.covers.iter().any(|covered| covered == title))
245 });
246 if title_hidden {
247 errors.push(DeclarationError::of::<R>(
248 Site::RecordTitle,
249 DeclarationErrorKind::FieldAccess {
250 fault: FieldAccessFault::RecordTitle,
251 },
252 ));
253 }
254 let watchers = declared.form.watchers();
255 let watched = hiding.clone().any(|rule| {
258 let keys = FieldMask {
259 hidden: rule.covers.clone(),
260 locked: Vec::new(),
261 }
262 .form(&declared.fields);
263 watchers.iter().any(|(watched, driven)| {
264 keys.hides(watched) && driven.as_ref().is_none_or(|driven| !keys.hides(driven))
265 })
266 });
267 if watched {
268 errors.push(DeclarationError::of::<R>(
269 Site::Form,
270 DeclarationErrorKind::FieldAccess {
271 fault: FieldAccessFault::Watched,
272 },
273 ));
274 }
275 if model.is_empty() {
276 return;
277 }
278 let undeclared = |what, name: &str| DeclarationErrorKind::FieldAccess {
279 fault: FieldAccessFault::UndeclaredReads {
280 what,
281 name: name.to_string(),
282 },
283 };
284 let mut faults = Vec::new();
285 for column in declared.table.declared_columns() {
286 if column.reads().is_none() {
287 faults.push((Site::Table, undeclared("column", column.name())));
288 }
289 }
290 for filter in declared.table.declared_filters() {
291 if filter.reads().is_none() {
292 faults.push((Site::Table, undeclared("filter", filter.name())));
293 }
294 }
295 for column in declared.detail.undeclared_reads() {
296 faults.push((Site::Detail, undeclared("column", column)));
297 }
298 errors.extend(
299 faults
300 .into_iter()
301 .map(|(site, kind)| DeclarationError::of::<R>(site, kind)),
302 );
303}
304
305pub(crate) fn check_columns<R: Resource>(
308 cx: &Cx,
309 declared: &Mounted<R>,
310 errors: &mut Vec<DeclarationError>,
311) {
312 if !restricts_any(cx) {
313 return;
314 }
315 let table: Vec<_> = declared.table.declared_columns().iter().collect();
316 check_page::<R>(cx, Site::Table, &table, errors);
317 check_page::<R>(cx, Site::Detail, &declared.detail.columns(), errors);
318}
319
320fn check_page<R: Resource>(
325 cx: &Cx,
326 site: Site,
327 columns: &[&BoxColumn<R::Model>],
328 errors: &mut Vec<DeclarationError>,
329) {
330 let loads: Vec<Option<Vec<(model::ModelId, String)>>> = columns
331 .iter()
332 .map(|column| model::included_models::<R::Model>(&column.includes().into_vec()))
333 .collect();
334 let unresolved = loads.iter().any(Option::is_none);
335 let page_restricted = loads
336 .iter()
337 .flatten()
338 .flatten()
339 .find(|(model, _)| is_restricted_model(cx, *model))
340 .map(|(_, name)| name.clone());
341 for (column, own) in columns.iter().zip(&loads) {
342 let name = column.name().to_string();
343 let fault = match column.related() {
344 Related::None => continue,
345 Related::Source(source) if is_mounted(cx, source) => continue,
346 Related::Row => match (unresolved, &page_restricted) {
347 (true, _) => FieldAccessFault::UnresolvedRelation { name },
348 (false, Some(model)) => FieldAccessFault::UnmaskedRelation {
349 name,
350 model: model.clone(),
351 },
352 (false, None) => continue,
353 },
354 Related::Source(_) | Related::Unmasked => match own {
355 None => FieldAccessFault::UnresolvedRelation { name },
356 Some(models) => {
357 match models
358 .iter()
359 .find(|(model, _)| is_restricted_model(cx, *model))
360 {
361 Some((_, model)) => FieldAccessFault::UnmaskedRelation {
362 name,
363 model: model.clone(),
364 },
365 None => continue,
366 }
367 }
368 },
369 };
370 errors.push(DeclarationError::of::<R>(
371 site.clone(),
372 DeclarationErrorKind::FieldAccess { fault },
373 ));
374 }
375}
376
377pub(crate) fn restricts_field(cx: &Cx, model: model::ModelId, field: &str) -> bool {
379 model_rules(cx, model, &[])
380 .iter()
381 .any(|rule| rule.restricts_view() && rule.covers.iter().any(|covered| covered == field))
382}
383
384pub(crate) fn check_choices<T: 'static, F>(
387 cx: &Cx,
388 schema: &Schema<F>,
389 site: Site,
390 errors: &mut Vec<DeclarationError>,
391) {
392 let mounted = |source| is_mounted(cx, source);
393 let restricted = |model: model::ModelId| is_restricted_model(cx, model);
394 for field in schema.fields() {
395 let Some(choice) = field.as_choice() else {
396 continue;
397 };
398 let name = field.name();
399 if let Some(model) = choice.unmasked_model(mounted, restricted) {
400 errors.push(DeclarationError::of::<T>(
401 site.clone(),
402 DeclarationErrorKind::FieldAccess {
403 fault: FieldAccessFault::UnmaskedRelation {
404 name: name.to_string(),
405 model: model.to_string(),
406 },
407 },
408 ));
409 }
410 if let Some(column) = choice.narrowed_by(|model, column| restricts_field(cx, model, column))
411 {
412 errors.push(DeclarationError::of::<T>(
413 site.clone(),
414 DeclarationErrorKind::FieldAccess {
415 fault: FieldAccessFault::NarrowedBy {
416 field: column.to_string(),
417 },
418 },
419 ));
420 }
421 }
422}
423
424pub(crate) fn check_filters<R: Resource>(
428 cx: &Cx,
429 declared: &Mounted<R>,
430 errors: &mut Vec<DeclarationError>,
431) {
432 if !restricts_any(cx) {
433 return;
434 }
435 let mounted = |source| is_mounted(cx, source);
436 let restricted = |model: model::ModelId| is_restricted_model(cx, model);
437 for filter in declared.table.declared_filters() {
438 let name = filter.name().to_string();
439 let fault = if let Some(model) = filter.unmasked_model(&mounted, &restricted) {
440 Some(FieldAccessFault::UnmaskedRelation {
441 name,
442 model: model.to_string(),
443 })
444 } else {
445 match filter.reads() {
446 None => None,
447 Some(reads) => match model::read_relations::<R::Model>(reads.fields()) {
448 Some(models) if !reads.is_unresolved() => models
449 .into_iter()
450 .find(|(model, _)| is_restricted_model(cx, *model))
451 .map(|(_, model)| FieldAccessFault::UnmaskedRelation { name, model }),
452 _ => Some(FieldAccessFault::UnresolvedRelation { name }),
453 },
454 }
455 };
456 if let Some(fault) = fault {
457 errors.push(DeclarationError::of::<R>(
458 Site::Table,
459 DeclarationErrorKind::FieldAccess { fault },
460 ));
461 }
462 }
463}