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
5/// Why [`RouterBuilderPanelExt::panel`](crate::RouterBuilderPanelExt::panel) refused a panel:
6/// every declaration mistake it found.
7///
8/// The router's builder returns it as a [`topcoat::Error`]; recover it with
9/// `error.downcast_ref::<MountError>()`.
10#[derive(Debug)]
11pub struct MountError {
12    panel: String,
13    errors: Vec<DeclarationError>,
14}
15
16impl MountError {
17    pub(crate) fn new(panel: &str, errors: Vec<DeclarationError>) -> Self {
18        Self {
19            panel: panel.to_string(),
20            errors,
21        }
22    }
23
24    /// The refused panel's prefix.
25    pub fn panel(&self) -> &str {
26        &self.panel
27    }
28
29    /// Every mistake found, in declaration order.
30    pub fn errors(&self) -> &[DeclarationError] {
31        &self.errors
32    }
33}
34
35impl fmt::Display for MountError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "panel '{}' cannot mount:", self.panel)?;
38        for error in &self.errors {
39            write!(f, "\n  - {error}")?;
40        }
41        Ok(())
42    }
43}
44
45impl std::error::Error for MountError {}
46
47/// One declaration mistake: who declares it, where, and what is wrong.
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct DeclarationError {
51    /// The resource or page type that declares it; `None` for the panel's own configuration.
52    pub resource: Option<&'static str>,
53    /// The part of the declaration it is in.
54    pub site: Site,
55    /// What is wrong.
56    pub kind: DeclarationErrorKind,
57}
58
59impl DeclarationError {
60    /// A mistake in the panel's own configuration.
61    pub(crate) fn panel(kind: DeclarationErrorKind) -> Self {
62        Self {
63            resource: None,
64            site: Site::Panel,
65            kind,
66        }
67    }
68
69    /// A mistake in `T`'s declaration.
70    pub(crate) fn of<T>(site: Site, kind: DeclarationErrorKind) -> Self {
71        Self {
72            resource: Some(std::any::type_name::<T>()),
73            site,
74            kind,
75        }
76    }
77}
78
79impl fmt::Display for DeclarationError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        if let Some(resource) = self.resource {
82            write!(f, "`{resource}`")?;
83            match &self.site {
84                Site::Panel | Site::Registration => {}
85                Site::Tenancy => f.write_str(" tenancy")?,
86                Site::Table => f.write_str(" table")?,
87                Site::Form => f.write_str(" form")?,
88                Site::Detail => f.write_str(" detail")?,
89                Site::Relation(key) => write!(f, " relation `{key}`")?,
90            }
91            f.write_str(": ")?;
92        }
93        write!(f, "{}", self.kind)
94    }
95}
96
97impl std::error::Error for DeclarationError {}
98
99/// The part of a declaration a [`DeclarationError`] is in.
100#[derive(Debug, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum Site {
103    /// The panel's own configuration: its prefix, served directories, assets and auth.
104    Panel,
105    /// The resource or page itself: its registration, slug, actions or create columns, or a
106    /// second home page.
107    Registration,
108    /// `ResourceDef::tenancy`.
109    Tenancy,
110    /// `ResourceDef::table`.
111    Table,
112    /// `ResourceDef::form`, checked against the record form.
113    Form,
114    /// `ResourceDef::detail`.
115    Detail,
116    /// The `ResourceDef::relation` to the resource with this slug, or this type when the panel
117    /// does not register it.
118    Relation(String),
119}
120
121/// Why an action's input cannot be served:
122/// [`DeclarationErrorKind::ActionInput`].
123#[derive(Debug, Clone, PartialEq, Eq)]
124#[non_exhaustive]
125pub enum ActionInputFault {
126    /// A field named like a key the action's POST carries itself: `csrf_token`, `confirm` or
127    /// `ids`.
128    ReservedField(String),
129    /// A file field, whose upload an action's POST does not read.
130    FileField(String),
131    /// No field, but a parse that refuses an empty submission, so the action could never run.
132    RefusesEmpty,
133    /// A dependent choice, whose options no route serves an action's input.
134    DependentChoice(String),
135}
136
137/// Why a panel's sign-up page cannot be served: [`DeclarationErrorKind::SignUp`].
138#[derive(Debug, Clone, PartialEq, Eq)]
139#[non_exhaustive]
140pub enum SignUpFault {
141    /// `Auth::registration` on `Auth::disabled`, which serves no sign-in to land on.
142    AuthDisabled,
143    /// The registrar creates another user type than the authenticator loads.
144    UserType {
145        /// The registrar's `User` type.
146        registrar: &'static str,
147    },
148    /// A field named like a key the sign-up POST carries itself: `csrf_token` or `next`.
149    ReservedField(String),
150    /// A file field, whose upload the sign-up POST does not read.
151    FileField(String),
152    /// A relationship choice, whose options would list records to a visitor nobody signed in.
153    RelationshipField(String),
154}
155
156impl fmt::Display for SignUpFault {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            Self::AuthDisabled => f.write_str(
160                "registration needs auth: `Auth::disabled()` serves no sign-in for a new \
161             account to land on",
162            ),
163            Self::UserType { registrar } => write!(
164                f,
165                "the registrar creates `{registrar}`, but the authenticator loads another \
166             type: register the type the panel signs in"
167            ),
168            Self::ReservedField(field) => write!(
169                f,
170                "the sign-up form declares a field named '{field}', which its POST carries \
171             itself: rename the field"
172            ),
173            Self::FileField(field) => write!(
174                f,
175                "the sign-up form declares a file field '{field}', which its POST does not \
176             upload: ask for the file once the account signs in"
177            ),
178            Self::RelationshipField(field) => write!(
179                f,
180                "the sign-up form declares a relationship choice '{field}', which would list \
181             records to a visitor nobody signed in: offer static options"
182            ),
183        }
184    }
185}
186
187/// Why a many-to-many field or relation cannot link records:
188/// [`DeclarationErrorKind::ManyToMany`].
189#[derive(Debug, Clone, PartialEq, Eq)]
190#[non_exhaustive]
191pub enum ManyToManyFault {
192    /// The field is no `#[has_many(via = joins.target)]` stepping through a `has_many` to a join
193    /// model, then the join model's `belongs_to` to the target.
194    NotJoinModel,
195    /// A foreign key of the join model spans several columns, or references no single-column
196    /// primary key.
197    CompositeKey,
198    /// The join model has a column a new link cannot fill: not nullable, not `#[auto]`, and not
199    /// one of its two foreign keys.
200    UnwritableColumn {
201        /// The join model.
202        model: String,
203        /// The column.
204        column: String,
205    },
206    /// The form's control for the field is not a multiple choice over a relationship.
207    NotMultipleChoice,
208    /// The relationship's records are not the target's.
209    OtherSource,
210    /// A multiple choice in a resource's form over no many-to-many field, which no column stores.
211    NoJoin,
212}
213
214/// What is wrong with a declaration.
215#[derive(Debug, Clone, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum DeclarationErrorKind {
218    /// The router holds no `toasty::Db`.
219    MissingDb,
220    /// Auth is on, but the `Db` does not register the shipped models it needs.
221    MissingAuthModels {
222        /// The unregistered models.
223        models: Vec<&'static str>,
224    },
225    /// `Panel::shell_assets` is set, but the router has no asset bundle.
226    ShellAssetsWithoutBundle,
227    /// A form declares a file field, but the panel installs no `Uploader` to store its bytes.
228    FileFieldWithoutUploader {
229        /// The file field's name.
230        field: String,
231    },
232    /// A path segment the panel routes cannot be a literal URL segment.
233    InvalidSegment {
234        /// What names the segment: `panel prefix`, `ResourceDef::slug` or `PageDef::slug`.
235        item: &'static str,
236        /// The refused segment.
237        segment: String,
238        /// Why it is refused.
239        fault: SegmentFault,
240    },
241    /// The prefix overlaps Topcoat's runtime endpoints.
242    PrefixOverlapsRuntime {
243        /// The panel's prefix.
244        prefix: String,
245    },
246    /// The prefix overlaps another panel's.
247    PrefixOverlapsPanel {
248        /// The other panel's prefix.
249        other: String,
250    },
251    /// A `Panel::serve_dir` pattern that does not end in a catch-all.
252    ServeDirWithoutCatchAll {
253        /// The pattern.
254        path: String,
255    },
256    /// The panel serves the same directory pattern twice.
257    ServeDirTwice {
258        /// The pattern.
259        path: String,
260    },
261    /// Another panel already serves the directory pattern.
262    ServeDirTaken {
263        /// The pattern.
264        path: String,
265        /// The serving panel's prefix.
266        panel: String,
267    },
268    /// A second `Panel::home` page.
269    SecondHome,
270    /// The resource is registered on the panel twice.
271    DuplicateResource,
272    /// The page is registered on the panel twice, `Panel::home` included.
273    DuplicatePage,
274    /// The resource is not mounted on the context's panel.
275    NotMounted,
276    /// A slug naming a route the panel serves itself.
277    ReservedSlug {
278        /// The slug.
279        slug: String,
280    },
281    /// A slug another resource or page already mounts at.
282    DuplicateSlug {
283        /// The slug.
284        slug: String,
285    },
286    /// Two actions share a `NAME`.
287    DuplicateAction {
288        /// The shared name.
289        name: &'static str,
290    },
291    /// An action's input cannot be served.
292    ActionInput {
293        /// The action's `NAME`.
294        action: &'static str,
295        /// What is wrong with it.
296        fault: ActionInputFault,
297    },
298    /// The panel's sign-up page cannot be served.
299    SignUp(SignUpFault),
300    /// Two relations to the same resource.
301    DuplicateRelation,
302    /// A relation to a resource the panel does not register.
303    UnregisteredRelation,
304    /// A relationship field, or a relationship filter, whose options come from a resource the
305    /// panel does not register.
306    UnregisteredOptionSource {
307        /// The field.
308        field: String,
309        /// The option source's type.
310        source: &'static str,
311    },
312    /// A relation column labelling its records by a resource the panel does not register.
313    UnregisteredLabelSource {
314        /// The column.
315        column: String,
316        /// The label source's type.
317        source: &'static str,
318    },
319    /// A lens with more than one step where a single field of the model is needed.
320    TraversalLens {
321        /// The lens's step count.
322        steps: usize,
323    },
324    /// A multi-step lens that resolves to no single column.
325    UnresolvedLens {
326        /// The lens's model.
327        model: &'static str,
328        /// The lens's field indices, step by step.
329        steps: Vec<usize>,
330    },
331    /// An embedded path or value its declaration never bound to the app schema.
332    Unbound {
333        /// The path's model, or the embedded value's type.
334        item: &'static str,
335    },
336    /// A `Tenancy::column` lens that names no field of the model.
337    TenancyColumnNotAField,
338    /// A `Tenancy::via` lens that names one field of the model.
339    TenancyViaOwnColumn,
340    /// A `Tenancy::via` lens whose first step is no `belongs_to` relation.
341    TenancyViaWithoutBelongsTo,
342    /// A `Tenancy::via` resource whose form writes the parent's foreign key other than through a
343    /// relationship field over a tenant-scoped resource of the parent's model.
344    UnguardedForeignKey {
345        /// The `belongs_to` relation the lens steps through.
346        relation: String,
347        /// The foreign-key columns written another way.
348        keys: Vec<String>,
349    },
350    /// A table with no column.
351    NoColumns,
352    /// `Table::paginate(0)`.
353    ZeroPageSize,
354    /// Two table columns share a name.
355    DuplicateColumn {
356        /// The shared name.
357        name: String,
358    },
359    /// Two table filters share a name.
360    DuplicateFilter {
361        /// The shared name.
362        name: String,
363    },
364    /// A table filter's name holds a `.`, which its URL parameters reserve for a part:
365    /// `f.<name>.<part>`.
366    DottedFilterName {
367        /// The name.
368        name: String,
369    },
370    /// A select filter option whose value the field's type does not parse, so choosing it
371    /// filters nothing.
372    UnparsedFilterOption {
373        /// The filter's name.
374        filter: String,
375        /// The option's value.
376        value: String,
377    },
378    /// Two schema fields share a name.
379    DuplicateField {
380        /// The shared name.
381        name: String,
382    },
383    /// A field disabled on the create form, with no `default` and no blank answer: no create
384    /// could fill it.
385    DisabledWithoutValue {
386        /// The disabled field.
387        field: String,
388    },
389    /// A repeater's item disables one of its row's controls or gives it a default, which a row
390    /// does not apply.
391    RepeaterLeafModifier {
392        /// The repeater.
393        field: String,
394        /// The control in its row.
395        leaf: String,
396    },
397    /// A field's default is a value its control never posts: one its type does not parse, one
398    /// not among a choice's options, or a list or one value where the control posts the other.
399    UnpostedDefault {
400        /// The field.
401        field: String,
402        /// The default, a list spelled `["a", "b"]`.
403        value: String,
404    },
405    /// A condition watches a field the schema does not place.
406    UnplacedWatchedField {
407        /// The watched field.
408        field: String,
409    },
410    /// A condition watches a field that another condition hides while what it guards still
411    /// shows.
412    HiddenWatchedField {
413        /// The watched field.
414        field: String,
415    },
416    /// A condition names a value its watched field never posts: not one of a choice's options, or
417    /// neither `true` nor `false` for a toggle.
418    UnpostedConditionValue {
419        /// The watched field.
420        field: String,
421        /// The value.
422        value: String,
423    },
424    /// A dependent choice's column belongs to another model than its relationship's source, or
425    /// the choice has no relationship.
426    MisdeclaredDependentChoice {
427        /// The dependent choice.
428        field: String,
429    },
430    /// A dependent choice that takes several options, or that depends on one that does: a
431    /// multiple choice's key holds a list, which narrows nothing.
432    MultipleDependentChoice {
433        /// The dependent choice.
434        field: String,
435    },
436    /// A dependent choice depends on a field the schema does not place.
437    UnplacedParentField {
438        /// The dependent choice.
439        field: String,
440        /// The field it depends on.
441        parent: String,
442    },
443    /// A field a condition hides has no blank answer, so a submission hiding it cannot parse.
444    RequiredConditionalField {
445        /// The field.
446        field: String,
447    },
448    /// A resource whose policy allows create but that has no form.
449    CreateWithoutForm,
450    /// A record-form field binding a key no control posts, after the form renders every field
451    /// it does not place: a hand-written [`RecordForm::control`](crate::RecordForm::control)
452    /// that renders no control for it.
453    MissingControl {
454        /// The record-form field.
455        field: String,
456        /// The key it binds.
457        key: String,
458    },
459    /// A tenant-scoped resource's record form claims the tenant column.
460    FormClaimsTenantColumn {
461        /// The record-form field.
462        field: String,
463        /// The tenant column.
464        column: String,
465    },
466    /// A choice field with neither options nor a relationship.
467    EmptyChoice {
468        /// The field.
469        field: String,
470    },
471    /// A relationship field, or a relationship filter, over a model with a composite primary key.
472    CompositeKeyChoice {
473        /// The field, or the filter's name.
474        field: String,
475    },
476    /// A field marked unique over a column no unique index covers.
477    UniqueWithoutIndex {
478        /// The field.
479        field: String,
480    },
481    /// A unique field whose non-nullable column stores one value for every empty submission.
482    OptionalUnique {
483        /// The field.
484        field: String,
485    },
486    /// [`create_column`](crate::ResourceDef::create_column) names the tenant column.
487    CreateColumnsNameTenant {
488        /// The tenant column.
489        column: String,
490    },
491    /// A non-nullable column nothing writes on create.
492    UnwrittenColumn {
493        /// The column.
494        column: String,
495    },
496    /// A many-to-many field or relation whose join model cannot link records.
497    ManyToMany {
498        /// The field, or the relation's `via` field.
499        field: String,
500        /// What is wrong.
501        fault: ManyToManyFault,
502    },
503}
504
505impl fmt::Display for DeclarationErrorKind {
506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507        match self {
508            Self::MissingDb => f.write_str(
509                "the router holds no Db: install it with `.app_context(db)` before mounting the \
510                 panel",
511            ),
512            Self::MissingAuthModels { models } => write!(
513                f,
514                "auth is on, but the Db does not register its shipped models ({}): register them \
515                 with `toasty::models!(…, tablo_core::auth::AdminUser, \
516                 tablo_core::auth::AuthSession)`, or opt out with `.auth(Auth::disabled())`",
517                models.join(", ")
518            ),
519            Self::ShellAssetsWithoutBundle => f.write_str(
520                "shell_assets need the router's asset bundle: install it with `.assets(..)` \
521                 before mounting the panel",
522            ),
523            Self::FileFieldWithoutUploader { field } => write!(
524                f,
525                "file field `{field}` needs an uploader to store its bytes: install one with \
526                 `Panel::uploads`"
527            ),
528            Self::InvalidSegment {
529                item,
530                segment,
531                fault,
532            } => write!(f, "{item} '{segment}' {fault}"),
533            Self::PrefixOverlapsRuntime { prefix } => write!(
534                f,
535                "prefix '{prefix}' overlaps Topcoat's runtime endpoints at '{}'",
536                crate::topcoat_compat::RUNTIME_PREFIX
537            ),
538            Self::PrefixOverlapsPanel { other } => write!(
539                f,
540                "the prefix overlaps the panel mounted at '{other}': each panel needs a prefix of \
541                 its own"
542            ),
543            Self::ServeDirWithoutCatchAll { path } => write!(
544                f,
545                "serve_dir path '{path}' must end in a catch-all like '/uploads/{{*file}}'"
546            ),
547            Self::ServeDirTwice { path } => write!(f, "serve_dir path '{path}' is declared twice"),
548            Self::ServeDirTaken { path, panel } => write!(
549                f,
550                "serve_dir path '{path}' is already served by the panel mounted at '{panel}'"
551            ),
552            Self::SecondHome => {
553                f.write_str("a home page is already registered: `Panel::home` takes one")
554            }
555            Self::DuplicateResource => f.write_str(
556                "registered twice: a panel mounts each resource type once, so mount a variant \
557                 under its own type",
558            ),
559            Self::DuplicatePage => f.write_str(
560                "registered twice: a panel mounts each page type once, so mount a variant under \
561                 its own type",
562            ),
563            Self::NotMounted => f.write_str(
564                "not mounted on the context's panel: register it with `Panel::resource`",
565            ),
566            Self::ReservedSlug { slug } => {
567                write!(f, "slug '{slug}' names a route the panel serves itself")
568            }
569            Self::DuplicateSlug { slug } => write!(
570                f,
571                "slug '{slug}' is taken by another resource or page: each needs a distinct `slug()`"
572            ),
573            Self::DuplicateAction { name } => write!(
574                f,
575                "two actions are named '{name}': each needs a distinct `NAME`"
576            ),
577            Self::ActionInput { action, fault } => match fault {
578                ActionInputFault::ReservedField(field) => write!(
579                    f,
580                    "action '{action}' declares an input field named '{field}', which its POST \
581                     carries itself: rename the field"
582                ),
583                ActionInputFault::FileField(field) => write!(
584                    f,
585                    "action '{action}' declares a file field '{field}' in its input, which an \
586                     action's POST does not upload: take the file in a record form"
587                ),
588                ActionInputFault::RefusesEmpty => write!(
589                    f,
590                    "action '{action}' declares no input field, but its input refuses an empty \
591                     submission: declare the fields it parses, or name `()`"
592                ),
593                ActionInputFault::DependentChoice(field) => write!(
594                    f,
595                    "action '{action}' declares a dependent choice '{field}' in its input, whose \
596                     options only a resource's form refreshes: drop `depends_on`"
597                ),
598            },
599            Self::SignUp(fault) => fault.fmt(f),
600            Self::DuplicateRelation => {
601                f.write_str("declared twice: each related resource is one relation")
602            }
603            Self::UnregisteredRelation => f.write_str(
604                "the related resource is not registered on this panel: declare it with \
605                 `Panel::resource`",
606            ),
607            Self::UnregisteredOptionSource { field, source } => write!(
608                f,
609                "field '{field}' takes its options from `{source}`, which this panel does not \
610                 register: declare it with `Panel::resource`"
611            ),
612            Self::UnregisteredLabelSource { column, source } => write!(
613                f,
614                "column '{column}' labels its records by `{source}`, which this panel does not \
615                 register: declare it with `Panel::resource`"
616            ),
617            Self::TraversalLens { steps } => write!(
618                f,
619                "a lens here names one field of its model, not a {steps}-step path"
620            ),
621            Self::UnresolvedLens { model, steps } => write!(
622                f,
623                "lens path {steps:?} resolves to no single column of `{model}`: only embedded \
624                 steps (embedded structs, enum variant fields and `#[document]` fields) bind, not \
625                 relation hops"
626            ),
627            Self::Unbound { item } => write!(
628                f,
629                "an embedded path into `{item}` is not bound to the app schema: a panel binds the \
630                 declarations it mounts; bind one built outside a panel with `.bind(&db)`"
631            ),
632            Self::TenancyColumnNotAField => f.write_str(
633                "the `Tenancy::column` lens names no field of the model: name a field typed \
634                 `TenantId`, or use `Tenancy::via` for a tenant reached through a relation",
635            ),
636            Self::TenancyViaOwnColumn => f.write_str(
637                "the `Tenancy::via` lens names one field of the model: use `Tenancy::column` for \
638                 the model's own tenant column",
639            ),
640            Self::TenancyViaWithoutBelongsTo => f.write_str(
641                "the `Tenancy::via` lens does not start at a `belongs_to` relation: the tenant \
642                 must be the parent's its foreign key names",
643            ),
644            Self::UnguardedForeignKey { relation, keys } => write!(
645                f,
646                "foreign key `{}` is not written through a relationship field over a \
647                 tenant-scoped resource of `{relation}`'s model: the write re-checks only such a \
648                 field's key, so any other could attach the row to another tenant's parent",
649                keys.join("`, `")
650            ),
651            Self::NoColumns => f.write_str(
652                "no column: declare columns with `Table::new(columns)` or in `ResourceDef::table`",
653            ),
654            Self::ZeroPageSize => f.write_str("`Table::paginate` needs a page size of at least 1"),
655            Self::DuplicateColumn { name } => write!(
656                f,
657                "two columns are named '{name}': each column needs a distinct name"
658            ),
659            Self::DuplicateFilter { name } => write!(
660                f,
661                "two filters are named '{name}': each filter needs a distinct name"
662            ),
663            Self::DottedFilterName { name } => write!(f, "filter name '{name}' holds a '.'"),
664            Self::UnparsedFilterOption { filter, value } => write!(
665                f,
666                "filter '{filter}' offers '{value}', which its field's type does not parse: spell \
667                 each option as the field's form value, or take the type's own with \
668                 `SelectFilter::of`"
669            ),
670            Self::DuplicateField { name } => write!(
671                f,
672                "two fields are named '{name}': each input needs a distinct field"
673            ),
674            Self::DisabledWithoutValue { field } => write!(
675                f,
676                "field '{field}' is disabled and required, so no create can fill it: give it a \
677                 `default` (a file field takes none), make it optional, or use `disabled_on_edit`"
678            ),
679            Self::RepeaterLeafModifier { field, leaf } => write!(
680                f,
681                "repeater '{field}' disables its row's '{leaf}' or gives it a default, which a \
682                 row does not apply: disable the repeater's own field instead"
683            ),
684            Self::UnpostedDefault { field, value } => write!(
685                f,
686                "field '{field}' defaults to '{value}', which its control never posts: spell the \
687                 default as the field's form value, a list for a multiple choice"
688            ),
689            Self::UnplacedWatchedField { field } => write!(
690                f,
691                "a condition watches field '{field}', which the schema does not place: place it \
692                 in the same schema"
693            ),
694            Self::HiddenWatchedField { field } => write!(
695                f,
696                "a condition watches field '{field}', which a condition hides while the guarded \
697                 field or block still shows: place the guarded one inside the block that hides \
698                 '{field}'"
699            ),
700            Self::UnpostedConditionValue { field, value } => write!(
701                f,
702                "a condition shows a field while '{field}' posts '{value}', which it never posts: \
703                 name one of its option values, or `true` or `false` for a toggle"
704            ),
705            Self::MisdeclaredDependentChoice { field } => write!(
706                f,
707                "choice '{field}' depends on a column its options do not have: call \
708                 `relationship::<R>()` and name a column of `R`'s model in `depends_on`"
709            ),
710            Self::MultipleDependentChoice { field } => write!(
711                f,
712                "choice '{field}' depends on another field through a multiple choice, which \
713                 neither narrows another choice nor is narrowed: drop `depends_on` or `multiple`"
714            ),
715            Self::UnplacedParentField { field, parent } => write!(
716                f,
717                "choice '{field}' depends on field '{parent}', which the schema does not place: \
718                 place it in the same schema"
719            ),
720            Self::RequiredConditionalField { field } => write!(
721                f,
722                "field '{field}' is conditional but has no blank answer: a submission that hides \
723                 it posts nothing, so make it an `Option` or give it `#[form(blank = ..)]`"
724            ),
725            Self::CreateWithoutForm => f.write_str(
726                "the policy allows create, but there is no form: name the record form in `type \
727                 Form`",
728            ),
729            Self::MissingControl { field, key } => write!(
730                f,
731                "record-form field `{field}` binds key `{key}`, but no control posts it: \
732                 `RecordForm::control` must render the field's control"
733            ),
734            Self::FormClaimsTenantColumn { field, column } => write!(
735                f,
736                "record-form field `{field}` claims the tenant column `{column}`, which the \
737                 framework stamps on create: drop it from the form"
738            ),
739            Self::EmptyChoice { field } => write!(
740                f,
741                "choice field `{field}` declares no options and no relationship, so it offers \
742                 nothing to choose: add `.options(..)` or `.relationship::<Source>()`, or \
743                 `#[form(relationship = Source)]` on a record form's field"
744            ),
745            Self::CompositeKeyChoice { field } => write!(
746                f,
747                "`{field}` reads a relationship whose model has a composite primary key, which \
748                 no option value can spell"
749            ),
750            Self::UniqueWithoutIndex { field } => write!(
751                f,
752                "field `{field}` is marked unique, but no unique index covers its column: add \
753                 `#[unique]` (or `#[unique(..)]`) to the model or drop `.unique()`, which would \
754                 otherwise check a rule the database does not enforce"
755            ),
756            Self::OptionalUnique { field } => write!(
757                f,
758                "field `{field}` is marked unique, but its record-form field answers an empty \
759                 submission and its column is not nullable, so every empty submission stores \
760                 the same value: make the field required, or an `Option`"
761            ),
762            Self::CreateColumnsNameTenant { column } => write!(
763                f,
764                "`create_column` names the tenant column `{column}`, which the framework stamps \
765                 on create: drop it there and delegate to `write_create`"
766            ),
767            Self::UnwrittenColumn { column } => write!(
768                f,
769                "the policy allows create, but nothing writes the non-nullable column `{column}`: \
770                 the record form has no such field, toasty fills no `#[default(..)]` for it, and \
771                 no `create_column` names it, so every create would fail at the driver"
772            ),
773            Self::ManyToMany { field, fault } => match fault {
774                ManyToManyFault::NotJoinModel => write!(
775                    f,
776                    "`{field}` is no many-to-many field: declare it \
777                     `#[has_many(via = joins.target)]`, through a `has_many` to a join model and \
778                     the join model's `belongs_to` to the target"
779                ),
780                ManyToManyFault::CompositeKey => write!(
781                    f,
782                    "`{field}`'s join model holds a key of several columns, or one referencing no \
783                     single-column primary key: a link writes one column per side"
784                ),
785                ManyToManyFault::UnwritableColumn { model, column } => write!(
786                    f,
787                    "`{field}`'s join model `{model}` has a column `{column}` a new link cannot \
788                     fill: make it nullable or `#[auto]`, or link the records in your own record fn"
789                ),
790                ManyToManyFault::NotMultipleChoice => write!(
791                    f,
792                    "`{field}` is a many-to-many field, so its control is a multiple choice over \
793                     a relationship: `.choice().relationship::<R>().multiple()`"
794                ),
795                ManyToManyFault::OtherSource => write!(
796                    f,
797                    "`{field}` offers another model's records than the ones its join model links"
798                ),
799                ManyToManyFault::NoJoin => write!(
800                    f,
801                    "multiple choice `{field}` names no many-to-many field of the model, so \
802                     nothing would store its records: bind it to a `#[has_many(via = ..)]` field"
803                ),
804            },
805        }
806    }
807}
808
809/// Why a path segment cannot be a literal URL segment.
810#[derive(Debug, Clone, Copy, PartialEq, Eq)]
811#[non_exhaustive]
812pub enum SegmentFault {
813    /// The segment is empty.
814    Empty,
815    /// The segment is `.` or `..`.
816    Dot,
817    /// The segment holds a character a route segment cannot.
818    Char(char),
819}
820
821impl fmt::Display for SegmentFault {
822    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823        match self {
824            Self::Empty => f.write_str("is empty"),
825            Self::Dot => f.write_str("is '.' or '..'"),
826            Self::Char(c) => write!(
827                f,
828                "contains {c:?}: quotes, backslashes, control characters, whitespace, URL \
829                 punctuation and the route pattern characters '{{', '}}', '(' and ')' are refused"
830            ),
831        }
832    }
833}
834
835/// Why `segment` cannot be a literal URL segment, if it cannot.
836///
837/// A `const fn`, so an action's `NAME` is checked when the panel's code compiles.
838pub(crate) const fn segment_fault(segment: &str) -> Option<SegmentFault> {
839    let bytes = segment.as_bytes();
840    if bytes.is_empty() {
841        return Some(SegmentFault::Empty);
842    }
843    if matches!(bytes, b"." | b"..") {
844        return Some(SegmentFault::Dot);
845    }
846    let mut at = 0;
847    while at < bytes.len() {
848        let (c, width) = decode_char(bytes, at);
849        if c.is_control()
850            || c.is_whitespace()
851            || matches!(
852                c,
853                '"' | '\\' | '/' | '?' | '#' | '%' | '&' | '=' | '{' | '}' | '(' | ')'
854            )
855        {
856            return Some(SegmentFault::Char(c));
857        }
858        at += width;
859    }
860    None
861}
862
863/// The character starting at byte `at` of valid UTF-8, and its width.
864const fn decode_char(bytes: &[u8], at: usize) -> (char, usize) {
865    let lead = bytes[at] as u32;
866    let (code, width) = if lead < 0x80 {
867        (lead, 1)
868    } else if lead < 0xE0 {
869        ((lead & 0x1F) << 6 | tail(bytes, at + 1), 2)
870    } else if lead < 0xF0 {
871        (
872            (lead & 0x0F) << 12 | tail(bytes, at + 1) << 6 | tail(bytes, at + 2),
873            3,
874        )
875    } else {
876        (
877            (lead & 0x07) << 18
878                | tail(bytes, at + 1) << 12
879                | tail(bytes, at + 2) << 6
880                | tail(bytes, at + 3),
881            4,
882        )
883    };
884    match char::from_u32(code) {
885        Some(c) => (c, width),
886        None => panic!("a str holds valid UTF-8"),
887    }
888}
889
890/// The six payload bits of the continuation byte at `at`.
891const fn tail(bytes: &[u8], at: usize) -> u32 {
892    bytes[at] as u32 & 0x3F
893}
894
895#[cfg(test)]
896mod tests;