tablo_core/declaration/field_access.rs
1//! The declaration mistakes of a resource's field access, and their wording.
2
3use std::fmt;
4
5/// Why a [`field_access`](crate::ResourceDef::field_access) cannot hide its field everywhere the
6/// panel shows it:
7/// [`DeclarationErrorKind::FieldAccess`](super::DeclarationErrorKind::FieldAccess).
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum FieldAccessFault {
11 /// Two declarations over one field.
12 ///
13 /// Declare it once, with both its `view` and its `edit` rule.
14 Duplicate,
15 /// The field is the inverse side of a relation, a `#[has_many]` or a `#[has_one]`, whose
16 /// records the related resource's relation table and policy show and link.
17 ///
18 /// Restrict them with the related resource's policy.
19 Inverse,
20 /// The field is the [soft-delete](crate::ResourceDef::soft_delete) column, whose state the
21 /// trash control and the Restore buttons show every user.
22 ///
23 /// Restrict restores and force deletes with the policy's `Restore` and `ForceDelete`.
24 SoftDelete,
25 /// The record title reads the field, and every page and relationship choice names a record
26 /// by its title. Asked of a rule with a `view` rule only.
27 ///
28 /// Title the record by a field every user may view.
29 RecordTitle,
30 /// A rule hides a primary key. The key is every record's URL id and its default label, which
31 /// every page and relationship choice shows, so no user may be kept from seeing it.
32 ///
33 /// Restrict a field that is not the key.
34 RecordKey,
35 /// A form condition, a dependent choice or another field's comparison watches the field, so
36 /// what the form shows or refuses would tell what it holds. Asked of a rule with a `view`
37 /// rule only.
38 ///
39 /// Watch or compare with a field every user may view.
40 Watched,
41 /// A column or a filter of a resource over a model a panel restricts a field of does not
42 /// declare the fields it reads, so the panel cannot tell whether it shows the restricted one.
43 ///
44 /// Declare `.reading(Reads::of(..))` on a `ComputedColumn`, a `QueryFilter` or a described
45 /// `TextColumn`, or implement `reads` on a custom column or filter.
46 UndeclaredReads {
47 /// `"column"` or `"filter"`.
48 what: &'static str,
49 /// The column's or the filter's name.
50 name: String,
51 },
52 /// A column or a filter reads records of a model a mounted resource restricts a field of,
53 /// under no field access a panel can mask.
54 ///
55 /// Show the records through a mounted resource: `RelationColumn::new::<R>` naming the fields it
56 /// reads, `RelationColumn::of::<R>` for their title, or a relationship filter over the foreign
57 /// key.
58 UnmaskedRelation {
59 /// The column's or the filter's name.
60 name: String,
61 /// The related model's name.
62 model: String,
63 },
64 /// A column or a filter reads a relation the mount cannot read off the model's schema, so it
65 /// cannot tell whether the relation reaches records a mounted resource restricts.
66 ///
67 /// Declare the relation as a `Deferred` field of the model.
68 UnresolvedRelation {
69 /// The column's or the filter's name.
70 name: String,
71 },
72 /// A dependent choice narrows its options by a field a mounted resource restricts, so the
73 /// options would show which records hold each value of it.
74 ///
75 /// Narrow by a field every user may view.
76 NarrowedBy {
77 /// The field the choice narrows by.
78 field: String,
79 },
80}
81
82impl fmt::Display for FieldAccessFault {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Duplicate => f.write_str(
86 "the field has two `field_access` declarations: declare it once, with both its \
87 `view` and its `edit` rule",
88 ),
89 Self::Inverse => f.write_str(
90 "the field is a `has_many` or a `has_one`, whose records the relation table shows \
91 and links: restrict them with the related resource's policy, or drop the \
92 relation for the users who may not see it",
93 ),
94 Self::SoftDelete => f.write_str(
95 "the field is the soft-delete column, whose state the trash control and the \
96 Restore and Delete permanently buttons show: restrict who restores and deletes \
97 with the policy's `Restore` and `ForceDelete` instead",
98 ),
99 Self::RecordTitle => f.write_str(
100 "the record title reads the field, and every page and relationship choice names \
101 the record by its title: title the record by a field every user may view",
102 ),
103 Self::RecordKey => f.write_str(
104 "the field is a primary key, which every URL and relationship choice names a record by: \
105 restrict a field that is not the key"
106 ),
107 Self::Watched => f.write_str(
108 "a form condition, a dependent choice or a comparison watches the field, so what \
109 the form shows or refuses would tell what the field holds: watch or compare with \
110 a field every user may view",
111 ),
112 Self::UndeclaredReads { what, name } => write!(
113 f,
114 "{what} `{name}` declares no fields it reads, so the panel cannot tell whether it \
115 shows a restricted one: declare them with `.reading(Reads::of(..))`, or \
116 implement `reads` on a custom {what}"
117 ),
118 Self::UnmaskedRelation { name, model } => write!(
119 f,
120 "`{name}` reads `{model}` records under no mounted resource's field access, and a \
121 mounted resource restricts a field of `{model}`: read them through a resource, \
122 with `RelationColumn::new::<R>` naming the fields it reads or `RelationColumn::of::<R>` \
123 for their title, and keep a computed or custom column off a page that loads them"
124 ),
125 Self::NarrowedBy { field } => write!(
126 f,
127 "a dependent choice narrows its options by `{field}`, which a mounted resource \
128 restricts, so the options would show which records hold each value of it: narrow \
129 by a field every user may view"
130 ),
131 Self::UnresolvedRelation { name } => write!(
132 f,
133 "`{name}` reads a relation the mount cannot read off the model's schema, so it \
134 cannot show that no restricted record reaches it"
135 ),
136 }
137 }
138}