tablo_core/declaration/kind.rs
1//! [`DeclarationErrorKind`]: every mistake a declaration can hold.
2
3use super::{
4 ActionInputFault, AreaFault, AuthMailFault, EditableColumnFault, FieldAccessFault,
5 FieldRuleFault, LocaleFault, ManyToManyFault, PolicyFault, ProfileFault, RelatedFormFault,
6 ResetFault, SegmentFault, SignUpFault, TenancyFault, TenantProfileFault,
7 TenantRegistrationFault, TwoFactorFault, VerificationFault,
8};
9
10/// What is wrong with a declaration.
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum DeclarationErrorKind {
14 /// The router holds no `toasty::Db`.
15 ///
16 /// Call `.with(db)` before `.panel(..)`.
17 MissingDb,
18 /// Auth is on, but the `Db` does not register the shipped models it needs: `AuthSession`,
19 /// `AdminUser` for the shipped authenticator, `ColumnPreference` while a list has a
20 /// toggleable column, `AuthToken` for a feature that mails links or two-factor sign-in, and
21 /// `AuthFactor` for two-factor sign-in.
22 ///
23 /// Build the `Db` from `tablo::models!(..)`, add them to `toasty::models!(..)`, or opt out
24 /// with `.auth(Auth::disabled())`.
25 MissingAuthModels {
26 /// The unregistered models.
27 models: Vec<&'static str>,
28 },
29 /// `Panel::shell_assets` is set, but the router has no asset bundle.
30 ///
31 /// Call `.assets(..)` before `.panel(..)`.
32 ShellAssetsWithoutBundle,
33 /// `Panel::notifications` on `Auth::disabled`, which signs nobody in to notify.
34 NotificationsWithoutAuth,
35 /// `Panel::audit_log` over a `Db` that does not register `AuditEntry`, so every write would
36 /// fail to store its entry.
37 ///
38 /// Build the `Db` from `tablo::models!(..)`, or add `AuditEntry` to `toasty::models!(..)`.
39 MissingAuditModel,
40 /// A `Brand` color that is no hex color or plain color function, which the panel's stylesheet
41 /// would carry as it is.
42 ///
43 /// Spell it `#4f46e5` or `oklch(0.55 0.2 260)`.
44 BrandColor {
45 /// The refused value.
46 value: String,
47 },
48 /// `Auth::external` beside a feature that needs Tablo to own the account's credentials.
49 ///
50 /// Drop the feature: the host app signs its users in, and keeps their credentials.
51 ExternalAuthFeature {
52 /// The builder call the panel may not declare.
53 feature: &'static str,
54 },
55 /// A form declares a file field, but the panel installs no `Uploader` to store its bytes.
56 ///
57 /// Install one with `Panel::uploads` or `Panel::uploads_dir`.
58 FileFieldWithoutUploader {
59 /// The file field's name.
60 field: String,
61 },
62 /// A path segment the panel routes cannot be a literal URL segment.
63 ///
64 /// Pick a plain segment such as `blog-posts`.
65 InvalidSegment {
66 /// What names the segment: `panel prefix`, `ResourceDef::slug` or `PageDef::slug`.
67 item: &'static str,
68 /// The refused segment.
69 segment: String,
70 /// Why it is refused.
71 fault: SegmentFault,
72 },
73 /// The prefix overlaps Topcoat's runtime endpoints.
74 ///
75 /// Pick another prefix.
76 PrefixOverlapsRuntime {
77 /// The panel's prefix.
78 prefix: String,
79 },
80 /// The prefix overlaps another panel's.
81 ///
82 /// Give each panel a prefix of its own.
83 PrefixOverlapsPanel {
84 /// The other panel's prefix.
85 other: String,
86 },
87 /// A `Panel::endpoint` route outside Topcoat's runtime endpoints.
88 ///
89 /// Declare only shards and procedures; the panel's prefix guards its other routes.
90 EndpointOutsideRuntime {
91 /// The route's path.
92 path: String,
93 },
94 /// A `Panel::serve_dir` pattern that does not end in a catch-all.
95 ///
96 /// End it in `{*file}`: `/uploads/{*file}`.
97 ServeDirWithoutCatchAll {
98 /// The pattern.
99 path: String,
100 },
101 /// The panel serves the same directory pattern twice.
102 ///
103 /// Drop the second `serve_dir`.
104 ServeDirTwice {
105 /// The pattern.
106 path: String,
107 },
108 /// Another panel already serves the directory pattern.
109 ///
110 /// Serve it from one panel.
111 ServeDirTaken {
112 /// The pattern.
113 path: String,
114 /// The serving panel's prefix.
115 panel: String,
116 },
117 /// The panel's content security policy cannot be served.
118 ContentSecurityPolicy(PolicyFault),
119 /// A second `Panel::home` page.
120 ///
121 /// Keep one home page; register the other with `page`.
122 SecondHome,
123 /// Every resource declares [`without_navigation`](crate::ResourceDef::without_navigation) and
124 /// the panel has no home page, so its root, where a sign-in lands, leads nowhere.
125 ///
126 /// Show one resource in the sidebar, or register a page with `Panel::home`.
127 NoRoot,
128 /// The resource is registered on the panel twice.
129 ///
130 /// Register it once; mount a variant under its own type.
131 DuplicateResource,
132 /// The page is registered on the panel twice, `Panel::home` included.
133 ///
134 /// Register it once; mount a variant under its own type.
135 DuplicatePage,
136 /// The resource is not mounted on the context's panel.
137 ///
138 /// It refuses no mount: a request answers 500 when app code asks for a resource its panel
139 /// does not register, through [`wired_table`](crate::wired_table),
140 /// [`scoped_query`](crate::scoped_query), [`write_create`](crate::write_create) or
141 /// [`write_update`](crate::write_update). Register the resource on that panel, or reach it
142 /// through a context of a panel that does.
143 NotMounted,
144 /// A slug naming a route the panel serves itself: `login`, `logout`, `register`, `profile`,
145 /// `forgot-password`, `reset-password` or `-`.
146 ///
147 /// Rename it with `ResourceDef::slug` or `PageDef::slug`.
148 ReservedSlug {
149 /// The slug.
150 slug: String,
151 },
152 /// A slug another resource or page already mounts at.
153 ///
154 /// Give each a distinct slug.
155 DuplicateSlug {
156 /// The slug.
157 slug: String,
158 },
159 /// Two actions share a `NAME`.
160 ///
161 /// Rename one.
162 DuplicateAction {
163 /// The shared name.
164 name: &'static str,
165 },
166 /// Two widgets of a page, or of a resource's list, share a name.
167 ///
168 /// Give one a distinct [`WidgetDef::name`](crate::WidgetDef::name).
169 DuplicateWidget {
170 /// The shared name.
171 name: String,
172 },
173 /// A page's [filter](crate::PageFilter) shares its key with another, or its key is not a word
174 /// a query parameter carries: ASCII letters, digits, `-` and `_`.
175 ///
176 /// Give each filter a distinct key of those characters.
177 MisdeclaredPageFilter {
178 /// The key.
179 key: String,
180 },
181 /// A column cannot edit its field in a table's cell.
182 EditableColumn {
183 /// The column's name, which is its field's.
184 column: String,
185 /// What is wrong with it.
186 fault: EditableColumnFault,
187 },
188 /// An action's input cannot be served.
189 ActionInput {
190 /// The action's `NAME`.
191 action: &'static str,
192 /// What is wrong with it.
193 fault: ActionInputFault,
194 },
195 /// The panel's sign-up page cannot be served.
196 SignUp(SignUpFault),
197 /// The panel's profile page cannot be served.
198 Profile(ProfileFault),
199 /// An area cannot hold a page auth serves.
200 Area(AreaFault),
201 /// The panel's password reset cannot be served.
202 PasswordReset(ResetFault),
203 /// The panel's email verification cannot be served.
204 EmailVerification(VerificationFault),
205 /// The panel's tenant registration page cannot be served.
206 TenantRegistration(TenantRegistrationFault),
207 /// The panel's tenant settings page cannot be served.
208 TenantProfile(TenantProfileFault),
209 /// The panel's two-factor sign-in cannot be served.
210 TwoFactor(TwoFactorFault),
211 /// The panel's auth mail cannot be sent.
212 AuthMail(AuthMailFault),
213 /// The panel's locales or translations cannot be served.
214 Locale(LocaleFault),
215 /// Two relations to the same resource.
216 ///
217 /// Keep one.
218 DuplicateRelation,
219 /// A relation to a resource the panel does not register.
220 ///
221 /// Register the related resource.
222 UnregisteredRelation,
223 /// A `has_one` relation declared as a tab: its form edits inside the record's own form.
224 ///
225 /// Drop `Relation::tab`.
226 HasOneTab,
227 /// A relationship field, or a relationship filter, whose options come from a resource the
228 /// panel does not register.
229 ///
230 /// Register that resource.
231 UnregisteredOptionSource {
232 /// The field.
233 field: String,
234 /// The option source's type.
235 source: &'static str,
236 },
237 /// A relation column labelling its records by a resource the panel does not register:
238 /// `RelationColumn::{new, of, list}::<R>` or `CountColumn::of::<R>`.
239 ///
240 /// Register `R`, or use `RelationColumn::unscoped` for a model no resource administers.
241 UnregisteredLabelSource {
242 /// The column.
243 column: String,
244 /// The label source's type.
245 source: &'static str,
246 },
247 /// A lens with more than one step where a single field of the model is needed.
248 ///
249 /// Name one field of the model.
250 TraversalLens {
251 /// The lens's step count.
252 steps: usize,
253 },
254 /// A multi-step lens that resolves to no single column.
255 ///
256 /// Step only through embedded structs, enum variant fields and `#[document]` fields.
257 UnresolvedLens {
258 /// The lens's model.
259 model: &'static str,
260 /// The lens's field indices, step by step.
261 steps: Vec<usize>,
262 },
263 /// A model whose schema no driver compiles, so its embedded paths name no column. The error
264 /// gives the reason of each of Toasty's PostgreSQL, SQLite, MySQL and DynamoDB drivers.
265 ///
266 /// Fix the model for your driver.
267 Uncompiled {
268 /// The model.
269 model: &'static str,
270 /// Each driver's reason to refuse it.
271 error: String,
272 },
273 /// A resource's tenancy that cannot scope its rows.
274 Tenancy {
275 /// What is wrong.
276 fault: TenancyFault,
277 },
278 /// A field access that cannot hide its field everywhere the panel shows it.
279 FieldAccess {
280 /// What is wrong.
281 fault: FieldAccessFault,
282 },
283 /// A table with no column.
284 ///
285 /// Declare columns; a resource whose model's table lists nothing declares its own `table`.
286 NoColumns,
287 /// `Table::page_size(0)`.
288 ///
289 /// Pass a page size of at least 1.
290 ZeroPageSize,
291 /// `Table::default_sort_by` names no sortable column.
292 ///
293 /// Declare its column `.sortable()`.
294 UnsortableDefaultSort {
295 /// The named column.
296 column: String,
297 },
298 /// Two table columns share a name.
299 ///
300 /// Rename one.
301 DuplicateColumn {
302 /// The shared name.
303 name: String,
304 },
305 /// Two table filters share a name.
306 ///
307 /// Rename one.
308 DuplicateFilter {
309 /// The shared name.
310 name: String,
311 },
312 /// Two list tabs share a key.
313 ///
314 /// Give each [`ListTab`](crate::ListTab) a distinct key.
315 DuplicateTab {
316 /// The shared key.
317 key: String,
318 },
319 /// A table offers two groupings by one field.
320 ///
321 /// Declare each [`Table::group_by`](crate::Table::group_by) once.
322 DuplicateGrouping {
323 /// The field's name.
324 name: String,
325 },
326 /// A table filter's name holds a `.`, which its URL parameters reserve for a part:
327 /// `f.<name>.<part>`.
328 ///
329 /// Rename it without the `.`.
330 DottedFilterName {
331 /// The name.
332 name: String,
333 },
334 /// A select filter option whose value the field's type does not parse, so choosing it
335 /// filters nothing.
336 ///
337 /// Spell it as the field's form value, or build the filter with `SelectFilter::of`.
338 UnparsedFilterOption {
339 /// The filter's name.
340 filter: String,
341 /// The option's value.
342 value: String,
343 },
344 /// An option of [`Table::option_tabs`](crate::Table::option_tabs) whose value the field's
345 /// type does not parse, so its tab admits no row.
346 ///
347 /// Spell the option as the field's form value.
348 UnparsedTabOption {
349 /// The option's value.
350 value: String,
351 },
352 /// Two schema fields share a name.
353 ///
354 /// Bind each input to a distinct field.
355 DuplicateField {
356 /// The shared name.
357 name: String,
358 },
359 /// A field disabled or hidden on the create form, or one a condition can disable, with no
360 /// `default` and no blank answer: a create could not fill it.
361 ///
362 /// Give it a `default`, make it optional, or use `disabled_on_edit`.
363 DisabledWithoutValue {
364 /// The disabled field.
365 field: String,
366 },
367 /// A repeater's item disables one of its row's controls or gives it a default, which a row
368 /// does not apply.
369 ///
370 /// Disable the repeater's own field.
371 RepeaterLeafModifier {
372 /// The repeater.
373 field: String,
374 /// The control in its row.
375 leaf: String,
376 },
377 /// A repeater's item has a password control, which an edit cannot render back, so every save
378 /// would overwrite the stored secret.
379 ///
380 /// Keep the secret out of the repeater's item.
381 RepeaterPassword {
382 /// The repeater.
383 field: String,
384 /// The password control in its row.
385 leaf: String,
386 },
387 /// A text field's declared length, bounds or pattern cannot work.
388 FieldRule {
389 /// The field, or `repeater.leaf` for a control in a repeater's rows.
390 field: String,
391 /// What is wrong.
392 fault: FieldRuleFault,
393 },
394 /// A field's default is a value its control never posts: one its type does not parse, one
395 /// not among a choice's options, or a list or one value where the control posts the other.
396 ///
397 /// Spell the default as the field's form value, a list for a multiple choice.
398 UnpostedDefault {
399 /// The field.
400 field: String,
401 /// The default, a list spelled `["a", "b"]`.
402 value: String,
403 },
404 /// A condition watches a field the schema does not place.
405 ///
406 /// Place the watched field in the same schema.
407 UnplacedWatchedField {
408 /// The watched field.
409 field: String,
410 },
411 /// A condition watches a field that another condition hides while what it guards still
412 /// shows.
413 ///
414 /// Place the guarded field inside the block that hides the watched one.
415 HiddenWatchedField {
416 /// The watched field.
417 field: String,
418 },
419 /// A condition, or a dependent choice, watches a field a condition disables, which posts
420 /// nothing while it is disabled: the browser would follow the value the user last chose, the
421 /// server the stored one or the default.
422 ///
423 /// Watch another field, or drop `disabled_when` from the watched one.
424 DisabledWatchedField {
425 /// The watched field.
426 field: String,
427 },
428 /// A condition names a value its watched field never posts: not one of a choice's options, or
429 /// neither `true` nor `false` for a toggle.
430 ///
431 /// Name one of its options, or `true` or `false` for a toggle.
432 UnpostedConditionValue {
433 /// The watched field.
434 field: String,
435 /// The value.
436 value: String,
437 },
438 /// A dependent choice's column belongs to another model than its relationship's source, or
439 /// the choice has no relationship.
440 ///
441 /// Declare `relationship::<R>()`, and name a column of `R`'s model in `depends_on`.
442 MisdeclaredDependentChoice {
443 /// The dependent choice.
444 field: String,
445 },
446 /// A choice whose [`options_where`](crate::ChoiceField::options_where) narrows no
447 /// relationship: declare `relationship::<R>()`, or drop `options_where`.
448 UnfilterableChoice {
449 /// The filtered choice.
450 field: String,
451 },
452 /// A dependent choice that takes several options, or that depends on one that does: a
453 /// multiple choice's key holds a list, which narrows nothing.
454 ///
455 /// Drop `depends_on` or `multiple`.
456 MultipleDependentChoice {
457 /// The dependent choice.
458 field: String,
459 },
460 /// A dependent choice rendered as [radios](crate::ChoiceField::radio) or
461 /// [buttons](crate::ChoiceField::buttons): the browser lists its options once, so it cannot
462 /// narrow them when the parent changes.
463 ///
464 /// Drop `depends_on`, or `radio` or `buttons`.
465 ListedDependentChoice {
466 /// The dependent choice.
467 field: String,
468 },
469 /// A [creatable](crate::ChoiceField::creatable) choice that is not a single select over a
470 /// resource's records: a multiple choice, radios or buttons, a dependent or filtered choice, a
471 /// source that is not a resource, no relationship, or a choice in a repeater's rows.
472 ///
473 /// Drop `creatable`, or declare the choice as a single select with `relationship::<R>()` over
474 /// a resource `R`.
475 UncreatableChoice {
476 /// The creatable choice: `repeater.leaf` for one in a repeater's rows.
477 field: String,
478 },
479 /// A dependent choice depends on a field the schema does not place.
480 ///
481 /// Place that field in the same schema.
482 UnplacedParentField {
483 /// The dependent choice.
484 field: String,
485 /// The field it depends on.
486 parent: String,
487 },
488 /// A field a condition hides has no blank answer, so a submission hiding it cannot parse.
489 ///
490 /// Make it an `Option`, or give it `#[form(blank = ..)]`.
491 RequiredConditionalField {
492 /// The field.
493 field: String,
494 },
495 /// A record-form field binding a key no control posts, after the form renders every field
496 /// it does not place: a hand-written [`RecordForm::control`](crate::RecordForm::control)
497 /// that renders no control for it.
498 ///
499 /// Render the field's control.
500 MissingControl {
501 /// The record-form field.
502 field: String,
503 /// The key it binds.
504 key: String,
505 },
506 /// A tenant-scoped resource's record form claims the tenant column.
507 ///
508 /// Drop the field: the framework stamps the tenant.
509 FormClaimsTenantColumn {
510 /// The record-form field.
511 field: String,
512 /// The tenant column.
513 column: String,
514 },
515 /// A soft-deleting resource's record form claims the soft-delete column, which only a delete
516 /// and a restore write.
517 ///
518 /// Drop the field, or mark it `#[form(skip)]` on a `ModelForm`: only a delete and a restore
519 /// write the column.
520 FormClaimsSoftDeleteColumn {
521 /// The record-form field.
522 field: String,
523 /// The soft-delete column.
524 column: String,
525 },
526 /// A form field named after a key the form's POST carries itself: the CSRF token, the edit
527 /// form's `record_version`, or the create form's `create_another`.
528 ReservedFormField {
529 /// The field.
530 field: String,
531 },
532 /// A choice field with neither options nor a relationship.
533 ///
534 /// Declare `.options(..)`, `.relationship::<R>()`, or `#[form(relationship = R)]`.
535 EmptyChoice {
536 /// The field.
537 field: String,
538 },
539 /// A relationship field, or a relationship filter, over a model with a composite primary key.
540 ///
541 /// Choose over a model with a single-column key.
542 CompositeKeyChoice {
543 /// The field, or the filter's name.
544 field: String,
545 },
546 /// A field marked unique over a column no unique index covers.
547 ///
548 /// Add `#[unique]` to the model, or drop `.unique()`.
549 UniqueWithoutIndex {
550 /// The field.
551 field: String,
552 },
553 /// A unique field whose non-nullable column stores one value for every empty submission.
554 ///
555 /// Make the field required, or an `Option`.
556 OptionalUnique {
557 /// The field.
558 field: String,
559 },
560 /// [`create_column`](crate::ResourceDef::create_column) names the tenant column.
561 ///
562 /// Drop it: the framework stamps the tenant.
563 CreateColumnsNameTenant {
564 /// The tenant column.
565 column: String,
566 },
567 /// A non-nullable column nothing writes on create, for a resource that serves a create page:
568 /// not the form, not a Toasty `#[default(..)]`, not the tenant stamp, not a `create_column`.
569 ///
570 /// Add the field to the form, a default to the model, or a `create_column`; or turn the
571 /// create page off with `without_page(ResourcePage::Create)`.
572 UnwrittenColumn {
573 /// The column.
574 column: String,
575 },
576 /// [`importable`](crate::ResourceDef::importable) on a resource that serves no create page:
577 /// it has no record form, or turns the page off with
578 /// [`without_page`](crate::ResourceDef::without_page).
579 ///
580 /// Drop `importable`, or serve the create page.
581 ImportWithoutCreate,
582 /// A detail listing no column on a resource that serves the view page.
583 ///
584 /// Declare a [`detail`](crate::ResourceDef::detail) or a table, or turn the page off with
585 /// `without_page(ResourcePage::View)`.
586 EmptyDetail,
587 /// A `has_one` relation whose related form cannot render inside the owner's form.
588 RelatedForm(RelatedFormFault),
589 /// A many-to-many field or relation whose join model cannot link records.
590 ManyToMany {
591 /// The field, or the relation's `via` field.
592 field: String,
593 /// What is wrong.
594 fault: ManyToManyFault,
595 },
596}