Skip to main content

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}