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, 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}