use std::fmt;
use super::{
DeclarationError, DeclarationErrorKind, fmt_action_input, fmt_field_rule, fmt_many_to_many,
};
impl fmt::Display for DeclarationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(resource) = self.resource {
write!(f, "`{resource}`{}: ", self.site)?;
}
write!(f, "{}", self.kind)
}
}
const EMPTY_DETAIL: &str = "the view page shows a detail listing no column: declare a `detail` or \
a table, or turn the page off with `without_page(ResourcePage::View)`";
impl fmt::Display for DeclarationErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingDb => f.write_str(
"the router holds no Db: install it with `.with(db)` before mounting the \
panel",
),
Self::MissingAuthModels { models } => write!(
f,
"auth is on, but the Db does not register its shipped models ({models}): build \
the Db from `tablo::models!(…)`, which registers them, or opt out with \
`.auth(Auth::disabled())`",
models = models.join(", ")
),
Self::ShellAssetsWithoutBundle => f.write_str(
"shell_assets need the router's asset bundle: install it with `.assets(..)` \
before mounting the panel",
),
Self::NotificationsWithoutAuth => f.write_str(
"notifications are for signed-in users: `Panel::notifications` needs auth on",
),
Self::MissingAuditModel => f.write_str(
"`Panel::audit_log` stores its entries in `AuditEntry`, which the Db does not \
register: build the Db from `tablo::models!(…)`, or add `AuditEntry` to \
`toasty::models!(…)`",
),
Self::BrandColor { value } => write!(
f,
"the brand color `{value}` is no CSS color: spell it `#4f46e5` or \
`oklch(0.55 0.2 260)`",
),
Self::ExternalAuthFeature { feature } => write!(
f,
"`Auth::external` leaves credentials to the host app: drop `{feature}`",
),
Self::FileFieldWithoutUploader { field } => write!(
f,
"file field `{field}` needs an uploader to store its bytes: install one with \
`Panel::uploads`"
),
Self::InvalidSegment { .. }
| Self::PrefixOverlapsRuntime { .. }
| Self::PrefixOverlapsPanel { .. }
| Self::EndpointOutsideRuntime { .. }
| Self::ServeDirWithoutCatchAll { .. }
| Self::ServeDirTwice { .. }
| Self::ServeDirTaken { .. } => fmt_route(f, self),
Self::ContentSecurityPolicy(fault) => fault.fmt(f),
Self::SecondHome => {
f.write_str("a home page is already registered: `Panel::home` takes one")
}
Self::NoRoot => f.write_str(
"every resource is hidden from navigation and no home page is registered, so the \
panel's root, where a sign-in lands, leads nowhere: show one resource in the \
sidebar, or register a page with `Panel::home`",
),
Self::DuplicateResource => f.write_str(
"registered twice: a panel mounts each resource type once, so mount a variant \
under its own type",
),
Self::DuplicatePage => f.write_str(
"registered twice: a panel mounts each page type once, so mount a variant under \
its own type",
),
Self::NotMounted => f.write_str(
"not mounted on the context's panel: register it with `Panel::resource`",
),
Self::ReservedSlug { slug } => {
write!(f, "slug '{slug}' names a route the panel serves itself")
}
Self::DuplicateSlug { slug } => write!(
f,
"slug '{slug}' is taken by another resource or page: each needs a distinct `slug()`"
),
Self::DuplicateAction { name } => write!(
f,
"two actions are named '{name}': each needs a distinct `NAME`"
),
Self::DuplicateWidget { name } => write!(
f,
"two widgets are named '{name}': each needs a distinct `WidgetDef::name`"
),
Self::MisdeclaredPageFilter { key } => write!(
f,
"page filter '{key}' shares its key with another, or its key is not ASCII \
letters, digits, `-` and `_`: give each filter a distinct key of those"
),
Self::EditableColumn { column, fault } => fault.fmt(f, column),
Self::ActionInput { action, fault } => fmt_action_input(f, action, fault),
Self::SignUp(fault) => fault.fmt(f),
Self::Profile(fault) => fault.fmt(f),
Self::Area(fault) => fault.fmt(f),
Self::PasswordReset(fault) => fault.fmt(f),
Self::EmailVerification(fault) => fault.fmt(f),
Self::TenantRegistration(fault) => fault.fmt(f),
Self::TenantProfile(fault) => fault.fmt(f),
Self::TwoFactor(fault) => fault.fmt(f),
Self::Oidc(fault) => fault.fmt(f),
Self::AuthMail(fault) => fault.fmt(f),
Self::Locale(fault) => fault.fmt(f),
Self::DuplicateRelation => {
f.write_str("declared twice: each related resource is one relation")
}
Self::HasOneTab => f.write_str(
"a `has_one` relation cannot be a tab: its form edits inside the record's own \
form",
),
Self::UnregisteredRelation => f.write_str(
"the related resource is not registered on this panel: declare it with \
`Panel::resource`",
),
Self::UnregisteredOptionSource { field, source } => write!(
f,
"field '{field}' takes its options from `{source}`, which this panel does not \
register: declare it with `Panel::resource`"
),
Self::UnregisteredLabelSource { column, source } => write!(
f,
"column '{column}' labels its records by `{source}`, which this panel does not \
register: declare it with `Panel::resource`"
),
Self::TraversalLens { steps } => write!(
f,
"a lens here names one field of its model, not a {steps}-step path"
),
Self::UnresolvedLens { model, steps } => write!(
f,
"lens path {steps:?} resolves to no single column of `{model}`: only embedded \
steps (embedded structs, enum variant fields and `#[document]` fields) bind, not \
relation hops"
),
Self::Uncompiled { model, error } => write!(
f,
"`{model}` compiles to no schema, so its embedded paths name no column: {error}"
),
Self::Tenancy { fault } => fault.fmt(f),
Self::FieldAccess { fault } => fault.fmt(f),
Self::NoColumns => f.write_str(
"no column: declare columns with `Table::new(columns)` or in `ResourceDef::table`",
),
Self::ZeroPageSize => f.write_str("`Table::page_size` needs a page size of at least 1"),
Self::UnsortableDefaultSort { column } => write!(
f,
"`Table::default_sort_by` sorts on '{column}', but the table declares no sortable column for it"
),
Self::DuplicateColumn { name } => write!(
f,
"two columns are named '{name}': each column needs a distinct name"
),
Self::DuplicateFilter { name } => write!(
f,
"two filters are named '{name}': each filter needs a distinct name"
),
Self::DuplicateTab { key } => write!(
f,
"two list tabs share the key '{key}': each tab needs a distinct key"
),
Self::DuplicateGrouping { name } => write!(
f,
"the table groups by '{name}' twice: declare each grouping once"
),
Self::DottedFilterName { name } => write!(f, "filter name '{name}' holds a '.'"),
Self::UnparsedFilterOption { filter, value } => write!(
f,
"filter '{filter}' offers '{value}', which its field's type does not parse: spell \
each option as the field's form value, or take the type's own with \
`SelectFilter::of`"
),
Self::UnparsedTabOption { value } => write!(
f,
"the option tabs offer '{value}', which the field's type does not parse: spell \
each option as the field's form value"
),
Self::DuplicateField { name } => write!(
f,
"two fields are named '{name}': each input needs a distinct field"
),
Self::DisabledWithoutValue { field } => write!(
f,
"field '{field}' is disabled or hidden on the create form, or a condition can \
disable it, and it is required, so a create cannot fill it: give it a `default` \
(a file field takes none), make it optional, or use `disabled_on_edit`"
),
Self::RepeaterLeafModifier { field, leaf } => write!(
f,
"repeater '{field}' disables its row's '{leaf}' or gives it a default, which a \
row does not apply: disable the repeater's own field instead"
),
Self::RepeaterPassword { field, leaf } => write!(
f,
"repeater '{field}' rows hold password '{leaf}', which an edit never renders \
back, so each save overwrites it: keep the secret out of the item"
),
Self::FieldRule { field, fault } => fmt_field_rule(f, field, fault),
Self::UnpostedDefault { field, value } => write!(
f,
"field '{field}' defaults to '{value}', which its control never posts: spell the \
default as the field's form value, a list for a multiple choice"
),
Self::UnplacedWatchedField { .. }
| Self::HiddenWatchedField { .. }
| Self::DisabledWatchedField { .. }
| Self::UnpostedConditionValue { .. } => fmt_condition(f, self),
Self::MisdeclaredDependentChoice { .. }
| Self::UnfilterableChoice { .. }
| Self::MultipleDependentChoice { .. }
| Self::ListedDependentChoice { .. }
| Self::UncreatableChoice { .. }
| Self::UnplacedParentField { .. } => fmt_choice(f, self),
Self::RequiredConditionalField { field } => write!(
f,
"field '{field}' is conditional but has no blank answer: a submission that hides \
it posts nothing, so make it an `Option` or give it `#[form(blank = ..)]`"
),
Self::MissingControl { field, key } => write!(
f,
"record-form field `{field}` binds key `{key}`, but no control posts it: \
`RecordForm::control` must render the field's control"
),
Self::FormClaimsTenantColumn { field, column } => write!(
f,
"record-form field `{field}` claims the tenant column `{column}`, which the \
framework stamps on create: drop it from the form"
),
Self::FormClaimsSoftDeleteColumn { field, column } => write!(
f,
"record-form field `{field}` claims the soft-delete column `{column}`, which only \
a delete and a restore write: drop it from the form, or mark it \
`#[form(skip)]` on a `ModelForm`"
),
Self::ReservedFormField { field } => write!(
f,
"form field `{field}` takes a name the form's POST carries itself, so the two \
would read each other's value: rename the field"
),
Self::EmptyChoice { field } => write!(
f,
"choice field `{field}` declares no options and no relationship, so it offers \
nothing to choose: add `.options(..)` or `.relationship::<Source>()`, or \
`#[form(relationship = Source)]` on a record form's field"
),
Self::CompositeKeyChoice { field } => write!(
f,
"`{field}` reads a relationship whose model has a composite primary key, which \
no option value can spell"
),
Self::UniqueWithoutIndex { field } => write!(
f,
"field `{field}` is marked unique, but no unique index covers its column: add \
`#[unique]` (or `#[unique(..)]`) to the model or drop `.unique()`, which would \
otherwise check a rule the database does not enforce"
),
Self::OptionalUnique { field } => write!(
f,
"field `{field}` is marked unique, but its record-form field answers an empty \
submission and its column is not nullable, so every empty submission stores \
the same value: make the field required, or an `Option`"
),
Self::CreateColumnsNameTenant { column } => write!(
f,
"`create_column` names the tenant column `{column}`, which the framework stamps \
on create: drop it there and delegate to `write_create`"
),
Self::UnwrittenColumn { column } => write!(
f,
"nothing writes the non-nullable column `{column}` on create: the record form has \
no such field, toasty fills no `#[default(..)]` for it, and no `create_column` \
names it, so every create would fail at the driver; a resource that creates \
nothing declares `without_page(ResourcePage::Create)`"
),
Self::ImportWithoutCreate => write!(
f,
"`importable` creates each row through the create form, which this resource does \
not serve: it has no record form, or declares `without_page(ResourcePage::Create)`"
),
Self::EmptyDetail => f.write_str(EMPTY_DETAIL),
Self::RelatedForm(fault) => fault.fmt(f),
Self::ManyToMany { field, fault } => fmt_many_to_many(f, field, fault),
}
}
}
fn fmt_choice(f: &mut fmt::Formatter<'_>, kind: &DeclarationErrorKind) -> fmt::Result {
match kind {
DeclarationErrorKind::MisdeclaredDependentChoice { field } => write!(
f,
"choice '{field}' depends on a column its options do not have: call \
`relationship::<R>()` and name a column of `R`'s model in `depends_on`"
),
DeclarationErrorKind::UnfilterableChoice { field } => write!(
f,
"choice '{field}' filters its options but loads none from a relationship: call \
`relationship::<R>()` or drop `options_where`"
),
DeclarationErrorKind::MultipleDependentChoice { field } => write!(
f,
"choice '{field}' depends on another field through a multiple choice, which \
neither narrows another choice nor is narrowed: drop `depends_on` or `multiple`"
),
DeclarationErrorKind::ListedDependentChoice { field } => write!(
f,
"choice '{field}' depends on another field but renders its options as radios or \
buttons, which the browser cannot narrow: drop `depends_on`, or `radio` or \
`buttons`"
),
DeclarationErrorKind::UncreatableChoice { field } => write!(
f,
"choice '{field}' is creatable but is not a single select over a resource's \
records: drop `creatable`, or declare a single select with \
`relationship::<R>()` over a resource `R`, outside a repeater and without \
`disabled_when`"
),
DeclarationErrorKind::UnplacedParentField { field, parent } => write!(
f,
"choice '{field}' depends on field '{parent}', which the schema does not place: \
place it in the same schema"
),
_ => unreachable!("not a choice's error"),
}
}
fn fmt_condition(f: &mut fmt::Formatter<'_>, kind: &DeclarationErrorKind) -> fmt::Result {
match kind {
DeclarationErrorKind::UnplacedWatchedField { field } => write!(
f,
"a condition watches field '{field}', which the schema does not place: place it \
in the same schema"
),
DeclarationErrorKind::HiddenWatchedField { field } => write!(
f,
"a condition watches field '{field}', which a condition hides while the guarded \
field or block still shows: place the guarded one inside the block that hides \
'{field}'"
),
DeclarationErrorKind::DisabledWatchedField { field } => write!(
f,
"a condition or a dependent choice watches field '{field}', which a condition \
disables and so posts nothing: watch another field, or drop `disabled_when` from \
'{field}'"
),
DeclarationErrorKind::UnpostedConditionValue { field, value } => write!(
f,
"a condition shows a field while '{field}' posts '{value}', which it never posts: \
name one of its option values, or `true` or `false` for a toggle"
),
_ => unreachable!("not a condition's error"),
}
}
fn fmt_route(f: &mut fmt::Formatter<'_>, kind: &DeclarationErrorKind) -> fmt::Result {
match kind {
DeclarationErrorKind::InvalidSegment {
item,
segment,
fault,
} => write!(f, "{item} '{segment}' {fault}"),
DeclarationErrorKind::PrefixOverlapsRuntime { prefix } => write!(
f,
"prefix '{prefix}' overlaps Topcoat's runtime endpoints at '{}'",
crate::topcoat_compat::RUNTIME_PREFIX
),
DeclarationErrorKind::PrefixOverlapsPanel { other } => write!(
f,
"the prefix overlaps the panel mounted at '{other}': each panel needs a prefix of \
its own"
),
DeclarationErrorKind::EndpointOutsideRuntime { path } => write!(
f,
"endpoint '{path}' is not a shard or procedure under '{}': the panel's prefix or \
the app's own layers guard a route elsewhere",
crate::topcoat_compat::RUNTIME_PREFIX
),
DeclarationErrorKind::ServeDirWithoutCatchAll { path } => write!(
f,
"serve_dir path '{path}' must end in a catch-all like '/uploads/{{*file}}'"
),
DeclarationErrorKind::ServeDirTwice { path } => {
write!(f, "serve_dir path '{path}' is declared twice")
}
DeclarationErrorKind::ServeDirTaken { path, panel } => write!(
f,
"serve_dir path '{path}' is already served by the panel mounted at '{panel}'"
),
_ => unreachable!("not a route's error"),
}
}