1use std::fmt;
6
7#[derive(Debug, Clone, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub enum RouteState<T> {
14 NoSubRoute,
18
19 SubRoute(T),
23
24 ParseFailed {
28 remaining_path: String,
30 attempted_patterns: Vec<String>,
32 closest_match: Option<ClosestMatch>,
34 },
35}
36
37#[derive(Debug, Clone, PartialEq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct ClosestMatch {
43 pub pattern: String,
45 pub matched_length: usize,
47 pub failure_reason: String,
49}
50
51#[derive(Debug, Clone, PartialEq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct RouteDebugInfo {
57 pub failed_at_level: usize,
59 pub consumed_path: String,
61 pub remaining_path: String,
63 pub available_routes: Vec<String>,
65 pub suggestion: Option<String>,
67}
68
69#[derive(Debug, Clone, PartialEq)]
73pub enum ParseError {
74 InvalidPath(String),
78
79 MissingParameter(String),
83
84 TypeConversion(String),
88
89 InvalidQuery(String),
93
94 UrlEncoding(String),
98
99 SegmentCountMismatch { expected: usize, actual: usize },
103
104 SegmentMismatch { expected: String, actual: String, position: usize },
108}
109
110impl fmt::Display for ParseError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 ParseError::InvalidPath(msg) => {
114 write!(f, "Invalid path: {msg}")
115 }
116 ParseError::MissingParameter(param) => {
117 write!(f, "Missing required parameter: {param}")
118 }
119 ParseError::TypeConversion(msg) => {
120 write!(f, "Type conversion error: {msg}")
121 }
122 ParseError::InvalidQuery(msg) => {
123 write!(f, "Invalid query parameter: {msg}")
124 }
125 ParseError::UrlEncoding(msg) => {
126 write!(f, "URL encoding error: {msg}")
127 }
128 ParseError::SegmentCountMismatch { expected, actual } => {
129 write!(f, "Path segment count mismatch: expected {expected} segments, found {actual}")
130 }
131 ParseError::SegmentMismatch {
132 expected,
133 actual,
134 position,
135 } => {
136 write!(
137 f,
138 "Path segment mismatch at position {position}: expected '{expected}', found '{actual}'"
139 )
140 }
141 }
142 }
143}
144
145impl std::error::Error for ParseError {
146 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
147 None
149 }
150}
151
152pub type ParseResult<T> = Result<T, ParseError>;
156
157impl ParseError {
159 pub fn invalid_path<S: Into<String>>(msg: S) -> Self {
161 ParseError::InvalidPath(msg.into())
162 }
163
164 pub fn missing_parameter<S: Into<String>>(param: S) -> Self {
166 ParseError::MissingParameter(param.into())
167 }
168
169 pub fn type_conversion<S: Into<String>>(msg: S) -> Self {
171 ParseError::TypeConversion(msg.into())
172 }
173
174 pub fn invalid_query<S: Into<String>>(msg: S) -> Self {
176 ParseError::InvalidQuery(msg.into())
177 }
178
179 pub fn url_encoding<S: Into<String>>(msg: S) -> Self {
181 ParseError::UrlEncoding(msg.into())
182 }
183
184 pub fn segment_count_mismatch(expected: usize, actual: usize) -> Self {
186 ParseError::SegmentCountMismatch { expected, actual }
187 }
188
189 pub fn segment_mismatch<S: Into<String>>(expected: S, actual: S, position: usize) -> Self {
191 ParseError::SegmentMismatch {
192 expected: expected.into(),
193 actual: actual.into(),
194 position,
195 }
196 }
197}
198
199impl<T> RouteState<T> {
201 pub fn no_sub_route() -> Self {
203 RouteState::NoSubRoute
204 }
205
206 pub fn sub_route(sub_router: T) -> Self {
208 RouteState::SubRoute(sub_router)
209 }
210
211 pub fn parse_failed<S: Into<String>>(
213 remaining_path: S,
214 attempted_patterns: Vec<String>,
215 closest_match: Option<ClosestMatch>,
216 ) -> Self {
217 RouteState::ParseFailed {
218 remaining_path: remaining_path.into(),
219 attempted_patterns,
220 closest_match,
221 }
222 }
223
224 pub fn is_no_sub_route(&self) -> bool {
226 matches!(self, RouteState::NoSubRoute)
227 }
228
229 pub fn is_sub_route(&self) -> bool {
231 matches!(self, RouteState::SubRoute(_))
232 }
233
234 pub fn is_parse_failed(&self) -> bool {
236 matches!(self, RouteState::ParseFailed { .. })
237 }
238
239 pub fn as_sub_route(&self) -> Option<&T> {
241 match self {
242 RouteState::SubRoute(sub) => Some(sub),
243 _ => None,
244 }
245 }
246
247 pub fn as_sub_route_mut(&mut self) -> Option<&mut T> {
249 match self {
250 RouteState::SubRoute(sub) => Some(sub),
251 _ => None,
252 }
253 }
254
255 pub fn into_option(self) -> Option<T> {
257 match self {
258 RouteState::SubRoute(sub) => Some(sub),
259 _ => None,
260 }
261 }
262
263 pub fn from_option(option: Option<T>) -> Self {
265 match option {
266 Some(sub) => RouteState::SubRoute(sub),
267 None => RouteState::NoSubRoute,
268 }
269 }
270
271 pub fn map<U, F>(self, f: F) -> RouteState<U>
273 where
274 F: FnOnce(T) -> U,
275 {
276 match self {
277 RouteState::NoSubRoute => RouteState::NoSubRoute,
278 RouteState::SubRoute(sub) => RouteState::SubRoute(f(sub)),
279 RouteState::ParseFailed {
280 remaining_path,
281 attempted_patterns,
282 closest_match,
283 } => RouteState::ParseFailed {
284 remaining_path,
285 attempted_patterns,
286 closest_match,
287 },
288 }
289 }
290
291 pub fn debug_info(&self) -> Option<RouteDebugInfo> {
293 match self {
294 RouteState::ParseFailed {
295 remaining_path,
296 attempted_patterns,
297 closest_match,
298 } => Some(RouteDebugInfo {
299 failed_at_level: 0, consumed_path: String::new(), remaining_path: remaining_path.clone(),
302 available_routes: attempted_patterns.clone(),
303 suggestion: closest_match.as_ref().map(|m| {
304 format!(
305 "Did you mean '{}'? (matched {} characters, failed because: {})",
306 m.pattern, m.matched_length, m.failure_reason
307 )
308 }),
309 }),
310 _ => None,
311 }
312 }
313}
314
315impl ClosestMatch {
317 pub fn new<S1: Into<String>, S2: Into<String>>(pattern: S1, matched_length: usize, failure_reason: S2) -> Self {
319 ClosestMatch {
320 pattern: pattern.into(),
321 matched_length,
322 failure_reason: failure_reason.into(),
323 }
324 }
325}
326
327impl RouteDebugInfo {
329 pub fn new<S1: Into<String>, S2: Into<String>>(
331 failed_at_level: usize,
332 consumed_path: S1,
333 remaining_path: S2,
334 available_routes: Vec<String>,
335 suggestion: Option<String>,
336 ) -> Self {
337 RouteDebugInfo {
338 failed_at_level,
339 consumed_path: consumed_path.into(),
340 remaining_path: remaining_path.into(),
341 available_routes,
342 suggestion,
343 }
344 }
345
346 pub fn to_error_message(&self) -> String {
348 let mut message = format!(
349 "Route parsing failed at level {}: consumed '{}', remaining '{}'",
350 self.failed_at_level, self.consumed_path, self.remaining_path
351 );
352
353 if !self.available_routes.is_empty() {
354 message.push_str(&format!("\nAvailable routes: {}", self.available_routes.join(", ")));
355 }
356
357 if let Some(suggestion) = &self.suggestion {
358 message.push_str(&format!("\nSuggestion: {suggestion}"));
359 }
360
361 message
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_error_display() {
371 let error = ParseError::invalid_path("test path");
372 assert_eq!(error.to_string(), "Invalid path: test path");
373
374 let error = ParseError::missing_parameter("id");
375 assert_eq!(error.to_string(), "Missing required parameter: id");
376
377 let error = ParseError::segment_count_mismatch(3, 2);
378 assert_eq!(error.to_string(), "Path segment count mismatch: expected 3 segments, found 2");
379
380 let error = ParseError::segment_mismatch("user", "admin", 1);
381 assert_eq!(
382 error.to_string(),
383 "Path segment mismatch at position 1: expected 'user', found 'admin'"
384 );
385 }
386
387 #[test]
388 fn test_error_equality() {
389 let error1 = ParseError::invalid_path("test");
390 let error2 = ParseError::invalid_path("test");
391 let error3 = ParseError::invalid_path("other");
392
393 assert_eq!(error1, error2);
394 assert_ne!(error1, error3);
395 }
396}