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, OidcFault, PolicyFault, ProfileFault,
6 RelatedFormFault, 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 provider sign-in cannot be served.
212 Oidc(OidcFault),
213 /// The panel's auth mail cannot be sent.
214 AuthMail(AuthMailFault),
215 /// The panel's locales or translations cannot be served.
216 Locale(LocaleFault),
217 /// Two relations to the same resource.
218 ///
219 /// Keep one.
220 DuplicateRelation,
221 /// A relation to a resource the panel does not register.
222 ///
223 /// Register the related resource.
224 UnregisteredRelation,
225 /// A `has_one` relation declared as a tab: its form edits inside the record's own form.
226 ///
227 /// Drop `Relation::tab`.
228 HasOneTab,
229 /// A relationship field, or a relationship filter, whose options come from a resource the
230 /// panel does not register.
231 ///
232 /// Register that resource.
233 UnregisteredOptionSource {
234 /// The field.
235 field: String,
236 /// The option source's type.
237 source: &'static str,
238 },
239 /// A relation column labelling its records by a resource the panel does not register:
240 /// `RelationColumn::{new, of, list}::<R>` or `CountColumn::of::<R>`.
241 ///
242 /// Register `R`, or use `RelationColumn::unscoped` for a model no resource administers.
243 UnregisteredLabelSource {
244 /// The column.
245 column: String,
246 /// The label source's type.
247 source: &'static str,
248 },
249 /// A lens with more than one step where a single field of the model is needed.
250 ///
251 /// Name one field of the model.
252 TraversalLens {
253 /// The lens's step count.
254 steps: usize,
255 },
256 /// A multi-step lens that resolves to no single column.
257 ///
258 /// Step only through embedded structs, enum variant fields and `#[document]` fields.
259 UnresolvedLens {
260 /// The lens's model.
261 model: &'static str,
262 /// The lens's field indices, step by step.
263 steps: Vec<usize>,
264 },
265 /// A model whose schema no driver compiles, so its embedded paths name no column. The error
266 /// gives the reason of each of Toasty's PostgreSQL, SQLite, MySQL and DynamoDB drivers.
267 ///
268 /// Fix the model for your driver.
269 Uncompiled {
270 /// The model.
271 model: &'static str,
272 /// Each driver's reason to refuse it.
273 error: String,
274 },
275 /// A resource's tenancy that cannot scope its rows.
276 Tenancy {
277 /// What is wrong.
278 fault: TenancyFault,
279 },
280 /// A field access that cannot hide its field everywhere the panel shows it.
281 FieldAccess {
282 /// What is wrong.
283 fault: FieldAccessFault,
284 },
285 /// A table with no column.
286 ///
287 /// Declare columns; a resource whose model's table lists nothing declares its own `table`.
288 NoColumns,
289 /// `Table::page_size(0)`.
290 ///
291 /// Pass a page size of at least 1.
292 ZeroPageSize,
293 /// `Table::default_sort_by` names no sortable column.
294 ///
295 /// Declare its column `.sortable()`.
296 UnsortableDefaultSort {
297 /// The named column.
298 column: String,
299 },
300 /// Two table columns share a name.
301 ///
302 /// Rename one.
303 DuplicateColumn {
304 /// The shared name.
305 name: String,
306 },
307 /// Two table filters share a name.
308 ///
309 /// Rename one.
310 DuplicateFilter {
311 /// The shared name.
312 name: String,
313 },
314 /// Two list tabs share a key.
315 ///
316 /// Give each [`ListTab`](crate::ListTab) a distinct key.
317 DuplicateTab {
318 /// The shared key.
319 key: String,
320 },
321 /// A table offers two groupings by one field.
322 ///
323 /// Declare each [`Table::group_by`](crate::Table::group_by) once.
324 DuplicateGrouping {
325 /// The field's name.
326 name: String,
327 },
328 /// A table filter's name holds a `.`, which its URL parameters reserve for a part:
329 /// `f.<name>.<part>`.
330 ///
331 /// Rename it without the `.`.
332 DottedFilterName {
333 /// The name.
334 name: String,
335 },
336 /// A select filter option whose value the field's type does not parse, so choosing it
337 /// filters nothing.
338 ///
339 /// Spell it as the field's form value, or build the filter with `SelectFilter::of`.
340 UnparsedFilterOption {
341 /// The filter's name.
342 filter: String,
343 /// The option's value.
344 value: String,
345 },
346 /// An option of [`Table::option_tabs`](crate::Table::option_tabs) whose value the field's
347 /// type does not parse, so its tab admits no row.
348 ///
349 /// Spell the option as the field's form value.
350 UnparsedTabOption {
351 /// The option's value.
352 value: String,
353 },
354 /// Two schema fields share a name.
355 ///
356 /// Bind each input to a distinct field.
357 DuplicateField {
358 /// The shared name.
359 name: String,
360 },
361 /// A field disabled or hidden on the create form, or one a condition can disable, with no
362 /// `default` and no blank answer: a create could not fill it.
363 ///
364 /// Give it a `default`, make it optional, or use `disabled_on_edit`.
365 DisabledWithoutValue {
366 /// The disabled field.
367 field: String,
368 },
369 /// A repeater's item disables one of its row's controls or gives it a default, which a row
370 /// does not apply.
371 ///
372 /// Disable the repeater's own field.
373 RepeaterLeafModifier {
374 /// The repeater.
375 field: String,
376 /// The control in its row.
377 leaf: String,
378 },
379 /// A repeater's item has a password control, which an edit cannot render back, so every save
380 /// would overwrite the stored secret.
381 ///
382 /// Keep the secret out of the repeater's item.
383 RepeaterPassword {
384 /// The repeater.
385 field: String,
386 /// The password control in its row.
387 leaf: String,
388 },
389 /// A text field's declared length, bounds or pattern cannot work.
390 FieldRule {
391 /// The field, or `repeater.leaf` for a control in a repeater's rows.
392 field: String,
393 /// What is wrong.
394 fault: FieldRuleFault,
395 },
396 /// A field's default is a value its control never posts: one its type does not parse, one
397 /// not among a choice's options, or a list or one value where the control posts the other.
398 ///
399 /// Spell the default as the field's form value, a list for a multiple choice.
400 UnpostedDefault {
401 /// The field.
402 field: String,
403 /// The default, a list spelled `["a", "b"]`.
404 value: String,
405 },
406 /// A condition watches a field the schema does not place.
407 ///
408 /// Place the watched field in the same schema.
409 UnplacedWatchedField {
410 /// The watched field.
411 field: String,
412 },
413 /// A condition watches a field that another condition hides while what it guards still
414 /// shows.
415 ///
416 /// Place the guarded field inside the block that hides the watched one.
417 HiddenWatchedField {
418 /// The watched field.
419 field: String,
420 },
421 /// A condition, or a dependent choice, watches a field a condition disables, which posts
422 /// nothing while it is disabled: the browser would follow the value the user last chose, the
423 /// server the stored one or the default.
424 ///
425 /// Watch another field, or drop `disabled_when` from the watched one.
426 DisabledWatchedField {
427 /// The watched field.
428 field: String,
429 },
430 /// A condition names a value its watched field never posts: not one of a choice's options, or
431 /// neither `true` nor `false` for a toggle.
432 ///
433 /// Name one of its options, or `true` or `false` for a toggle.
434 UnpostedConditionValue {
435 /// The watched field.
436 field: String,
437 /// The value.
438 value: String,
439 },
440 /// A dependent choice's column belongs to another model than its relationship's source, or
441 /// the choice has no relationship.
442 ///
443 /// Declare `relationship::<R>()`, and name a column of `R`'s model in `depends_on`.
444 MisdeclaredDependentChoice {
445 /// The dependent choice.
446 field: String,
447 },
448 /// A choice whose [`options_where`](crate::ChoiceField::options_where) narrows no
449 /// relationship: declare `relationship::<R>()`, or drop `options_where`.
450 UnfilterableChoice {
451 /// The filtered choice.
452 field: String,
453 },
454 /// A dependent choice that takes several options, or that depends on one that does: a
455 /// multiple choice's key holds a list, which narrows nothing.
456 ///
457 /// Drop `depends_on` or `multiple`.
458 MultipleDependentChoice {
459 /// The dependent choice.
460 field: String,
461 },
462 /// A dependent choice rendered as [radios](crate::ChoiceField::radio) or
463 /// [buttons](crate::ChoiceField::buttons): the browser lists its options once, so it cannot
464 /// narrow them when the parent changes.
465 ///
466 /// Drop `depends_on`, or `radio` or `buttons`.
467 ListedDependentChoice {
468 /// The dependent choice.
469 field: String,
470 },
471 /// A [creatable](crate::ChoiceField::creatable) choice that is not a single select over a
472 /// resource's records: a multiple choice, radios or buttons, a dependent or filtered choice, a
473 /// source that is not a resource, no relationship, or a choice in a repeater's rows.
474 ///
475 /// Drop `creatable`, or declare the choice as a single select with `relationship::<R>()` over
476 /// a resource `R`.
477 UncreatableChoice {
478 /// The creatable choice: `repeater.leaf` for one in a repeater's rows.
479 field: String,
480 },
481 /// A dependent choice depends on a field the schema does not place.
482 ///
483 /// Place that field in the same schema.
484 UnplacedParentField {
485 /// The dependent choice.
486 field: String,
487 /// The field it depends on.
488 parent: String,
489 },
490 /// A field a condition hides has no blank answer, so a submission hiding it cannot parse.
491 ///
492 /// Make it an `Option`, or give it `#[form(blank = ..)]`.
493 RequiredConditionalField {
494 /// The field.
495 field: String,
496 },
497 /// A record-form field binding a key no control posts, after the form renders every field
498 /// it does not place: a hand-written [`RecordForm::control`](crate::RecordForm::control)
499 /// that renders no control for it.
500 ///
501 /// Render the field's control.
502 MissingControl {
503 /// The record-form field.
504 field: String,
505 /// The key it binds.
506 key: String,
507 },
508 /// A tenant-scoped resource's record form claims the tenant column.
509 ///
510 /// Drop the field: the framework stamps the tenant.
511 FormClaimsTenantColumn {
512 /// The record-form field.
513 field: String,
514 /// The tenant column.
515 column: String,
516 },
517 /// A soft-deleting resource's record form claims the soft-delete column, which only a delete
518 /// and a restore write.
519 ///
520 /// Drop the field, or mark it `#[form(skip)]` on a `ModelForm`: only a delete and a restore
521 /// write the column.
522 FormClaimsSoftDeleteColumn {
523 /// The record-form field.
524 field: String,
525 /// The soft-delete column.
526 column: String,
527 },
528 /// A form field named after a key the form's POST carries itself: the CSRF token, the edit
529 /// form's `record_version`, or the create form's `create_another`.
530 ReservedFormField {
531 /// The field.
532 field: String,
533 },
534 /// A choice field with neither options nor a relationship.
535 ///
536 /// Declare `.options(..)`, `.relationship::<R>()`, or `#[form(relationship = R)]`.
537 EmptyChoice {
538 /// The field.
539 field: String,
540 },
541 /// A relationship field, or a relationship filter, over a model with a composite primary key.
542 ///
543 /// Choose over a model with a single-column key.
544 CompositeKeyChoice {
545 /// The field, or the filter's name.
546 field: String,
547 },
548 /// A field marked unique over a column no unique index covers.
549 ///
550 /// Add `#[unique]` to the model, or drop `.unique()`.
551 UniqueWithoutIndex {
552 /// The field.
553 field: String,
554 },
555 /// A unique field whose non-nullable column stores one value for every empty submission.
556 ///
557 /// Make the field required, or an `Option`.
558 OptionalUnique {
559 /// The field.
560 field: String,
561 },
562 /// [`create_column`](crate::ResourceDef::create_column) names the tenant column.
563 ///
564 /// Drop it: the framework stamps the tenant.
565 CreateColumnsNameTenant {
566 /// The tenant column.
567 column: String,
568 },
569 /// A non-nullable column nothing writes on create, for a resource that serves a create page:
570 /// not the form, not a Toasty `#[default(..)]`, not the tenant stamp, not a `create_column`.
571 ///
572 /// Add the field to the form, a default to the model, or a `create_column`; or turn the
573 /// create page off with `without_page(ResourcePage::Create)`.
574 UnwrittenColumn {
575 /// The column.
576 column: String,
577 },
578 /// [`importable`](crate::ResourceDef::importable) on a resource that serves no create page:
579 /// it has no record form, or turns the page off with
580 /// [`without_page`](crate::ResourceDef::without_page).
581 ///
582 /// Drop `importable`, or serve the create page.
583 ImportWithoutCreate,
584 /// A detail listing no column on a resource that serves the view page.
585 ///
586 /// Declare a [`detail`](crate::ResourceDef::detail) or a table, or turn the page off with
587 /// `without_page(ResourcePage::View)`.
588 EmptyDetail,
589 /// A `has_one` relation whose related form cannot render inside the owner's form.
590 RelatedForm(RelatedFormFault),
591 /// A many-to-many field or relation whose join model cannot link records.
592 ManyToMany {
593 /// The field, or the relation's `via` field.
594 field: String,
595 /// What is wrong.
596 fault: ManyToManyFault,
597 },
598}