1use std::fmt;
4
5#[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 pub fn panel(&self) -> &str {
26 &self.panel
27 }
28
29 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#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct DeclarationError {
51 pub resource: Option<&'static str>,
53 pub site: Site,
55 pub kind: DeclarationErrorKind,
57}
58
59impl DeclarationError {
60 pub(crate) fn panel(kind: DeclarationErrorKind) -> Self {
62 Self {
63 resource: None,
64 site: Site::Panel,
65 kind,
66 }
67 }
68
69 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::View => f.write_str(" view")?,
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#[derive(Debug, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub enum Site {
103 Panel,
105 Registration,
108 Tenancy,
110 Table,
112 Form,
114 View,
116 Relation(String),
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123#[non_exhaustive]
124pub enum DeclarationErrorKind {
125 MissingDb,
127 MissingAuthModels {
129 models: Vec<&'static str>,
131 },
132 ShellAssetsWithoutBundle,
134 InvalidSegment {
136 item: &'static str,
138 segment: String,
140 fault: SegmentFault,
142 },
143 PrefixOverlapsRuntime {
145 prefix: String,
147 },
148 PrefixOverlapsPanel {
150 other: String,
152 },
153 ServeDirWithoutCatchAll {
155 path: String,
157 },
158 ServeDirTwice {
160 path: String,
162 },
163 ServeDirTaken {
165 path: String,
167 panel: String,
169 },
170 SecondHome,
172 DuplicateResource,
174 NotMounted,
176 ReservedSlug {
178 slug: String,
180 },
181 DuplicateSlug {
183 slug: String,
185 },
186 DuplicateAction {
188 name: &'static str,
190 },
191 DuplicateRelation,
193 UnregisteredRelation,
195 UnregisteredOptionSource {
197 field: String,
199 source: &'static str,
201 },
202 TraversalLens {
204 steps: usize,
206 },
207 UnresolvedLens {
209 model: &'static str,
211 steps: Vec<usize>,
213 },
214 TenancyColumnNotAField,
216 TenancyViaOwnColumn,
218 TenancyViaWithoutBelongsTo,
220 UnguardedForeignKey {
223 relation: String,
225 keys: Vec<String>,
227 },
228 NoColumns,
230 ZeroPageSize,
232 DuplicateColumn {
234 name: String,
236 },
237 DuplicateFilter {
239 name: String,
241 },
242 DuplicateField {
244 name: String,
246 },
247 EmptyFormOverride,
249 FormWithoutRecordForm,
251 CreateWithoutForm,
253 UnboundControl {
255 control: String,
257 },
258 ControlBoundTwice {
260 control: String,
262 },
263 MissingControl {
265 field: String,
267 key: String,
269 },
270 NoBlankAnswer {
272 control: String,
274 field: String,
276 in_repeater: bool,
278 },
279 FormClaimsTenantColumn {
281 field: String,
283 column: String,
285 },
286 CompositeKeyChoice {
288 field: String,
290 },
291 UniqueWithoutIndex {
293 field: String,
295 },
296 CreateColumnsNameTenant {
298 column: String,
300 },
301 UnknownCreateColumn {
303 column: &'static str,
305 },
306 UnwrittenColumn {
308 column: String,
310 },
311}
312
313impl fmt::Display for DeclarationErrorKind {
314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 match self {
316 Self::MissingDb => f.write_str(
317 "the router holds no Db: install it with `.app_context(db)` before mounting the \
318 panel",
319 ),
320 Self::MissingAuthModels { models } => write!(
321 f,
322 "auth is on, but the Db does not register its shipped models ({}): register them \
323 with `toasty::models!(…, tablo_core::auth::AdminUser, \
324 tablo_core::auth::AuthSession)`, or opt out with `.auth(Auth::disabled())`",
325 models.join(", ")
326 ),
327 Self::ShellAssetsWithoutBundle => f.write_str(
328 "shell_assets need the router's asset bundle: install it with `.assets(..)` \
329 before mounting the panel",
330 ),
331 Self::InvalidSegment {
332 item,
333 segment,
334 fault,
335 } => write!(f, "{item} '{segment}' {fault}"),
336 Self::PrefixOverlapsRuntime { prefix } => write!(
337 f,
338 "prefix '{prefix}' overlaps Topcoat's runtime endpoints at '{}'",
339 crate::topcoat_compat::RUNTIME_PREFIX
340 ),
341 Self::PrefixOverlapsPanel { other } => write!(
342 f,
343 "the prefix overlaps the panel mounted at '{other}': each panel needs a prefix of \
344 its own"
345 ),
346 Self::ServeDirWithoutCatchAll { path } => write!(
347 f,
348 "serve_dir path '{path}' must end in a catch-all like '/uploads/{{*file}}'"
349 ),
350 Self::ServeDirTwice { path } => write!(f, "serve_dir path '{path}' is declared twice"),
351 Self::ServeDirTaken { path, panel } => write!(
352 f,
353 "serve_dir path '{path}' is already served by the panel mounted at '{panel}'"
354 ),
355 Self::SecondHome => {
356 f.write_str("a home page is already registered: `Panel::home` takes one")
357 }
358 Self::DuplicateResource => f.write_str(
359 "registered twice: a panel mounts each resource type once, so mount a variant \
360 under its own type",
361 ),
362 Self::NotMounted => f.write_str(
363 "not mounted on the request's panel: register it with `Panel::resource`",
364 ),
365 Self::ReservedSlug { slug } => {
366 write!(f, "slug '{slug}' names a route the panel serves itself")
367 }
368 Self::DuplicateSlug { slug } => write!(
369 f,
370 "slug '{slug}' is taken by another resource or page: each needs a distinct `slug()`"
371 ),
372 Self::DuplicateAction { name } => write!(
373 f,
374 "two actions are named '{name}': each needs a distinct `NAME`"
375 ),
376 Self::DuplicateRelation => {
377 f.write_str("declared twice: each related resource is one relation")
378 }
379 Self::UnregisteredRelation => f.write_str(
380 "the related resource is not registered on this panel: declare it with \
381 `Panel::resource`",
382 ),
383 Self::UnregisteredOptionSource { field, source } => write!(
384 f,
385 "field '{field}' takes its options from `{source}`, which this panel does not \
386 register: declare it with `Panel::resource`"
387 ),
388 Self::TraversalLens { steps } => write!(
389 f,
390 "a lens here names one field of its model, not a {steps}-step path"
391 ),
392 Self::UnresolvedLens { model, steps } => write!(
393 f,
394 "lens path {steps:?} resolves to no single column of `{model}`: only embedded \
395 steps (embedded structs, enum variant fields and `#[document]` fields) bind, not \
396 relation hops"
397 ),
398 Self::TenancyColumnNotAField => f.write_str(
399 "the `Tenancy::column` lens names no field of the model: name a field typed \
400 `TenantId`, or use `Tenancy::via` for a tenant reached through a relation",
401 ),
402 Self::TenancyViaOwnColumn => f.write_str(
403 "the `Tenancy::via` lens names one field of the model: use `Tenancy::column` for \
404 the model's own tenant column",
405 ),
406 Self::TenancyViaWithoutBelongsTo => f.write_str(
407 "the `Tenancy::via` lens does not start at a `belongs_to` relation: the tenant \
408 must be the parent's its foreign key names",
409 ),
410 Self::UnguardedForeignKey { relation, keys } => write!(
411 f,
412 "foreign key `{}` is not written through a relationship field over a \
413 tenant-scoped resource of `{relation}`'s model: the write re-checks only such a \
414 field's key, so any other could attach the row to another tenant's parent",
415 keys.join("`, `")
416 ),
417 Self::NoColumns => f.write_str(
418 "no column: declare columns with `Table::new(columns)` or in `ResourceDef::table`",
419 ),
420 Self::ZeroPageSize => f.write_str("`Table::paginate` needs a page size of at least 1"),
421 Self::DuplicateColumn { name } => write!(
422 f,
423 "two columns are named '{name}': each column needs a distinct name"
424 ),
425 Self::DuplicateFilter { name } => write!(
426 f,
427 "two filters are named '{name}': each filter needs a distinct name"
428 ),
429 Self::DuplicateField { name } => write!(
430 f,
431 "two fields are named '{name}': each input needs a distinct field"
432 ),
433 Self::EmptyFormOverride => f.write_str(
434 "`form()` declares no control over a record form with fields: drop the override \
435 to render the derived schema",
436 ),
437 Self::FormWithoutRecordForm => f.write_str(
438 "a form schema is declared, but `type Form` serves no form: name the record form \
439 there",
440 ),
441 Self::CreateWithoutForm => f.write_str(
442 "the policy allows create, but there is no form: name the record form in `type \
443 Form`",
444 ),
445 Self::UnboundControl { control } => write!(
446 f,
447 "control `{control}` is bound by no record-form field, so what the user types \
448 there is never written"
449 ),
450 Self::ControlBoundTwice { control } => write!(
451 f,
452 "control `{control}` is bound by more than one record-form field"
453 ),
454 Self::MissingControl { field, key } => write!(
455 f,
456 "record-form field `{field}` binds key `{key}`, but no control declares it"
457 ),
458 Self::NoBlankAnswer {
459 control,
460 field,
461 in_repeater,
462 } => {
463 let place = if *in_repeater {
464 "sits inside a `Repeater`, so it may be posted empty"
465 } else {
466 "is optional"
467 };
468 write!(
469 f,
470 "control `{control}` {place}, but record-form field `{field}` has no blank \
471 answer: declare `#[form(blank = ..)]`, make the field an `Option`, or make \
472 the control required"
473 )
474 }
475 Self::FormClaimsTenantColumn { field, column } => write!(
476 f,
477 "record-form field `{field}` claims the tenant column `{column}`, which the \
478 framework stamps on create: drop it from the form"
479 ),
480 Self::CompositeKeyChoice { field } => write!(
481 f,
482 "relationship field `{field}` loads a model with a composite primary key, which \
483 no option value can spell"
484 ),
485 Self::UniqueWithoutIndex { field } => write!(
486 f,
487 "field `{field}` is marked unique, but no unique index covers its column: add \
488 `#[unique]` (or `#[unique(..)]`) to the model or drop `.unique()`, which would \
489 otherwise check a rule the database does not enforce"
490 ),
491 Self::CreateColumnsNameTenant { column } => write!(
492 f,
493 "`create_columns` names the tenant column `{column}`, which the framework stamps \
494 on create: drop it there and delegate to `write_create`"
495 ),
496 Self::UnknownCreateColumn { column } => write!(
497 f,
498 "`create_columns` names `{column}`, which is no field of the model"
499 ),
500 Self::UnwrittenColumn { column } => write!(
501 f,
502 "the policy allows create, but nothing writes the non-nullable column `{column}`: \
503 the record form has no such field, toasty fills no `#[default(..)]` for it, and \
504 `create_columns` does not name it, so every create would fail at the driver"
505 ),
506 }
507 }
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512#[non_exhaustive]
513pub enum SegmentFault {
514 Empty,
516 Dot,
518 Char(char),
520}
521
522impl fmt::Display for SegmentFault {
523 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524 match self {
525 Self::Empty => f.write_str("is empty"),
526 Self::Dot => f.write_str("is '.' or '..'"),
527 Self::Char(c) => write!(
528 f,
529 "contains {c:?}: quotes, backslashes, control characters, whitespace, URL \
530 punctuation and the route pattern characters '{{', '}}', '(' and ')' are refused"
531 ),
532 }
533 }
534}
535
536pub(crate) const fn segment_fault(segment: &str) -> Option<SegmentFault> {
540 let bytes = segment.as_bytes();
541 if bytes.is_empty() {
542 return Some(SegmentFault::Empty);
543 }
544 if matches!(bytes, b"." | b"..") {
545 return Some(SegmentFault::Dot);
546 }
547 let mut at = 0;
548 while at < bytes.len() {
549 let (c, width) = decode_char(bytes, at);
550 if c.is_control()
551 || c.is_whitespace()
552 || matches!(
553 c,
554 '"' | '\\' | '/' | '?' | '#' | '%' | '&' | '=' | '{' | '}' | '(' | ')'
555 )
556 {
557 return Some(SegmentFault::Char(c));
558 }
559 at += width;
560 }
561 None
562}
563
564const fn decode_char(bytes: &[u8], at: usize) -> (char, usize) {
566 let lead = bytes[at] as u32;
567 let (code, width) = if lead < 0x80 {
568 (lead, 1)
569 } else if lead < 0xE0 {
570 ((lead & 0x1F) << 6 | tail(bytes, at + 1), 2)
571 } else if lead < 0xF0 {
572 (
573 (lead & 0x0F) << 12 | tail(bytes, at + 1) << 6 | tail(bytes, at + 2),
574 3,
575 )
576 } else {
577 (
578 (lead & 0x07) << 18
579 | tail(bytes, at + 1) << 12
580 | tail(bytes, at + 2) << 6
581 | tail(bytes, at + 3),
582 4,
583 )
584 };
585 match char::from_u32(code) {
586 Some(c) => (c, width),
587 None => panic!("a str holds valid UTF-8"),
588 }
589}
590
591const fn tail(bytes: &[u8], at: usize) -> u32 {
593 bytes[at] as u32 & 0x3F
594}
595
596#[cfg(test)]
597mod tests;