Skip to main content

tablo_core/
declaration.rs

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