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>(
189 cx: &Cx,
190 declared: &Mounted<R>,
191 errors: &mut Vec<DeclarationError>,
192) {
193 let model = model_rules(
194 cx,
195 <R::Model as toasty::schema::Model>::id(),
196 &declared.field_rules,
197 );
198 let key_fields = pk::key_names::<R::Model>();
199 let mut seen = Vec::new();
200 for rule in &declared.field_rules {
201 let site = |field: &str| Site::FieldAccess(field.to_string());
202 let fault = |fault| DeclarationErrorKind::FieldAccess { fault };
203 let field = match &rule.field {
204 Ok(field) => field,
205 Err((placeholder, error)) => {
206 errors.push(DeclarationError::of::<R>(site(placeholder), error.clone()));
207 continue;
208 }
209 };
210 if seen.contains(&field) {
211 errors.push(DeclarationError::of::<R>(
212 site(field),
213 fault(FieldAccessFault::Duplicate),
214 ));
215 }
216 seen.push(field);
217 if rule.inverse {
218 errors.push(DeclarationError::of::<R>(
219 site(field),
220 fault(FieldAccessFault::Inverse),
221 ));
222 continue;
223 }
224 let trash = declared.soft_delete.as_ref();
225 if trash.is_some_and(|trash| trash.field().is_ok_and(|(_, name)| name == field)) {
226 errors.push(DeclarationError::of::<R>(
227 site(field),
228 fault(FieldAccessFault::SoftDelete),
229 ));
230 }
231 if rule.restricts_view() && key_fields.contains(field) {
232 errors.push(DeclarationError::of::<R>(
233 site(field),
234 fault(FieldAccessFault::RecordKey),
235 ));
236 }
237 }
238 let hiding = model.iter().filter(|rule| rule.restricts_view());
242 let title_hidden = declared.record_title_field().is_some_and(|title| {
243 hiding
244 .clone()
245 .any(|rule| rule.covers.iter().any(|covered| covered == title))
246 });
247 if title_hidden {
248 errors.push(DeclarationError::of::<R>(
249 Site::RecordTitle,
250 DeclarationErrorKind::FieldAccess {
251 fault: FieldAccessFault::RecordTitle,
252 },
253 ));
254 }
255 let watchers = declared.form.watchers();
256 let watched = hiding.clone().any(|rule| {
259 let keys = FieldMask {
260 hidden: rule.covers.clone(),
261 locked: Vec::new(),
262 }
263 .form(&declared.fields);
264 watchers.iter().any(|(watched, driven)| {
265 keys.hides(watched) && driven.as_ref().is_none_or(|driven| !keys.hides(driven))
266 })
267 });
268 if watched {
269 errors.push(DeclarationError::of::<R>(
270 Site::Form,
271 DeclarationErrorKind::FieldAccess {
272 fault: FieldAccessFault::Watched,
273 },
274 ));
275 }
276 if model.is_empty() {
277 return;
278 }
279 let undeclared = |what, name: &str| DeclarationErrorKind::FieldAccess {
280 fault: FieldAccessFault::UndeclaredReads {
281 what,
282 name: name.to_string(),
283 },
284 };
285 let mut faults = Vec::new();
286 for column in declared.table.declared_columns() {
287 if column.reads().is_none() {
288 faults.push((Site::Table, undeclared("column", column.name())));
289 }
290 }
291 if declared.table.row_color_reads_undeclared() {
292 faults.push((Site::Table, undeclared("row color", "row_color")));
293 }
294 if declared.table.row_link_reads_undeclared() {
295 faults.push((Site::Table, undeclared("row link", "row_link")));
296 }
297 for column in declared.detail.undeclared_reads() {
298 faults.push((Site::Detail, undeclared("column", column)));
299 }
300 errors.extend(
301 faults
302 .into_iter()
303 .map(|(site, kind)| DeclarationError::of::<R>(site, kind)),
304 );
305}
306
307pub(crate) fn check_columns<R: Resource>(
310 cx: &Cx,
311 declared: &Mounted<R>,
312 errors: &mut Vec<DeclarationError>,
313) {
314 if !restricts_any(cx) {
315 return;
316 }
317 let table: Vec<_> = declared.table.declared_columns().iter().collect();
318 let mut row_readers = Vec::new();
319 if declared.table.row_color_reads_row() {
320 row_readers.push("row color");
321 }
322 if declared.table.row_link_reads_row() {
323 row_readers.push("row link");
324 }
325 check_page::<R>(cx, Site::Table, &table, &row_readers, errors);
326 check_page::<R>(cx, Site::Detail, &declared.detail.columns(), &[], errors);
327}
328
329fn check_page<R: Resource>(
335 cx: &Cx,
336 site: Site,
337 columns: &[&BoxColumn<R::Model>],
338 row_readers: &[&str],
339 errors: &mut Vec<DeclarationError>,
340) {
341 let loads: Vec<Option<Vec<(model::ModelId, String)>>> = columns
342 .iter()
343 .map(|column| model::included_models::<R::Model>(&column.includes().into_vec()))
344 .collect();
345 let unresolved = loads.iter().any(Option::is_none);
346 let page_restricted = loads
347 .iter()
348 .flatten()
349 .flatten()
350 .find(|(model, _)| is_restricted_model(cx, *model))
351 .map(|(_, name)| name.clone());
352 let reads_row = |name: String| match (unresolved, &page_restricted) {
353 (true, _) => Some(FieldAccessFault::UnresolvedRelation { name }),
354 (false, Some(model)) => Some(FieldAccessFault::UnmaskedRelation {
355 name,
356 model: model.clone(),
357 }),
358 (false, None) => None,
359 };
360 let push = |errors: &mut Vec<DeclarationError>, fault| {
361 errors.push(DeclarationError::of::<R>(
362 site.clone(),
363 DeclarationErrorKind::FieldAccess { fault },
364 ));
365 };
366 for fault in row_readers
367 .iter()
368 .filter_map(|name| reads_row(name.to_string()))
369 {
370 push(errors, fault);
371 }
372 for (column, own) in columns.iter().zip(&loads) {
373 let name = column.name().to_string();
374 let fault = match column.internals().related() {
375 Related::None => continue,
376 Related::Source(source) if is_mounted(cx, source) => continue,
377 Related::Row => match reads_row(name) {
378 Some(fault) => fault,
379 None => continue,
380 },
381 Related::Source(_) | Related::Unmasked => match own {
382 None => FieldAccessFault::UnresolvedRelation { name },
383 Some(models) => {
384 match models
385 .iter()
386 .find(|(model, _)| is_restricted_model(cx, *model))
387 {
388 Some((_, model)) => FieldAccessFault::UnmaskedRelation {
389 name,
390 model: model.clone(),
391 },
392 None => continue,
393 }
394 }
395 },
396 };
397 push(errors, fault);
398 }
399}
400
401pub(crate) fn restricts_field(cx: &Cx, model: model::ModelId, field: &str) -> bool {
403 model_rules(cx, model, &[])
404 .iter()
405 .any(|rule| rule.restricts_view() && rule.covers.iter().any(|covered| covered == field))
406}
407
408pub(crate) fn check_choices<T: 'static, F>(
411 cx: &Cx,
412 schema: &Schema<F>,
413 site: Site,
414 errors: &mut Vec<DeclarationError>,
415) {
416 let mounted = |source| is_mounted(cx, source);
417 let restricted = |model: model::ModelId| is_restricted_model(cx, model);
418 for field in schema.fields() {
419 let Some(choice) = field.as_choice() else {
420 continue;
421 };
422 let name = field.name();
423 if let Some(model) = choice.unmasked_model(mounted, restricted) {
424 errors.push(DeclarationError::of::<T>(
425 site.clone(),
426 DeclarationErrorKind::FieldAccess {
427 fault: FieldAccessFault::UnmaskedRelation {
428 name: name.to_string(),
429 model: model.to_string(),
430 },
431 },
432 ));
433 }
434 if let Some(column) = choice.narrowed_by(|model, column| restricts_field(cx, model, column))
435 {
436 errors.push(DeclarationError::of::<T>(
437 site.clone(),
438 DeclarationErrorKind::FieldAccess {
439 fault: FieldAccessFault::NarrowedBy {
440 field: column.to_string(),
441 },
442 },
443 ));
444 }
445 }
446}
447
448pub(crate) fn check_filters<R: Resource>(
452 cx: &Cx,
453 declared: &Mounted<R>,
454 errors: &mut Vec<DeclarationError>,
455) {
456 if !restricts_any(cx) {
457 return;
458 }
459 let mounted = |source| is_mounted(cx, source);
460 let restricted = |model: model::ModelId| is_restricted_model(cx, model);
461 for filter in declared.table.declared_filters() {
465 let name = filter.name().to_string();
466 let fault = if let Some(model) = filter.internals().unmasked_model(&mounted, &restricted) {
467 Some(FieldAccessFault::UnmaskedRelation {
468 name,
469 model: model.to_string(),
470 })
471 } else {
472 match filter.reads() {
473 None => Some(FieldAccessFault::UndeclaredReads {
474 what: "filter",
475 name,
476 }),
477 reads => relation_fault(cx, name, reads),
478 }
479 };
480 errors.extend(fault.map(|fault| {
481 DeclarationError::of::<R>(Site::Table, DeclarationErrorKind::FieldAccess { fault })
482 }));
483 }
484 for tab in declared.table.declared_tabs() {
485 let name = tab.key().to_string();
486 let fault = relation_fault(cx, name, Some(tab.reads()));
487 errors.extend(fault.map(|fault| {
488 DeclarationError::of::<R>(Site::Table, DeclarationErrorKind::FieldAccess { fault })
489 }));
490 }
491 for summary in declared.table.declared_summaries() {
492 let fault = relation_fault(cx, summary.name(), Some(summary.reads()));
493 errors.extend(fault.map(|fault| {
494 DeclarationError::of::<R>(Site::Table, DeclarationErrorKind::FieldAccess { fault })
495 }));
496 }
497}
498
499fn relation_fault<M: toasty::schema::Model>(
502 cx: &Cx,
503 name: String,
504 reads: Option<crate::Reads<M>>,
505) -> Option<FieldAccessFault> {
506 let reads = reads?;
507 match model::read_relations::<M>(reads.fields()) {
508 Some(models) if !reads.is_unresolved() => models
509 .into_iter()
510 .find(|(model, _)| is_restricted_model(cx, *model))
511 .map(|(_, model)| FieldAccessFault::UnmaskedRelation { name, model }),
512 _ => Some(FieldAccessFault::UnresolvedRelation { name }),
513 }
514}