Skip to main content

tablo_core/declaration/
wording.rs

1//! The wording of each [`DeclarationErrorKind`].
2
3use std::fmt;
4
5use super::{DeclarationErrorKind, fmt_action_input, fmt_field_rule, fmt_many_to_many};
6
7impl fmt::Display for DeclarationErrorKind {
8    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9        match self {
10            Self::MissingDb => f.write_str(
11                "the router holds no Db: install it with `.app_context(db)` before mounting the \
12                 panel",
13            ),
14            Self::MissingAuthModels { models } => write!(
15                f,
16                "auth is on, but the Db does not register its shipped models ({models}): build \
17                 the Db from `tablo::models!(…)`, which registers them, or opt out with \
18                 `.auth(Auth::disabled())`",
19                models = models.join(", ")
20            ),
21            Self::ShellAssetsWithoutBundle => f.write_str(
22                "shell_assets need the router's asset bundle: install it with `.assets(..)` \
23                 before mounting the panel",
24            ),
25            Self::NotificationsWithoutAuth => f.write_str(
26                "notifications are for signed-in users: `Panel::notifications` needs auth on",
27            ),
28            Self::FileFieldWithoutUploader { field } => write!(
29                f,
30                "file field `{field}` needs an uploader to store its bytes: install one with \
31                 `Panel::uploads`"
32            ),
33            Self::InvalidSegment {
34                item,
35                segment,
36                fault,
37            } => write!(f, "{item} '{segment}' {fault}"),
38            Self::PrefixOverlapsRuntime { prefix } => write!(
39                f,
40                "prefix '{prefix}' overlaps Topcoat's runtime endpoints at '{}'",
41                crate::topcoat_compat::RUNTIME_PREFIX
42            ),
43            Self::PrefixOverlapsPanel { other } => write!(
44                f,
45                "the prefix overlaps the panel mounted at '{other}': each panel needs a prefix of \
46                 its own"
47            ),
48            Self::EndpointOutsideRuntime { path } => write!(
49                f,
50                "endpoint '{path}' is not a shard or procedure under '{}': the panel's prefix or \
51                 the app's own layers guard a route elsewhere",
52                crate::topcoat_compat::RUNTIME_PREFIX
53            ),
54            Self::ServeDirWithoutCatchAll { path } => write!(
55                f,
56                "serve_dir path '{path}' must end in a catch-all like '/uploads/{{*file}}'"
57            ),
58            Self::ServeDirTwice { path } => write!(f, "serve_dir path '{path}' is declared twice"),
59            Self::ServeDirTaken { path, panel } => write!(
60                f,
61                "serve_dir path '{path}' is already served by the panel mounted at '{panel}'"
62            ),
63            Self::InvalidContentSecurityPolicy { policy } => write!(
64                f,
65                "content_security_policy {policy:?} is not a valid header value"
66            ),
67            Self::SecondHome => {
68                f.write_str("a home page is already registered: `Panel::home` takes one")
69            }
70            Self::DuplicateResource => f.write_str(
71                "registered twice: a panel mounts each resource type once, so mount a variant \
72                 under its own type",
73            ),
74            Self::DuplicatePage => f.write_str(
75                "registered twice: a panel mounts each page type once, so mount a variant under \
76                 its own type",
77            ),
78            Self::NotMounted => f.write_str(
79                "not mounted on the context's panel: register it with `Panel::resource`",
80            ),
81            Self::ReservedSlug { slug } => {
82                write!(f, "slug '{slug}' names a route the panel serves itself")
83            }
84            Self::DuplicateSlug { slug } => write!(
85                f,
86                "slug '{slug}' is taken by another resource or page: each needs a distinct `slug()`"
87            ),
88            Self::DuplicateAction { name } => write!(
89                f,
90                "two actions are named '{name}': each needs a distinct `NAME`"
91            ),
92            Self::ActionInput { action, fault } => fmt_action_input(f, action, fault),
93            Self::SignUp(fault) => fault.fmt(f),
94            Self::Profile(fault) => fault.fmt(f),
95            Self::PasswordReset(fault) => fault.fmt(f),
96            Self::EmailVerification(fault) => fault.fmt(f),
97            Self::TenantRegistration(fault) => fault.fmt(f),
98            Self::TenantProfile(fault) => fault.fmt(f),
99            Self::TwoFactor(fault) => fault.fmt(f),
100            Self::AuthMail(fault) => fault.fmt(f),
101            Self::Locale(fault) => fault.fmt(f),
102            Self::DuplicateRelation => {
103                f.write_str("declared twice: each related resource is one relation")
104            }
105            Self::UnregisteredRelation => f.write_str(
106                "the related resource is not registered on this panel: declare it with \
107                 `Panel::resource`",
108            ),
109            Self::UnregisteredOptionSource { field, source } => write!(
110                f,
111                "field '{field}' takes its options from `{source}`, which this panel does not \
112                 register: declare it with `Panel::resource`"
113            ),
114            Self::UnregisteredLabelSource { column, source } => write!(
115                f,
116                "column '{column}' labels its records by `{source}`, which this panel does not \
117                 register: declare it with `Panel::resource`"
118            ),
119            Self::TraversalLens { steps } => write!(
120                f,
121                "a lens here names one field of its model, not a {steps}-step path"
122            ),
123            Self::UnresolvedLens { model, steps } => write!(
124                f,
125                "lens path {steps:?} resolves to no single column of `{model}`: only embedded \
126                 steps (embedded structs, enum variant fields and `#[document]` fields) bind, not \
127                 relation hops"
128            ),
129            Self::Uncompiled { model, error } => write!(
130                f,
131                "`{model}` compiles to no schema, so its embedded paths name no column: {error}"
132            ),
133            Self::Tenancy { fault } => fault.fmt(f),
134            Self::FieldAccess { fault } => fault.fmt(f),
135            Self::NoColumns => f.write_str(
136                "no column: declare columns with `Table::new(columns)` or in `ResourceDef::table`",
137            ),
138            Self::ZeroPageSize => f.write_str("`Table::paginate` needs a page size of at least 1"),
139            Self::UnsortableDefaultSort { column } => write!(
140                f,
141                "`Table::default_sort` sorts on '{column}', but the table declares no sortable column for it"
142            ),
143            Self::DuplicateColumn { name } => write!(
144                f,
145                "two columns are named '{name}': each column needs a distinct name"
146            ),
147            Self::DuplicateFilter { name } => write!(
148                f,
149                "two filters are named '{name}': each filter needs a distinct name"
150            ),
151            Self::DottedFilterName { name } => write!(f, "filter name '{name}' holds a '.'"),
152            Self::UnparsedFilterOption { filter, value } => write!(
153                f,
154                "filter '{filter}' offers '{value}', which its field's type does not parse: spell \
155                 each option as the field's form value, or take the type's own with \
156                 `SelectFilter::of`"
157            ),
158            Self::DuplicateField { name } => write!(
159                f,
160                "two fields are named '{name}': each input needs a distinct field"
161            ),
162            Self::DisabledWithoutValue { field } => write!(
163                f,
164                "field '{field}' is disabled and required, so no create can fill it: give it a \
165                 `default` (a file field takes none), make it optional, or use `disabled_on_edit`"
166            ),
167            Self::RepeaterLeafModifier { field, leaf } => write!(
168                f,
169                "repeater '{field}' disables its row's '{leaf}' or gives it a default, which a \
170                 row does not apply: disable the repeater's own field instead"
171            ),
172            Self::RepeaterPassword { field, leaf } => write!(
173                f,
174                "repeater '{field}' rows hold password '{leaf}', which an edit never renders \
175                 back, so each save overwrites it: keep the secret out of the item"
176            ),
177            Self::FieldRule { field, fault } => fmt_field_rule(f, field, fault),
178            Self::UnpostedDefault { field, value } => write!(
179                f,
180                "field '{field}' defaults to '{value}', which its control never posts: spell the \
181                 default as the field's form value, a list for a multiple choice"
182            ),
183            Self::UnplacedWatchedField { field } => write!(
184                f,
185                "a condition watches field '{field}', which the schema does not place: place it \
186                 in the same schema"
187            ),
188            Self::HiddenWatchedField { field } => write!(
189                f,
190                "a condition watches field '{field}', which a condition hides while the guarded \
191                 field or block still shows: place the guarded one inside the block that hides \
192                 '{field}'"
193            ),
194            Self::UnpostedConditionValue { field, value } => write!(
195                f,
196                "a condition shows a field while '{field}' posts '{value}', which it never posts: \
197                 name one of its option values, or `true` or `false` for a toggle"
198            ),
199            Self::MisdeclaredDependentChoice { field } => write!(
200                f,
201                "choice '{field}' depends on a column its options do not have: call \
202                 `relationship::<R>()` and name a column of `R`'s model in `depends_on`"
203            ),
204            Self::MultipleDependentChoice { field } => write!(
205                f,
206                "choice '{field}' depends on another field through a multiple choice, which \
207                 neither narrows another choice nor is narrowed: drop `depends_on` or `multiple`"
208            ),
209            Self::ListedDependentChoice { field } => write!(
210                f,
211                "choice '{field}' depends on another field but renders its options as radios or \
212                 buttons, which the browser cannot narrow: drop `depends_on`, or `radio` or \
213                 `buttons`"
214            ),
215            Self::UnplacedParentField { field, parent } => write!(
216                f,
217                "choice '{field}' depends on field '{parent}', which the schema does not place: \
218                 place it in the same schema"
219            ),
220            Self::RequiredConditionalField { field } => write!(
221                f,
222                "field '{field}' is conditional but has no blank answer: a submission that hides \
223                 it posts nothing, so make it an `Option` or give it `#[form(blank = ..)]`"
224            ),
225            Self::MissingControl { field, key } => write!(
226                f,
227                "record-form field `{field}` binds key `{key}`, but no control posts it: \
228                 `RecordForm::control` must render the field's control"
229            ),
230            Self::FormClaimsTenantColumn { field, column } => write!(
231                f,
232                "record-form field `{field}` claims the tenant column `{column}`, which the \
233                 framework stamps on create: drop it from the form"
234            ),
235            Self::FormClaimsSoftDeleteColumn { field, column } => write!(
236                f,
237                "record-form field `{field}` claims the soft-delete column `{column}`, which only \
238                 a delete and a restore write: drop it from the form, or mark it \
239                 `#[form(skip)]` on a `ModelForm`"
240            ),
241            Self::ReservedFormField { field } => write!(
242                f,
243                "form field `{field}` takes a name the form's POST carries itself, so the two \
244                 would read each other's value: rename the field"
245            ),
246            Self::EmptyChoice { field } => write!(
247                f,
248                "choice field `{field}` declares no options and no relationship, so it offers \
249                 nothing to choose: add `.options(..)` or `.relationship::<Source>()`, or \
250                 `#[form(relationship = Source)]` on a record form's field"
251            ),
252            Self::CompositeKeyChoice { field } => write!(
253                f,
254                "`{field}` reads a relationship whose model has a composite primary key, which \
255                 no option value can spell"
256            ),
257            Self::UniqueWithoutIndex { field } => write!(
258                f,
259                "field `{field}` is marked unique, but no unique index covers its column: add \
260                 `#[unique]` (or `#[unique(..)]`) to the model or drop `.unique()`, which would \
261                 otherwise check a rule the database does not enforce"
262            ),
263            Self::OptionalUnique { field } => write!(
264                f,
265                "field `{field}` is marked unique, but its record-form field answers an empty \
266                 submission and its column is not nullable, so every empty submission stores \
267                 the same value: make the field required, or an `Option`"
268            ),
269            Self::CreateColumnsNameTenant { column } => write!(
270                f,
271                "`create_column` names the tenant column `{column}`, which the framework stamps \
272                 on create: drop it there and delegate to `write_create`"
273            ),
274            Self::UnwrittenColumn { column } => write!(
275                f,
276                "nothing writes the non-nullable column `{column}` on create: the record form has \
277                 no such field, toasty fills no `#[default(..)]` for it, and no `create_column` \
278                 names it, so every create would fail at the driver; a resource that creates \
279                 nothing declares `edit_only`"
280            ),
281            Self::ImportWithoutCreate => write!(
282                f,
283                "`importable` creates each row through the create form, which this resource does \
284                 not serve: it has no record form, or is `edit_only`"
285            ),
286            Self::ManyToMany { field, fault } => fmt_many_to_many(f, field, fault),
287        }
288    }
289}