Skip to main content

autumn_plugin_topcoat/
error.rs

1//! Error types.
2
3use std::fmt;
4
5/// A path prefix or mount path is not valid.
6#[non_exhaustive]
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8pub enum PathError {
9    /// The path is empty.
10    #[error("path is empty")]
11    Empty,
12    /// The path is `/`, which matches each path.
13    #[error("`/` matches every path and cannot be a prefix")]
14    Root,
15    /// The path does not start with `/`.
16    #[error("path {path:?} must start with '/'")]
17    #[non_exhaustive]
18    MissingLeadingSlash {
19        /// The refused path.
20        path: String,
21    },
22    /// The path ends with `/`.
23    #[error("path {path:?} must not end with '/'")]
24    #[non_exhaustive]
25    TrailingSlash {
26        /// The refused path.
27        path: String,
28    },
29    /// The path has an empty segment (`//`).
30    #[error("path {path:?} has an empty segment")]
31    #[non_exhaustive]
32    EmptySegment {
33        /// The refused path.
34        path: String,
35    },
36    /// The path has a `.` or `..` segment.
37    #[error("path {path:?} has a '.' or '..' segment")]
38    #[non_exhaustive]
39    DotSegment {
40        /// The refused path.
41        path: String,
42    },
43    /// The path has a character that is not an ASCII letter, a digit, `-`, `.`, `_` or `~`.
44    #[error("path {path:?} contains forbidden character {ch:?}")]
45    #[non_exhaustive]
46    ForbiddenChar {
47        /// The refused path.
48        path: String,
49        /// The first forbidden character.
50        ch: char,
51    },
52    /// The path is in a namespace that Autumn or Topcoat owns.
53    #[error("path {path:?} belongs to {owner}")]
54    #[non_exhaustive]
55    Reserved {
56        /// The refused path.
57        path: String,
58        /// The owner of the namespace.
59        owner: &'static str,
60    },
61}
62
63/// One problem in the plugin configuration.
64#[non_exhaustive]
65#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
66pub enum ConfigError {
67    /// The plugin has no Topcoat router.
68    #[error(
69        "the plugin has no Topcoat router; call TopcoatPlugin::router or TopcoatPlugin::router_with"
70    )]
71    NoRouter,
72    /// The Topcoat router has no routes.
73    #[error("the Topcoat router has no routes")]
74    EmptyRouter,
75    /// The mount path is not valid.
76    #[error("invalid mount path {input:?}: {cause}")]
77    #[non_exhaustive]
78    InvalidMount {
79        /// The input string.
80        input: String,
81        /// The cause.
82        cause: PathError,
83    },
84    /// An excluded prefix is not valid.
85    #[error("invalid excluded prefix {input:?}: {cause}")]
86    #[non_exhaustive]
87    InvalidExclude {
88        /// The input string.
89        input: String,
90        /// The cause.
91        cause: PathError,
92    },
93    /// A runtime prefix is not valid.
94    #[error("invalid runtime prefix {input:?}: {cause}")]
95    #[non_exhaustive]
96    InvalidRuntimePrefix {
97        /// The input string.
98        input: String,
99        /// The cause.
100        cause: PathError,
101    },
102    /// An excluded prefix is not strictly inside the mount path.
103    #[error("excluded prefix {prefix} must be strictly inside the mount {mount}")]
104    #[non_exhaustive]
105    ExcludedOutsideMount {
106        /// The excluded prefix.
107        prefix: String,
108        /// The mount path.
109        mount: String,
110    },
111    /// An excluded prefix overlaps the Topcoat namespace `/_topcoat`.
112    #[error("excluded prefix {prefix} overlaps the Topcoat namespace /_topcoat")]
113    #[non_exhaustive]
114    ExcludedOverlapsInternal {
115        /// The excluded prefix.
116        prefix: String,
117    },
118    /// A runtime prefix is not inside the mount path or under `/_topcoat`.
119    #[error("runtime prefix {prefix} must be inside the mount {mount} or under /_topcoat")]
120    #[non_exhaustive]
121    RuntimePrefixOutsideMount {
122        /// The runtime prefix.
123        prefix: String,
124        /// The mount path.
125        mount: String,
126    },
127    /// A runtime prefix is under an excluded prefix.
128    #[error("runtime prefix {prefix} is under the excluded prefix {excluded}")]
129    #[non_exhaustive]
130    RuntimePrefixExcluded {
131        /// The runtime prefix.
132        prefix: String,
133        /// The excluded prefix.
134        excluded: String,
135    },
136}
137
138/// All problems in the plugin configuration.
139///
140/// [`TopcoatPlugin::validate`](crate::TopcoatPlugin::validate) returns this
141/// value. The list is never empty.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct ConfigErrors(pub(crate) Vec<ConfigError>);
144
145impl ConfigErrors {
146    /// Returns the problems in the order the plugin found them.
147    #[must_use]
148    pub const fn as_slice(&self) -> &[ConfigError] {
149        self.0.as_slice()
150    }
151
152    /// Returns an iterator over the problems.
153    pub fn iter(&self) -> std::slice::Iter<'_, ConfigError> {
154        self.0.iter()
155    }
156
157    /// Returns the number of problems.
158    #[must_use]
159    pub const fn len(&self) -> usize {
160        self.0.len()
161    }
162
163    /// Returns `true` if the list has no problem. A value from the plugin is never empty.
164    #[must_use]
165    pub const fn is_empty(&self) -> bool {
166        self.0.is_empty()
167    }
168}
169
170impl IntoIterator for ConfigErrors {
171    type Item = ConfigError;
172    type IntoIter = std::vec::IntoIter<ConfigError>;
173
174    fn into_iter(self) -> Self::IntoIter {
175        self.0.into_iter()
176    }
177}
178
179impl<'a> IntoIterator for &'a ConfigErrors {
180    type Item = &'a ConfigError;
181    type IntoIter = std::slice::Iter<'a, ConfigError>;
182
183    fn into_iter(self) -> Self::IntoIter {
184        self.0.iter()
185    }
186}
187
188impl fmt::Display for ConfigErrors {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        for (index, error) in self.0.iter().enumerate() {
191            if index > 0 {
192                f.write_str("; ")?;
193            }
194            write!(f, "{error}")?;
195        }
196        Ok(())
197    }
198}
199
200impl std::error::Error for ConfigErrors {}
201
202/// An error that stops the startup of the app.
203#[non_exhaustive]
204#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
205pub enum StartupError {
206    /// The plugin configuration is not valid.
207    #[error("invalid TopcoatPlugin configuration: {0}")]
208    Config(ConfigErrors),
209    /// The router closure returned an error or panicked.
210    #[error("the Topcoat router factory failed: {message}")]
211    #[non_exhaustive]
212    Factory {
213        /// The error text.
214        message: String,
215    },
216    /// The plugin cannot build the Topcoat router, for example because
217    /// `RouterBuilder::build` panicked.
218    #[error("the plugin cannot build the Topcoat router: {message}")]
219    #[non_exhaustive]
220    RouterBuild {
221        /// The panic text.
222        message: String,
223    },
224    /// The Content-Security-Policy blocks Topcoat and the check is `Deny`.
225    #[error("CspDenied: the Content-Security-Policy blocks Topcoat ({findings}); {advice}")]
226    #[non_exhaustive]
227    CspDenied {
228        /// The problems, separated by commas.
229        findings: String,
230        /// What to change.
231        advice: String,
232    },
233}
234
235/// A page cannot get Autumn app data.
236#[non_exhaustive]
237#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
238pub enum AppDataError {
239    /// This plugin does not serve the page.
240    #[error("autumn-plugin-topcoat does not serve this render")]
241    NotMounted,
242    /// `AppState` has no extension of this type.
243    #[error("AppState has no extension of type {type_name}")]
244    #[non_exhaustive]
245    MissingExtension {
246        /// The type name.
247        type_name: &'static str,
248    },
249}
250
251/// A page cannot get Autumn request data.
252#[non_exhaustive]
253#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
254pub enum RequestScopeError {
255    /// No Autumn HTTP request backs this render, for example a WebSocket render.
256    #[error("no Autumn HTTP request backs this render")]
257    Detached,
258    /// The value is not on this request.
259    #[error("{what} is not available on this request")]
260    #[non_exhaustive]
261    Unavailable {
262        /// The name of the value.
263        what: &'static str,
264    },
265}