Skip to main content

hyperchad_router/
lib.rs

1//! Async routing system for `HyperChad` applications with request handling and navigation.
2//!
3//! This crate provides a comprehensive routing solution with support for:
4//!
5//! * Flexible route matching (exact paths, multiple alternatives, prefix matching)
6//! * Async request handling with full HTTP method support
7//! * Request body parsing (JSON, URL-encoded forms, multipart forms with file uploads)
8//! * Client information detection and request metadata
9//! * Programmatic navigation with content delivery channels
10//!
11//! # Features
12//!
13//! * **`serde`** - Enable JSON and form parsing (enabled by default)
14//! * **`form`** - Enable multipart form support (enabled by default)
15//! * **`static-routes`** - Enable static route compilation (enabled by default)
16//! * **`json`** - Enable JSON content support (enabled by default)
17//! * **`format`** - Enable HTML formatting (enabled by default)
18//! * **`syntax-highlighting`** - Enable syntax highlighting support
19//! * **`simd`** - Enable SIMD optimizations
20//!
21//! # Basic Example
22//!
23//! ```rust
24//! use hyperchad_router::Router;
25//!
26//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
27//! // Create a router with some routes
28//! let router = Router::new()
29//!     .with_route("/", |_req| async {
30//!         "<h1>Home</h1>".to_string()
31//!     })
32//!     .with_route("/about", |_req| async {
33//!         "<h1>About</h1>".to_string()
34//!     });
35//!
36//! // Navigate to a route
37//! let content = router.navigate("/").await?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! # Route Patterns
43//!
44//! Routes can match in different ways:
45//!
46//! ```rust
47//! use hyperchad_router::{Router, RoutePath};
48//!
49//! # async fn example() {
50//! let router = Router::new()
51//!     // Exact path match
52//!     .with_route("/home", |_req| async { "Home".to_string() })
53//!     // Multiple alternative paths
54//!     .with_route(&["/api/v1", "/api/v2"][..], |_req| async { "API".to_string() })
55//!     // Prefix match for static files
56//!     .with_route(RoutePath::LiteralPrefix("/static/".to_string()), |_req| async {
57//!         "Static content".to_string()
58//!     });
59//! # }
60//! ```
61//!
62//! # Request Handling
63//!
64//! Handle requests with full access to HTTP method, headers, query parameters, and body:
65//!
66//! ```rust
67//! # #[cfg(all(feature = "serde", feature = "form"))]
68//! # {
69//! use hyperchad_router::Router;
70//! use serde::Deserialize;
71//! use switchy::http::models::Method;
72//!
73//! #[derive(Deserialize)]
74//! struct LoginForm {
75//!     username: String,
76//!     password: String,
77//! }
78//!
79//! # async fn example() {
80//! let router = Router::new()
81//!     .with_route_result("/login", |req| async move {
82//!         if req.method == Method::Post {
83//!             let form: LoginForm = req.parse_form()?;
84//!             Ok::<_, Box<dyn std::error::Error>>(format!("Welcome, {}!", form.username))
85//!         } else {
86//!             Ok("<form>...</form>".to_string())
87//!         }
88//!     });
89//! # }
90//! # }
91//! ```
92
93#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
94#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
95#![allow(clippy::multiple_crate_versions)]
96
97use std::{
98    collections::BTreeMap,
99    pin::Pin,
100    sync::{Arc, RwLock},
101};
102
103use bytes::Bytes;
104use flume::{Receiver, Sender};
105use futures::Future;
106use hyperchad_renderer::Content;
107pub use hyperchad_transformer::{Container, Element};
108use qstring::QString;
109use switchy::http::models::Method;
110use switchy_async::task::JoinHandle;
111use thiserror::Error;
112
113/// Default client information based on the current operating system.
114///
115/// This is lazily initialized on first access and provides OS information
116/// for the default [`ClientInfo`].
117pub static DEFAULT_CLIENT_INFO: std::sync::LazyLock<std::sync::Arc<ClientInfo>> =
118    std::sync::LazyLock::new(|| {
119        let os_name = os_info::get().os_type().to_string();
120        std::sync::Arc::new(ClientInfo {
121            os: ClientOs { name: os_name },
122        })
123    });
124
125/// A route handler function type.
126///
127/// Route handlers take a [`RouteRequest`] and return a future that resolves to
128/// an optional [`Content`] or an error.
129pub type RouteFunc = Arc<
130    Box<
131        dyn (Fn(
132                RouteRequest,
133            ) -> Pin<
134                Box<
135                    dyn Future<Output = Result<Option<Content>, Box<dyn std::error::Error>>> + Send,
136                >,
137            >) + Send
138            + Sync,
139    >,
140>;
141
142/// Errors that can occur when parsing request data.
143#[cfg(feature = "serde")]
144#[derive(Debug, Error)]
145pub enum ParseError {
146    /// JSON deserialization error.
147    #[error(transparent)]
148    SerdeJson(#[from] serde_json::Error),
149    /// URL-encoded form deserialization error.
150    #[error(transparent)]
151    SerdeUrlEncoded(#[from] serde_urlencoded::de::Error),
152    /// Request body is missing.
153    #[error("Missing body")]
154    MissingBody,
155    /// Content-Type header is invalid or unsupported.
156    #[error("Invalid Content-Type")]
157    InvalidContentType,
158    /// I/O error during form parsing.
159    #[cfg(feature = "form")]
160    #[error(transparent)]
161    IO(#[from] std::io::Error),
162    /// Multipart form is missing boundary parameter.
163    #[cfg(feature = "form")]
164    #[error("Missing boundary")]
165    MissingBoundary,
166    /// UTF-8 parsing error.
167    #[cfg(feature = "form")]
168    #[error(transparent)]
169    ParseUtf8(#[from] std::string::FromUtf8Error),
170    /// Multipart parsing error.
171    #[cfg(feature = "form")]
172    #[error(transparent)]
173    Multipart(#[from] mime_multipart::Error),
174    /// Content-Disposition header is invalid.
175    #[cfg(feature = "form")]
176    #[error("Invalid Content‑Disposition")]
177    InvalidContentDisposition,
178    /// Custom deserialization error.
179    #[cfg(feature = "form")]
180    #[error("Custom deserialization error: {0}")]
181    CustomDeserialize(String),
182}
183
184#[cfg(feature = "form")]
185/// Serde deserializers for multipart form data.
186///
187/// This module provides custom deserializers for converting form field data into
188/// strongly-typed Rust structures.
189mod form_deserializer {
190    use serde::de::{self, Deserializer, IntoDeserializer, MapAccess, Visitor};
191    use std::collections::BTreeMap;
192    use std::fmt;
193
194    /// Deserializer for multipart form data.
195    ///
196    /// Converts a map of form fields into a Rust structure using serde.
197    pub struct FormDataDeserializer {
198        fields: std::collections::btree_map::IntoIter<String, String>,
199    }
200
201    impl FormDataDeserializer {
202        /// Create a new form data deserializer from a map of field names to values.
203        #[must_use]
204        pub fn new(data: BTreeMap<String, String>) -> Self {
205            Self {
206                fields: data.into_iter(),
207            }
208        }
209    }
210
211    /// Deserializer for individual form field string values.
212    ///
213    /// Attempts to parse string values into appropriate types with automatic
214    /// type inference for booleans, numbers, and strings.
215    pub struct StringValueDeserializer {
216        value: String,
217    }
218
219    impl StringValueDeserializer {
220        /// Create a new string value deserializer.
221        #[must_use]
222        #[allow(clippy::missing_const_for_fn)]
223        pub fn new(value: String) -> Self {
224            Self { value }
225        }
226    }
227
228    /// Deserialization error for form data.
229    #[derive(Debug)]
230    pub struct DeserializeError(String);
231
232    impl fmt::Display for DeserializeError {
233        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234            write!(f, "{}", self.0)
235        }
236    }
237
238    impl std::error::Error for DeserializeError {}
239
240    impl de::Error for DeserializeError {
241        fn custom<T: fmt::Display>(msg: T) -> Self {
242            Self(msg.to_string())
243        }
244    }
245
246    macro_rules! deserialize_primitive {
247        ($method:ident, $visit:ident, $ty:ty) => {
248            fn $method<V>(self, visitor: V) -> Result<V::Value, Self::Error>
249            where
250                V: Visitor<'de>,
251            {
252                self.value
253                    .parse::<$ty>()
254                    .map_err(|e| {
255                        de::Error::custom(format!(
256                            "failed to parse '{}' as {}: {}",
257                            self.value,
258                            stringify!($ty),
259                            e
260                        ))
261                    })
262                    .and_then(|v| visitor.$visit(v))
263            }
264        };
265    }
266
267    impl<'de> Deserializer<'de> for StringValueDeserializer {
268        type Error = DeserializeError;
269
270        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
271        where
272            V: Visitor<'de>,
273        {
274            if self.value.eq_ignore_ascii_case("true") {
275                return visitor.visit_bool(true);
276            }
277            if self.value.eq_ignore_ascii_case("false") {
278                return visitor.visit_bool(false);
279            }
280
281            if self.value.eq_ignore_ascii_case("null") {
282                return visitor.visit_unit();
283            }
284
285            if let Ok(v) = self.value.parse::<u64>() {
286                return visitor.visit_u64(v);
287            }
288
289            if let Ok(v) = self.value.parse::<i64>() {
290                return visitor.visit_i64(v);
291            }
292
293            if let Ok(v) = self.value.parse::<f64>() {
294                return visitor.visit_f64(v);
295            }
296
297            visitor.visit_string(self.value)
298        }
299
300        deserialize_primitive!(deserialize_bool, visit_bool, bool);
301        deserialize_primitive!(deserialize_i8, visit_i8, i8);
302        deserialize_primitive!(deserialize_i16, visit_i16, i16);
303        deserialize_primitive!(deserialize_i32, visit_i32, i32);
304        deserialize_primitive!(deserialize_i64, visit_i64, i64);
305        deserialize_primitive!(deserialize_i128, visit_i128, i128);
306        deserialize_primitive!(deserialize_u8, visit_u8, u8);
307        deserialize_primitive!(deserialize_u16, visit_u16, u16);
308        deserialize_primitive!(deserialize_u32, visit_u32, u32);
309        deserialize_primitive!(deserialize_u64, visit_u64, u64);
310        deserialize_primitive!(deserialize_u128, visit_u128, u128);
311        deserialize_primitive!(deserialize_f32, visit_f32, f32);
312        deserialize_primitive!(deserialize_f64, visit_f64, f64);
313
314        fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
315        where
316            V: Visitor<'de>,
317        {
318            if self.value.len() == 1 {
319                visitor.visit_char(self.value.chars().next().unwrap())
320            } else {
321                Err(de::Error::custom(format!(
322                    "expected single character, got '{}'",
323                    self.value
324                )))
325            }
326        }
327
328        fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
329        where
330            V: Visitor<'de>,
331        {
332            visitor.visit_string(self.value)
333        }
334
335        fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
336        where
337            V: Visitor<'de>,
338        {
339            visitor.visit_string(self.value)
340        }
341
342        fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
343        where
344            V: Visitor<'de>,
345        {
346            visitor.visit_byte_buf(self.value.into_bytes())
347        }
348
349        fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
350        where
351            V: Visitor<'de>,
352        {
353            visitor.visit_byte_buf(self.value.into_bytes())
354        }
355
356        fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
357        where
358            V: Visitor<'de>,
359        {
360            if self.value.is_empty() || self.value.eq_ignore_ascii_case("null") {
361                visitor.visit_none()
362            } else {
363                visitor.visit_some(self)
364            }
365        }
366
367        fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
368        where
369            V: Visitor<'de>,
370        {
371            visitor.visit_unit()
372        }
373
374        fn deserialize_unit_struct<V>(
375            self,
376            _name: &'static str,
377            visitor: V,
378        ) -> Result<V::Value, Self::Error>
379        where
380            V: Visitor<'de>,
381        {
382            visitor.visit_unit()
383        }
384
385        fn deserialize_newtype_struct<V>(
386            self,
387            _name: &'static str,
388            visitor: V,
389        ) -> Result<V::Value, Self::Error>
390        where
391            V: Visitor<'de>,
392        {
393            visitor.visit_newtype_struct(self)
394        }
395
396        fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
397        where
398            V: Visitor<'de>,
399        {
400            self.value.into_deserializer().deserialize_seq(visitor)
401        }
402
403        fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value, Self::Error>
404        where
405            V: Visitor<'de>,
406        {
407            self.deserialize_seq(visitor)
408        }
409
410        fn deserialize_tuple_struct<V>(
411            self,
412            _name: &'static str,
413            _len: usize,
414            visitor: V,
415        ) -> Result<V::Value, Self::Error>
416        where
417            V: Visitor<'de>,
418        {
419            self.deserialize_seq(visitor)
420        }
421
422        fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
423        where
424            V: Visitor<'de>,
425        {
426            self.value.into_deserializer().deserialize_map(visitor)
427        }
428
429        fn deserialize_struct<V>(
430            self,
431            _name: &'static str,
432            _fields: &'static [&'static str],
433            visitor: V,
434        ) -> Result<V::Value, Self::Error>
435        where
436            V: Visitor<'de>,
437        {
438            self.deserialize_map(visitor)
439        }
440
441        fn deserialize_enum<V>(
442            self,
443            _name: &'static str,
444            _variants: &'static [&'static str],
445            visitor: V,
446        ) -> Result<V::Value, Self::Error>
447        where
448            V: Visitor<'de>,
449        {
450            visitor.visit_enum(self.value.into_deserializer())
451        }
452
453        fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
454        where
455            V: Visitor<'de>,
456        {
457            visitor.visit_string(self.value)
458        }
459
460        fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
461        where
462            V: Visitor<'de>,
463        {
464            visitor.visit_unit()
465        }
466    }
467
468    /// Map accessor for iterating over form fields during deserialization.
469    struct FieldsMapAccess {
470        fields: std::collections::btree_map::IntoIter<String, String>,
471        value: Option<String>,
472    }
473
474    impl FieldsMapAccess {
475        /// Create a new map accessor from a form field iterator.
476        #[allow(clippy::missing_const_for_fn)]
477        fn new(fields: std::collections::btree_map::IntoIter<String, String>) -> Self {
478            Self {
479                fields,
480                value: None,
481            }
482        }
483    }
484
485    impl<'de> MapAccess<'de> for FieldsMapAccess {
486        type Error = DeserializeError;
487
488        fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
489        where
490            K: de::DeserializeSeed<'de>,
491        {
492            if let Some((key, value)) = self.fields.next() {
493                self.value = Some(value);
494                seed.deserialize(key.into_deserializer()).map(Some)
495            } else {
496                Ok(None)
497            }
498        }
499
500        fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
501        where
502            V: de::DeserializeSeed<'de>,
503        {
504            let value = self
505                .value
506                .take()
507                .ok_or_else(|| de::Error::custom("value is missing"))?;
508            seed.deserialize(StringValueDeserializer::new(value))
509        }
510    }
511
512    impl<'de> Deserializer<'de> for FormDataDeserializer {
513        type Error = DeserializeError;
514
515        fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
516        where
517            V: Visitor<'de>,
518        {
519            self.deserialize_map(visitor)
520        }
521
522        fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
523        where
524            V: Visitor<'de>,
525        {
526            visitor.visit_map(FieldsMapAccess::new(self.fields))
527        }
528
529        fn deserialize_struct<V>(
530            self,
531            _name: &'static str,
532            _fields: &'static [&'static str],
533            visitor: V,
534        ) -> Result<V::Value, Self::Error>
535        where
536            V: Visitor<'de>,
537        {
538            self.deserialize_map(visitor)
539        }
540
541        fn deserialize_bool<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
542        where
543            V: Visitor<'de>,
544        {
545            Err(de::Error::custom(
546                "cannot deserialize bool from form data map",
547            ))
548        }
549
550        fn deserialize_i8<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
551        where
552            V: Visitor<'de>,
553        {
554            Err(de::Error::custom(
555                "cannot deserialize i8 from form data map",
556            ))
557        }
558
559        fn deserialize_i16<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
560        where
561            V: Visitor<'de>,
562        {
563            Err(de::Error::custom(
564                "cannot deserialize i16 from form data map",
565            ))
566        }
567
568        fn deserialize_i32<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
569        where
570            V: Visitor<'de>,
571        {
572            Err(de::Error::custom(
573                "cannot deserialize i32 from form data map",
574            ))
575        }
576
577        fn deserialize_i64<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
578        where
579            V: Visitor<'de>,
580        {
581            Err(de::Error::custom(
582                "cannot deserialize i64 from form data map",
583            ))
584        }
585
586        fn deserialize_i128<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
587        where
588            V: Visitor<'de>,
589        {
590            Err(de::Error::custom(
591                "cannot deserialize i128 from form data map",
592            ))
593        }
594
595        fn deserialize_u8<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
596        where
597            V: Visitor<'de>,
598        {
599            Err(de::Error::custom(
600                "cannot deserialize u8 from form data map",
601            ))
602        }
603
604        fn deserialize_u16<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
605        where
606            V: Visitor<'de>,
607        {
608            Err(de::Error::custom(
609                "cannot deserialize u16 from form data map",
610            ))
611        }
612
613        fn deserialize_u32<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
614        where
615            V: Visitor<'de>,
616        {
617            Err(de::Error::custom(
618                "cannot deserialize u32 from form data map",
619            ))
620        }
621
622        fn deserialize_u64<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
623        where
624            V: Visitor<'de>,
625        {
626            Err(de::Error::custom(
627                "cannot deserialize u64 from form data map",
628            ))
629        }
630
631        fn deserialize_u128<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
632        where
633            V: Visitor<'de>,
634        {
635            Err(de::Error::custom(
636                "cannot deserialize u128 from form data map",
637            ))
638        }
639
640        fn deserialize_f32<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
641        where
642            V: Visitor<'de>,
643        {
644            Err(de::Error::custom(
645                "cannot deserialize f32 from form data map",
646            ))
647        }
648
649        fn deserialize_f64<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
650        where
651            V: Visitor<'de>,
652        {
653            Err(de::Error::custom(
654                "cannot deserialize f64 from form data map",
655            ))
656        }
657
658        fn deserialize_char<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
659        where
660            V: Visitor<'de>,
661        {
662            Err(de::Error::custom(
663                "cannot deserialize char from form data map",
664            ))
665        }
666
667        fn deserialize_str<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
668        where
669            V: Visitor<'de>,
670        {
671            Err(de::Error::custom(
672                "cannot deserialize str from form data map",
673            ))
674        }
675
676        fn deserialize_string<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
677        where
678            V: Visitor<'de>,
679        {
680            Err(de::Error::custom(
681                "cannot deserialize string from form data map",
682            ))
683        }
684
685        fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
686        where
687            V: Visitor<'de>,
688        {
689            Err(de::Error::custom(
690                "cannot deserialize bytes from form data map",
691            ))
692        }
693
694        fn deserialize_byte_buf<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
695        where
696            V: Visitor<'de>,
697        {
698            Err(de::Error::custom(
699                "cannot deserialize byte_buf from form data map",
700            ))
701        }
702
703        fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
704        where
705            V: Visitor<'de>,
706        {
707            visitor.visit_some(self)
708        }
709
710        fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
711        where
712            V: Visitor<'de>,
713        {
714            visitor.visit_unit()
715        }
716
717        fn deserialize_unit_struct<V>(
718            self,
719            _name: &'static str,
720            visitor: V,
721        ) -> Result<V::Value, Self::Error>
722        where
723            V: Visitor<'de>,
724        {
725            visitor.visit_unit()
726        }
727
728        fn deserialize_newtype_struct<V>(
729            self,
730            _name: &'static str,
731            visitor: V,
732        ) -> Result<V::Value, Self::Error>
733        where
734            V: Visitor<'de>,
735        {
736            visitor.visit_newtype_struct(self)
737        }
738
739        fn deserialize_seq<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
740        where
741            V: Visitor<'de>,
742        {
743            Err(de::Error::custom(
744                "cannot deserialize seq from form data map",
745            ))
746        }
747
748        fn deserialize_tuple<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
749        where
750            V: Visitor<'de>,
751        {
752            Err(de::Error::custom(
753                "cannot deserialize tuple from form data map",
754            ))
755        }
756
757        fn deserialize_tuple_struct<V>(
758            self,
759            _name: &'static str,
760            _len: usize,
761            _visitor: V,
762        ) -> Result<V::Value, Self::Error>
763        where
764            V: Visitor<'de>,
765        {
766            Err(de::Error::custom(
767                "cannot deserialize tuple_struct from form data map",
768            ))
769        }
770
771        fn deserialize_enum<V>(
772            self,
773            _name: &'static str,
774            _variants: &'static [&'static str],
775            _visitor: V,
776        ) -> Result<V::Value, Self::Error>
777        where
778            V: Visitor<'de>,
779        {
780            Err(de::Error::custom(
781                "cannot deserialize enum from form data map",
782            ))
783        }
784
785        fn deserialize_identifier<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
786        where
787            V: Visitor<'de>,
788        {
789            Err(de::Error::custom(
790                "cannot deserialize identifier from form data map",
791            ))
792        }
793
794        fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
795        where
796            V: Visitor<'de>,
797        {
798            visitor.visit_unit()
799        }
800    }
801}
802
803/// Client operating system information.
804#[derive(Debug, Clone, PartialEq, Eq, Default)]
805pub struct ClientOs {
806    /// Operating system name.
807    pub name: String,
808}
809
810/// Information about the client making a request.
811#[derive(Debug, Clone, PartialEq, Eq)]
812pub struct ClientInfo {
813    /// Client operating system.
814    pub os: ClientOs,
815}
816
817impl Default for ClientInfo {
818    fn default() -> Self {
819        DEFAULT_CLIENT_INFO.as_ref().clone()
820    }
821}
822
823/// Metadata about the request context.
824#[derive(Debug, Clone, PartialEq, Eq, Default)]
825pub struct RequestInfo {
826    /// Client making the request.
827    pub client: Arc<ClientInfo>,
828}
829
830/// An HTTP request for routing.
831///
832/// Contains all the information needed to handle an HTTP request including
833/// path, method, query parameters, headers, cookies, and body.
834#[derive(Debug, Clone, PartialEq, Eq)]
835pub struct RouteRequest {
836    /// Request path.
837    pub path: String,
838    /// HTTP method.
839    pub method: Method,
840    /// Query string parameters.
841    pub query: BTreeMap<String, String>,
842    /// HTTP headers.
843    pub headers: BTreeMap<String, String>,
844    /// HTTP cookies.
845    pub cookies: BTreeMap<String, String>,
846    /// Request metadata.
847    pub info: RequestInfo,
848    /// Request body bytes.
849    pub body: Option<Arc<Bytes>>,
850}
851
852impl RouteRequest {
853    /// Create a `RouteRequest` from a path string and request info.
854    ///
855    /// If the path contains a query string (indicated by `?`), it will be
856    /// parsed and stored in the `query` field.
857    #[must_use]
858    pub fn from_path(path: &str, info: RequestInfo) -> Self {
859        let (path, query) = if let Some((path, query)) = path.split_once('?') {
860            (path, query)
861        } else {
862            (path, "")
863        };
864
865        Self {
866            path: path.to_owned(),
867            method: Method::Get,
868            query: QString::from(query).into_iter().collect(),
869            headers: BTreeMap::new(),
870            cookies: BTreeMap::new(),
871            info,
872            body: None,
873        }
874    }
875
876    /// Get the Content-Type header value.
877    #[must_use]
878    pub fn content_type(&self) -> Option<&str> {
879        self.headers.get("content-type").map(String::as_str)
880    }
881
882    /// Parse multipart form data from the request body.
883    ///
884    /// # Errors
885    ///
886    /// * [`ParseError::MissingBody`] - The request body is missing
887    /// * [`ParseError::InvalidContentType`] - The Content-Type header is missing or invalid
888    /// * [`ParseError::Multipart`] - Failed to parse multipart form data
889    /// * [`ParseError::InvalidContentDisposition`] - Content-Disposition header is invalid or missing
890    /// * [`ParseError::ParseUtf8`] - Failed to parse form field as UTF-8
891    /// * [`ParseError::IO`] - I/O error reading uploaded file
892    /// * [`ParseError::CustomDeserialize`] - Failed to deserialize form data into the target type
893    ///
894    /// # Examples
895    ///
896    /// ```rust
897    /// # #[cfg(all(feature = "serde", feature = "form"))]
898    /// # {
899    /// use bytes::Bytes;
900    /// use hyperchad_router::{RequestInfo, RouteRequest};
901    /// use serde::Deserialize;
902    /// use std::sync::Arc;
903    ///
904    /// #[derive(Debug, Deserialize, PartialEq)]
905    /// struct LoginForm {
906    ///     username: String,
907    /// }
908    ///
909    /// let mut req = RouteRequest::from_path("/login", RequestInfo::default());
910    /// req.headers.insert(
911    ///     "content-type".to_string(),
912    ///     "application/x-www-form-urlencoded".to_string(),
913    /// );
914    /// req.body = Some(Arc::new(Bytes::from("username=moosic")));
915    ///
916    /// let form: LoginForm = req.parse_form().expect("form should parse");
917    /// assert_eq!(form, LoginForm { username: "moosic".to_string() });
918    /// # }
919    /// ```
920    #[cfg(feature = "form")]
921    pub fn parse_form<T: serde::de::DeserializeOwned>(&self) -> Result<T, ParseError> {
922        use base64::engine::{Engine as _, general_purpose};
923        use hyper_old::header::{ContentDisposition, ContentType, DispositionParam, Headers};
924        use mime_multipart::{Node, read_multipart_body};
925        use mime_old::Mime;
926        use std::io::{Cursor, Read as _};
927        fn parse_multipart_form_data(
928            body: &[u8],
929            content_type: &str,
930        ) -> Result<BTreeMap<String, String>, ParseError> {
931            {
932                fn process_nodes(
933                    nodes: Vec<Node>,
934                    map: &mut BTreeMap<String, String>,
935                ) -> Result<(), ParseError> {
936                    for node in nodes {
937                        match node {
938                            Node::Part(part) => {
939                                let cd = part
940                                    .headers
941                                    .get::<ContentDisposition>()
942                                    .ok_or(ParseError::InvalidContentDisposition)?;
943                                let field_name = cd
944                                    .parameters
945                                    .iter()
946                                    .find_map(|param| {
947                                        if let DispositionParam::Ext(key, val) = param
948                                            && key.eq_ignore_ascii_case("name")
949                                        {
950                                            return Some(val.clone());
951                                        }
952                                        None
953                                    })
954                                    .ok_or(ParseError::InvalidContentDisposition)?;
955
956                                let text = String::from_utf8(part.body)?;
957                                map.insert(field_name, text);
958                            }
959
960                            Node::File(filepart) => {
961                                let cd = filepart
962                                    .headers
963                                    .get::<ContentDisposition>()
964                                    .ok_or(ParseError::InvalidContentDisposition)?;
965                                let field_name = cd
966                                    .parameters
967                                    .iter()
968                                    .find_map(|param| {
969                                        if let DispositionParam::Ext(key, val) = param
970                                            && key.eq_ignore_ascii_case("name")
971                                        {
972                                            return Some(val.clone());
973                                        }
974                                        None
975                                    })
976                                    .ok_or(ParseError::InvalidContentDisposition)?;
977
978                                let mut f = std::fs::File::open(&filepart.path)?;
979                                let mut data = Vec::new();
980                                f.read_to_end(&mut data)?;
981
982                                let b64 = general_purpose::STANDARD.encode(&data);
983                                map.insert(field_name, b64);
984                            }
985
986                            Node::Multipart((_hdrs, subparts)) => {
987                                process_nodes(subparts, map)?;
988                            }
989                        }
990                    }
991                    Ok(())
992                }
993
994                let mut headers = Headers::new();
995                let mime_type: Mime = content_type
996                    .parse()
997                    .map_err(|()| ParseError::InvalidContentType)?;
998                headers.set(ContentType(mime_type));
999
1000                let mut cursor = Cursor::new(body);
1001                let parts: Vec<Node> = read_multipart_body(&mut cursor, &headers, false)?;
1002
1003                let mut map = BTreeMap::new();
1004                process_nodes(parts, &mut map)?;
1005
1006                Ok(map)
1007            }
1008        }
1009
1010        let content_type = self.content_type().ok_or(ParseError::InvalidContentType)?;
1011
1012        if let Some(body) = &self.body {
1013            if content_type.starts_with("application/x-www-form-urlencoded") {
1014                // Handle URL-encoded forms (standard HTML forms)
1015                serde_urlencoded::from_bytes(body).map_err(ParseError::SerdeUrlEncoded)
1016            } else if content_type.starts_with("multipart/form-data") {
1017                // Handle multipart forms (file uploads)
1018                let data = parse_multipart_form_data(body, content_type)?;
1019                let deserializer = form_deserializer::FormDataDeserializer::new(data);
1020                T::deserialize(deserializer)
1021                    .map_err(|e| ParseError::CustomDeserialize(e.to_string()))
1022            } else {
1023                Err(ParseError::InvalidContentType)
1024            }
1025        } else {
1026            Err(ParseError::MissingBody)
1027        }
1028    }
1029
1030    /// Parse JSON from the request body.
1031    ///
1032    /// # Errors
1033    ///
1034    /// * [`ParseError::MissingBody`] - The request body is missing
1035    /// * [`ParseError::SerdeJson`] - Failed to deserialize JSON data
1036    ///
1037    /// # Examples
1038    ///
1039    /// ```rust
1040    /// # #[cfg(feature = "serde")]
1041    /// # {
1042    /// use bytes::Bytes;
1043    /// use hyperchad_router::{RequestInfo, RouteRequest};
1044    /// use serde::Deserialize;
1045    /// use std::sync::Arc;
1046    ///
1047    /// #[derive(Debug, Deserialize, PartialEq)]
1048    /// struct Payload {
1049    ///     value: String,
1050    /// }
1051    ///
1052    /// let mut req = RouteRequest::from_path("/api", RequestInfo::default());
1053    /// req.body = Some(Arc::new(Bytes::from(r#"{"value":"ok"}"#)));
1054    ///
1055    /// let payload: Payload = req.parse_body().expect("json body should parse");
1056    /// assert_eq!(payload, Payload { value: "ok".to_string() });
1057    /// # }
1058    /// ```
1059    #[cfg(feature = "serde")]
1060    pub fn parse_body<T: serde::de::DeserializeOwned>(&self) -> Result<T, ParseError> {
1061        if let Some(body) = &self.body {
1062            Ok(serde_json::from_slice(body)?)
1063        } else {
1064            Err(ParseError::MissingBody)
1065        }
1066    }
1067}
1068
1069impl From<Navigation> for RouteRequest {
1070    fn from(value: Navigation) -> Self {
1071        Self {
1072            path: value.0,
1073            method: Method::Get,
1074            query: BTreeMap::new(),
1075            headers: BTreeMap::new(),
1076            cookies: BTreeMap::new(),
1077            info: RequestInfo { client: value.1 },
1078            body: None,
1079        }
1080    }
1081}
1082
1083impl From<&Navigation> for RouteRequest {
1084    fn from(value: &Navigation) -> Self {
1085        value.clone().into()
1086    }
1087}
1088
1089/// A route path matcher.
1090///
1091/// Supports exact matches, multiple alternative matches, and prefix matches.
1092#[derive(Debug, Clone, PartialEq, Eq)]
1093pub enum RoutePath {
1094    /// Match a single exact path.
1095    Literal(String),
1096    /// Match any of the specified paths.
1097    Literals(Vec<String>),
1098    /// Match paths that start with the specified prefix.
1099    LiteralPrefix(String),
1100}
1101
1102impl RoutePath {
1103    /// Check if this route path matches the given path.
1104    #[must_use]
1105    pub fn matches(&self, path: &str) -> bool {
1106        match self {
1107            Self::Literal(route_path) => route_path == path,
1108            Self::Literals(route_paths) => route_paths.iter().any(|x| x == path),
1109            Self::LiteralPrefix(route_path) => path.starts_with(route_path),
1110        }
1111    }
1112
1113    /// Strip the matched portion from the path.
1114    ///
1115    /// For exact matches, returns an empty string if the path matches.
1116    /// For prefix matches, returns the remainder after the prefix.
1117    /// Returns `None` if the path doesn't match.
1118    #[must_use]
1119    pub fn strip_match<'a>(&'a self, path: &'a str) -> Option<&'a str> {
1120        const EMPTY: &str = "";
1121
1122        match self {
1123            Self::Literal(..) | Self::Literals(..) => {
1124                if self.matches(path) {
1125                    Some(EMPTY)
1126                } else {
1127                    None
1128                }
1129            }
1130            Self::LiteralPrefix(route_path) => path.strip_prefix(route_path),
1131        }
1132    }
1133}
1134
1135impl From<&str> for RoutePath {
1136    fn from(value: &str) -> Self {
1137        Self::Literal(value.to_owned())
1138    }
1139}
1140
1141impl From<&String> for RoutePath {
1142    fn from(value: &String) -> Self {
1143        Self::Literal(value.to_owned())
1144    }
1145}
1146
1147impl From<&[&str; 1]> for RoutePath {
1148    fn from(value: &[&str; 1]) -> Self {
1149        Self::Literals(value.iter().map(ToString::to_string).collect())
1150    }
1151}
1152
1153impl From<&[&str; 2]> for RoutePath {
1154    fn from(value: &[&str; 2]) -> Self {
1155        Self::Literals(value.iter().map(ToString::to_string).collect())
1156    }
1157}
1158
1159impl From<&[&str; 3]> for RoutePath {
1160    fn from(value: &[&str; 3]) -> Self {
1161        Self::Literals(value.iter().map(ToString::to_string).collect())
1162    }
1163}
1164
1165impl From<&[&str; 4]> for RoutePath {
1166    fn from(value: &[&str; 4]) -> Self {
1167        Self::Literals(value.iter().map(ToString::to_string).collect())
1168    }
1169}
1170
1171impl From<&[&str; 5]> for RoutePath {
1172    fn from(value: &[&str; 5]) -> Self {
1173        Self::Literals(value.iter().map(ToString::to_string).collect())
1174    }
1175}
1176
1177impl From<&[&str; 6]> for RoutePath {
1178    fn from(value: &[&str; 6]) -> Self {
1179        Self::Literals(value.iter().map(ToString::to_string).collect())
1180    }
1181}
1182
1183impl From<&[&str; 7]> for RoutePath {
1184    fn from(value: &[&str; 7]) -> Self {
1185        Self::Literals(value.iter().map(ToString::to_string).collect())
1186    }
1187}
1188
1189impl From<&[&str; 8]> for RoutePath {
1190    fn from(value: &[&str; 8]) -> Self {
1191        Self::Literals(value.iter().map(ToString::to_string).collect())
1192    }
1193}
1194
1195impl From<&[&str; 9]> for RoutePath {
1196    fn from(value: &[&str; 9]) -> Self {
1197        Self::Literals(value.iter().map(ToString::to_string).collect())
1198    }
1199}
1200
1201impl From<&[&str; 10]> for RoutePath {
1202    fn from(value: &[&str; 10]) -> Self {
1203        Self::Literals(value.iter().map(ToString::to_string).collect())
1204    }
1205}
1206
1207impl From<&[&str]> for RoutePath {
1208    fn from(value: &[&str]) -> Self {
1209        Self::Literals(value.iter().map(ToString::to_string).collect())
1210    }
1211}
1212
1213impl From<Vec<&str>> for RoutePath {
1214    fn from(value: Vec<&str>) -> Self {
1215        Self::Literals(value.into_iter().map(ToString::to_string).collect())
1216    }
1217}
1218
1219impl From<String> for RoutePath {
1220    fn from(value: String) -> Self {
1221        Self::Literal(value)
1222    }
1223}
1224
1225impl From<&[String]> for RoutePath {
1226    fn from(value: &[String]) -> Self {
1227        Self::Literals(value.iter().map(ToString::to_string).collect())
1228    }
1229}
1230
1231impl From<&[&String]> for RoutePath {
1232    fn from(value: &[&String]) -> Self {
1233        Self::Literals(value.iter().map(ToString::to_string).collect())
1234    }
1235}
1236
1237impl From<Vec<String>> for RoutePath {
1238    fn from(value: Vec<String>) -> Self {
1239        Self::Literals(value)
1240    }
1241}
1242
1243/// Errors that can occur during navigation.
1244#[derive(Debug, Error)]
1245pub enum NavigateError {
1246    /// The requested path has no registered route handler.
1247    #[error("Invalid path")]
1248    InvalidPath,
1249    /// The route handler returned an error.
1250    #[error("Handler error: {0:?}")]
1251    Handler(Box<dyn std::error::Error + Send + Sync>),
1252    /// Failed to send navigation result through channel.
1253    #[error("Sender error")]
1254    Sender,
1255}
1256
1257/// HTTP router for handling requests and navigation.
1258///
1259/// The router manages route registration and dispatching requests to
1260/// appropriate handlers. Routes can be dynamic or static (with the
1261/// `static-routes` feature).
1262#[derive(Clone)]
1263pub struct Router {
1264    /// Static route handlers (enabled with `static-routes` feature).
1265    #[cfg(feature = "static-routes")]
1266    pub static_routes: Arc<RwLock<Vec<(RoutePath, RouteFunc)>>>,
1267    /// Dynamic route handlers.
1268    pub routes: Arc<RwLock<Vec<(RoutePath, RouteFunc)>>>,
1269    sender: Sender<Content>,
1270    /// Receiver for navigation content.
1271    pub receiver: Receiver<Content>,
1272}
1273
1274impl std::fmt::Debug for Router {
1275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1276        f.debug_struct("Router")
1277            .field("sender", &self.sender)
1278            .field("receiver", &self.receiver)
1279            .finish_non_exhaustive()
1280    }
1281}
1282
1283impl Default for Router {
1284    fn default() -> Self {
1285        Self::new()
1286    }
1287}
1288
1289/// A navigation request consisting of a path and client information.
1290///
1291/// This is a lightweight wrapper type used for programmatic navigation.
1292/// It contains the target path and information about the client making the request.
1293#[derive(Debug, Clone, PartialEq, Eq)]
1294pub struct Navigation(String, Arc<ClientInfo>);
1295
1296impl From<RouteRequest> for Navigation {
1297    fn from(value: RouteRequest) -> Self {
1298        let mut query = String::new();
1299
1300        for (key, value) in &value.query {
1301            if query.is_empty() {
1302                query.push('?');
1303            } else {
1304                query.push('&');
1305            }
1306            query.push_str(key);
1307            query.push('=');
1308            query.push_str(value);
1309        }
1310
1311        Self(format!("{}{query}", value.path), value.info.client)
1312    }
1313}
1314
1315impl From<&str> for RouteRequest {
1316    fn from(value: &str) -> Self {
1317        value.to_string().into()
1318    }
1319}
1320
1321impl From<String> for RouteRequest {
1322    fn from(value: String) -> Self {
1323        Self {
1324            path: value,
1325            method: Method::Get,
1326            query: BTreeMap::new(),
1327            headers: BTreeMap::new(),
1328            cookies: BTreeMap::new(),
1329            info: RequestInfo::default(),
1330            body: None,
1331        }
1332    }
1333}
1334
1335impl From<&String> for RouteRequest {
1336    fn from(value: &String) -> Self {
1337        value.clone().into()
1338    }
1339}
1340
1341impl From<(&str, ClientInfo)> for RouteRequest {
1342    fn from(value: (&str, ClientInfo)) -> Self {
1343        (value.0.to_string(), Arc::new(value.1)).into()
1344    }
1345}
1346
1347impl From<(String, ClientInfo)> for RouteRequest {
1348    fn from(value: (String, ClientInfo)) -> Self {
1349        (value.0, Arc::new(value.1)).into()
1350    }
1351}
1352
1353impl From<(&String, ClientInfo)> for RouteRequest {
1354    fn from(value: (&String, ClientInfo)) -> Self {
1355        (value.0.clone(), Arc::new(value.1)).into()
1356    }
1357}
1358
1359impl From<(&str, Arc<ClientInfo>)> for RouteRequest {
1360    fn from(value: (&str, Arc<ClientInfo>)) -> Self {
1361        (value.0.to_string(), value.1).into()
1362    }
1363}
1364
1365impl From<(String, Arc<ClientInfo>)> for RouteRequest {
1366    fn from(value: (String, Arc<ClientInfo>)) -> Self {
1367        (value.0, RequestInfo { client: value.1 }).into()
1368    }
1369}
1370
1371impl From<(&String, Arc<ClientInfo>)> for RouteRequest {
1372    fn from(value: (&String, Arc<ClientInfo>)) -> Self {
1373        (value.0.clone(), value.1).into()
1374    }
1375}
1376
1377impl From<(&str, RequestInfo)> for RouteRequest {
1378    fn from(value: (&str, RequestInfo)) -> Self {
1379        (value.0.to_string(), value.1).into()
1380    }
1381}
1382
1383impl From<(String, RequestInfo)> for RouteRequest {
1384    fn from(value: (String, RequestInfo)) -> Self {
1385        let (path, query) = if let Some((path, query)) = value.0.split_once('?') {
1386            (path.to_string(), query)
1387        } else {
1388            (value.0, "")
1389        };
1390
1391        Self {
1392            path,
1393            method: Method::Get,
1394            query: QString::from(query).into_iter().collect(),
1395            headers: BTreeMap::new(),
1396            cookies: BTreeMap::new(),
1397            info: value.1,
1398            body: None,
1399        }
1400    }
1401}
1402
1403impl From<(&String, RequestInfo)> for RouteRequest {
1404    fn from(value: (&String, RequestInfo)) -> Self {
1405        (value.0.clone(), value.1).into()
1406    }
1407}
1408
1409impl From<&RouteRequest> for Navigation {
1410    fn from(value: &RouteRequest) -> Self {
1411        value.clone().into()
1412    }
1413}
1414
1415impl From<&str> for Navigation {
1416    fn from(value: &str) -> Self {
1417        Self(value.to_string(), DEFAULT_CLIENT_INFO.clone())
1418    }
1419}
1420
1421impl From<String> for Navigation {
1422    fn from(value: String) -> Self {
1423        Self(value, DEFAULT_CLIENT_INFO.clone())
1424    }
1425}
1426
1427impl From<&String> for Navigation {
1428    fn from(value: &String) -> Self {
1429        Self(value.clone(), DEFAULT_CLIENT_INFO.clone())
1430    }
1431}
1432
1433impl From<(&str, ClientInfo)> for Navigation {
1434    fn from(value: (&str, ClientInfo)) -> Self {
1435        Self(value.0.to_string(), Arc::new(value.1))
1436    }
1437}
1438
1439impl From<(String, ClientInfo)> for Navigation {
1440    fn from(value: (String, ClientInfo)) -> Self {
1441        Self(value.0, Arc::new(value.1))
1442    }
1443}
1444
1445impl From<(&String, ClientInfo)> for Navigation {
1446    fn from(value: (&String, ClientInfo)) -> Self {
1447        Self(value.0.clone(), Arc::new(value.1))
1448    }
1449}
1450
1451impl From<(&str, Arc<ClientInfo>)> for Navigation {
1452    fn from(value: (&str, Arc<ClientInfo>)) -> Self {
1453        Self(value.0.to_string(), value.1)
1454    }
1455}
1456
1457impl From<(String, Arc<ClientInfo>)> for Navigation {
1458    fn from(value: (String, Arc<ClientInfo>)) -> Self {
1459        Self(value.0, value.1)
1460    }
1461}
1462
1463impl From<(&String, Arc<ClientInfo>)> for Navigation {
1464    fn from(value: (&String, Arc<ClientInfo>)) -> Self {
1465        Self(value.0.clone(), value.1)
1466    }
1467}
1468
1469impl From<(&str, RequestInfo)> for Navigation {
1470    fn from(value: (&str, RequestInfo)) -> Self {
1471        Self(value.0.to_string(), value.1.client)
1472    }
1473}
1474
1475impl From<(String, RequestInfo)> for Navigation {
1476    fn from(value: (String, RequestInfo)) -> Self {
1477        Self(value.0, value.1.client)
1478    }
1479}
1480
1481impl From<(&String, RequestInfo)> for Navigation {
1482    fn from(value: (&String, RequestInfo)) -> Self {
1483        Self(value.0.clone(), value.1.client)
1484    }
1485}
1486
1487impl Router {
1488    /// Create a new router with an unbounded channel for navigation events.
1489    #[must_use]
1490    pub fn new() -> Self {
1491        let (tx, rx) = flume::unbounded();
1492
1493        Self {
1494            #[cfg(feature = "static-routes")]
1495            static_routes: Arc::new(RwLock::new(vec![])),
1496            routes: Arc::new(RwLock::new(vec![])),
1497            sender: tx,
1498            receiver: rx,
1499        }
1500    }
1501
1502    /// Register a route with a handler that returns a `Result`.
1503    ///
1504    /// # Panics
1505    ///
1506    /// Will panic if routes `RwLock` is poisoned.
1507    #[must_use]
1508    pub fn with_route_result<
1509        C: TryInto<Content>,
1510        Response: Into<Option<C>>,
1511        F: Future<Output = Result<Response, BoxE>> + Send + 'static,
1512        BoxE: Into<Box<dyn std::error::Error>>,
1513    >(
1514        self,
1515        route: impl Into<RoutePath>,
1516        handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1517    ) -> Self
1518    where
1519        C::Error: Into<Box<dyn std::error::Error>>,
1520    {
1521        self.routes
1522            .write()
1523            .unwrap()
1524            .push((route.into(), gen_route_func_result(handler)));
1525        self
1526    }
1527
1528    /// Register a route with a handler that returns no content on success.
1529    ///
1530    /// # Panics
1531    ///
1532    /// Will panic if routes `RwLock` is poisoned.
1533    #[must_use]
1534    pub fn with_no_content_result<
1535        F: Future<Output = Result<(), BoxE>> + Send + 'static,
1536        BoxE: Into<Box<dyn std::error::Error>>,
1537    >(
1538        self,
1539        route: impl Into<RoutePath>,
1540        handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1541    ) -> Self {
1542        self.with_route_result::<Content, Option<Content>, _, _>(route, move |req: RouteRequest| {
1543            let fut = handler(req);
1544            async move { fut.await.map(|()| None::<Content>).map_err(Into::into) }
1545        })
1546    }
1547
1548    /// Register a static route with a handler that returns a `Result`.
1549    ///
1550    /// Static routes are only compiled in when the `static-routes` feature is enabled.
1551    ///
1552    /// # Panics
1553    ///
1554    /// Will panic if routes `RwLock` is poisoned.
1555    #[allow(clippy::needless_pass_by_value)]
1556    #[must_use]
1557    pub fn with_static_route_result<
1558        C: TryInto<Content>,
1559        Response: Into<Option<C>>,
1560        F: Future<Output = Result<Response, BoxE>> + Send + 'static,
1561        BoxE: Into<Box<dyn std::error::Error>>,
1562    >(
1563        self,
1564        #[allow(unused_variables)] route: impl Into<RoutePath>,
1565        #[allow(unused_variables)] handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1566    ) -> Self
1567    where
1568        C::Error: Into<Box<dyn std::error::Error>>,
1569    {
1570        #[cfg(feature = "static-routes")]
1571        self.static_routes
1572            .write()
1573            .unwrap()
1574            .push((route.into(), gen_route_func_result(handler)));
1575        self
1576    }
1577
1578    /// Register a route with an infallible handler.
1579    ///
1580    /// # Panics
1581    ///
1582    /// Will panic if routes `RwLock` is poisoned.
1583    #[must_use]
1584    pub fn with_route<
1585        C: TryInto<Content>,
1586        Response: Into<Option<C>>,
1587        F: Future<Output = Response> + Send + 'static,
1588    >(
1589        self,
1590        route: impl Into<RoutePath>,
1591        handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1592    ) -> Self
1593    where
1594        C::Error: std::error::Error + 'static,
1595    {
1596        self.routes
1597            .write()
1598            .unwrap()
1599            .push((route.into(), gen_route_func(handler)));
1600        self
1601    }
1602
1603    /// Register a static route with an infallible handler.
1604    ///
1605    /// Static routes are only compiled in when the `static-routes` feature is enabled.
1606    ///
1607    /// # Panics
1608    ///
1609    /// Will panic if routes `RwLock` is poisoned.
1610    #[allow(clippy::needless_pass_by_value)]
1611    #[must_use]
1612    pub fn with_static_route<
1613        C: TryInto<Content>,
1614        Response: Into<Option<C>>,
1615        F: Future<Output = Response> + Send + 'static,
1616    >(
1617        self,
1618        #[allow(unused_variables)] route: impl Into<RoutePath>,
1619        #[allow(unused_variables)] handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1620    ) -> Self
1621    where
1622        C::Error: std::error::Error + 'static,
1623    {
1624        #[cfg(feature = "static-routes")]
1625        self.static_routes
1626            .write()
1627            .unwrap()
1628            .push((route.into(), gen_route_func(handler)));
1629        self
1630    }
1631
1632    /// Get the route handler function for a given path.
1633    ///
1634    /// Searches dynamic routes first, then static routes if enabled.
1635    ///
1636    /// # Panics
1637    ///
1638    /// * Will panic if `routes` `RwLock` is poisoned
1639    /// * Will panic if `static_routes` `RwLock` is poisoned (when `static-routes` feature is enabled)
1640    #[must_use]
1641    pub fn get_route_func(&self, path: &str) -> Option<RouteFunc> {
1642        let dyn_route = self
1643            .routes
1644            .read()
1645            .unwrap()
1646            .iter()
1647            .find(|(route, _)| route.matches(path))
1648            .cloned()
1649            .map(|(_, handler)| handler);
1650
1651        #[cfg(feature = "static-routes")]
1652        if dyn_route.is_none() {
1653            return self
1654                .static_routes
1655                .read()
1656                .unwrap()
1657                .iter()
1658                .find(|(route, _)| route.matches(path))
1659                .cloned()
1660                .map(|(_, handler)| handler);
1661        }
1662
1663        dyn_route
1664    }
1665
1666    /// Navigate to a path and return the resulting content.
1667    ///
1668    /// # Errors
1669    ///
1670    /// * Returns [`NavigateError::InvalidPath`] if no route matches the path
1671    /// * Returns [`NavigateError::Handler`] if the route handler returns an error
1672    ///
1673    /// # Panics
1674    ///
1675    /// Will panic if routes `RwLock` is poisoned.
1676    pub async fn navigate(
1677        &self,
1678        navigation: impl Into<RouteRequest>,
1679    ) -> Result<Option<Content>, NavigateError> {
1680        let req = navigation.into();
1681
1682        log::debug!("navigate: method={} path={}", req.method, req.path);
1683
1684        let handler = self.get_route_func(&req.path);
1685
1686        Ok(if let Some(handler) = handler {
1687            match handler(req).await {
1688                Ok(view) => view,
1689                Err(e) => {
1690                    log::error!("Failed to fetch route view: {e:?}");
1691                    return Err(NavigateError::Handler(Box::new(std::io::Error::other(
1692                        e.to_string(),
1693                    ))));
1694                }
1695            }
1696        } else {
1697            log::warn!("Invalid navigation path={}", req.path);
1698            return Err(NavigateError::InvalidPath);
1699        })
1700    }
1701
1702    /// Navigate to a path and send the resulting content through the channel.
1703    ///
1704    /// # Errors
1705    ///
1706    /// * Returns [`NavigateError::InvalidPath`] if no route matches the path
1707    /// * Returns [`NavigateError::Handler`] if the route handler returns an error
1708    /// * Returns [`NavigateError::Sender`] if sending through the channel fails
1709    ///
1710    /// # Panics
1711    ///
1712    /// Will panic if routes `RwLock` is poisoned.
1713    pub async fn navigate_send(
1714        &self,
1715        navigation: impl Into<RouteRequest>,
1716    ) -> Result<(), NavigateError> {
1717        let req = navigation.into();
1718
1719        log::debug!("navigate_send: method={} path={}", req.method, req.path);
1720
1721        let view = {
1722            let handler = self.get_route_func(&req.path);
1723
1724            if let Some(handler) = handler {
1725                match handler(req).await {
1726                    Ok(view) => view,
1727                    Err(e) => {
1728                        log::error!("Failed to fetch route view: {e:?}");
1729                        return Err(NavigateError::Handler(Box::new(std::io::Error::other(
1730                            e.to_string(),
1731                        ))));
1732                    }
1733                }
1734            } else {
1735                log::warn!("Invalid navigation path={}", req.path);
1736                return Err(NavigateError::InvalidPath);
1737            }
1738        };
1739
1740        if let Some(view) = view {
1741            self.sender.send(view).map_err(|e| {
1742                log::error!("Failed to send: {e:?}");
1743                NavigateError::Sender
1744            })?;
1745        }
1746
1747        Ok(())
1748    }
1749
1750    /// Spawn a task to navigate and send the result.
1751    ///
1752    /// Uses the current async runtime handle.
1753    ///
1754    /// # Errors
1755    ///
1756    /// * The returned `JoinHandle` resolves to an error if navigation fails
1757    ///
1758    /// # Panics
1759    ///
1760    /// * Panics if called outside of an active async runtime
1761    #[must_use]
1762    pub fn navigate_spawn(
1763        &self,
1764        navigation: impl Into<RouteRequest>,
1765    ) -> JoinHandle<Result<(), Box<dyn std::error::Error + Send>>> {
1766        let navigation = navigation.into();
1767
1768        log::debug!("navigate_spawn: navigation={navigation:?}");
1769
1770        self.navigate_spawn_on(&switchy_async::runtime::Handle::current(), navigation)
1771    }
1772
1773    /// Spawn a task to navigate and send the result on a specific runtime handle.
1774    ///
1775    /// # Errors
1776    ///
1777    /// * The returned `JoinHandle` resolves to an error if navigation fails
1778    #[must_use]
1779    pub fn navigate_spawn_on(
1780        &self,
1781        handle: &switchy_async::runtime::Handle,
1782        navigation: impl Into<RouteRequest>,
1783    ) -> JoinHandle<Result<(), Box<dyn std::error::Error + Send>>> {
1784        let navigation = navigation.into();
1785
1786        log::debug!("navigate_spawn_on: navigation={navigation:?}");
1787
1788        let router = self.clone();
1789        handle.spawn_with_name("NativeApp navigate_spawn", async move {
1790            router
1791                .navigate_send(navigation)
1792                .await
1793                .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)
1794        })
1795    }
1796
1797    /// Wait for the next navigation content from the channel.
1798    ///
1799    /// Returns `None` if the channel is closed.
1800    #[must_use]
1801    pub async fn wait_for_navigation(&self) -> Option<Content> {
1802        self.receiver.recv_async().await.ok()
1803    }
1804
1805    /// Check if a dynamic route exists for the given path
1806    ///
1807    /// # Panics
1808    ///
1809    /// Will panic if `routes` `RwLock` is poisoned.
1810    #[must_use]
1811    pub fn has_route(&self, path: &str) -> bool {
1812        self.routes
1813            .read()
1814            .unwrap()
1815            .iter()
1816            .any(|(route, _)| route.matches(path))
1817    }
1818
1819    /// Check if a static route exists for the given path
1820    ///
1821    /// # Panics
1822    ///
1823    /// Will panic if `static_routes` `RwLock` is poisoned.
1824    #[allow(clippy::missing_const_for_fn)]
1825    #[must_use]
1826    pub fn has_static_route(&self, path: &str) -> bool {
1827        #[cfg(feature = "static-routes")]
1828        {
1829            self.static_routes
1830                .read()
1831                .unwrap()
1832                .iter()
1833                .any(|(route, _)| route.matches(path))
1834        }
1835        #[cfg(not(feature = "static-routes"))]
1836        {
1837            let _ = path;
1838            false
1839        }
1840    }
1841
1842    /// Add a route to an existing router (modifies in-place)
1843    ///
1844    /// # Panics
1845    ///
1846    /// Will panic if routes `RwLock` is poisoned.
1847    pub fn add_route_result<
1848        C: TryInto<Content>,
1849        Response: Into<Option<C>>,
1850        F: Future<Output = Result<Response, BoxE>> + Send + 'static,
1851        BoxE: Into<Box<dyn std::error::Error>>,
1852    >(
1853        &self,
1854        route: impl Into<RoutePath>,
1855        handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1856    ) where
1857        C::Error: Into<Box<dyn std::error::Error>>,
1858    {
1859        self.routes
1860            .write()
1861            .unwrap()
1862            .push((route.into(), gen_route_func_result(handler)));
1863    }
1864}
1865
1866/// Generate a route handler function from an infallible async handler.
1867///
1868/// Wraps the handler to convert its response into the expected [`RouteFunc`] signature.
1869fn gen_route_func<
1870    C: TryInto<Content>,
1871    Response: Into<Option<C>>,
1872    F: Future<Output = Response> + Send + 'static,
1873>(
1874    handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1875) -> RouteFunc
1876where
1877    C::Error: std::error::Error + 'static,
1878{
1879    Arc::new(Box::new(move |req| {
1880        Box::pin({
1881            let handler = handler.clone();
1882            async move {
1883                let resp: Result<Option<Content>, Box<dyn std::error::Error>> = handler(req)
1884                    .await
1885                    .into()
1886                    .map(TryInto::try_into)
1887                    .transpose()
1888                    .map_err(|e| {
1889                        log::error!("Failed to handle route: {e:?}");
1890                        Box::new(e) as Box<dyn std::error::Error>
1891                    });
1892                resp
1893            }
1894        })
1895    }))
1896}
1897
1898/// Generate a route handler function from a fallible async handler.
1899///
1900/// Wraps the handler to convert its `Result` response into the expected [`RouteFunc`] signature.
1901fn gen_route_func_result<
1902    C: TryInto<Content>,
1903    Response: Into<Option<C>>,
1904    F: Future<Output = Result<Response, BoxE>> + Send + 'static,
1905    BoxE: Into<Box<dyn std::error::Error>>,
1906>(
1907    handler: impl Fn(RouteRequest) -> F + Send + Sync + Clone + 'static,
1908) -> RouteFunc
1909where
1910    C::Error: Into<Box<dyn std::error::Error>>,
1911{
1912    Arc::new(Box::new(move |req| {
1913        Box::pin({
1914            let handler = handler.clone();
1915            async move {
1916                let resp: Result<Response, Box<dyn std::error::Error>> =
1917                    handler(req).await.map_err(Into::into);
1918                match resp.map(|x| {
1919                    let x: Result<Option<Content>, Box<dyn std::error::Error>> = x
1920                        .into()
1921                        .map(TryInto::try_into)
1922                        .transpose()
1923                        .map_err(Into::into);
1924                    x
1925                }) {
1926                    Ok(x) => match x {
1927                        Ok(x) => Ok(x),
1928                        Err(e) => Err(e),
1929                    },
1930                    Err(e) => Err(e),
1931                }
1932            }
1933        })
1934    }))
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939    #[allow(unused)]
1940    use super::*;
1941
1942    mod route_path_tests {
1943        use super::*;
1944
1945        #[test_log::test]
1946        fn test_literal_exact_match() {
1947            let route = RoutePath::Literal("/home".to_string());
1948            assert!(route.matches("/home"));
1949            assert!(!route.matches("/about"));
1950            assert!(!route.matches("/home/page"));
1951        }
1952
1953        #[test_log::test]
1954        fn test_literals_multiple_matches() {
1955            let route = RoutePath::Literals(vec!["/api/v1".to_string(), "/api/v2".to_string()]);
1956            assert!(route.matches("/api/v1"));
1957            assert!(route.matches("/api/v2"));
1958            assert!(!route.matches("/api/v3"));
1959        }
1960
1961        #[test_log::test]
1962        fn test_literal_prefix_matching() {
1963            let route = RoutePath::LiteralPrefix("/static/".to_string());
1964            assert!(route.matches("/static/"));
1965            assert!(route.matches("/static/css/style.css"));
1966            assert!(route.matches("/static/js/app.js"));
1967            assert!(!route.matches("/api/static"));
1968            assert!(!route.matches("/stati"));
1969        }
1970
1971        #[test_log::test]
1972        fn test_strip_match_literal() {
1973            let route = RoutePath::Literal("/home".to_string());
1974            assert_eq!(route.strip_match("/home"), Some(""));
1975            assert_eq!(route.strip_match("/about"), None);
1976        }
1977
1978        #[test_log::test]
1979        fn test_strip_match_literals() {
1980            let route = RoutePath::Literals(vec!["/api/v1".to_string(), "/api/v2".to_string()]);
1981            assert_eq!(route.strip_match("/api/v1"), Some(""));
1982            assert_eq!(route.strip_match("/api/v2"), Some(""));
1983            assert_eq!(route.strip_match("/api/v3"), None);
1984        }
1985
1986        #[test_log::test]
1987        fn test_strip_match_prefix() {
1988            let route = RoutePath::LiteralPrefix("/static/".to_string());
1989            assert_eq!(route.strip_match("/static/"), Some(""));
1990            assert_eq!(
1991                route.strip_match("/static/css/style.css"),
1992                Some("css/style.css")
1993            );
1994            assert_eq!(route.strip_match("/static/js/app.js"), Some("js/app.js"));
1995            assert_eq!(route.strip_match("/api/static"), None);
1996        }
1997
1998        #[test_log::test]
1999        fn test_from_str() {
2000            let route: RoutePath = "/home".into();
2001            assert_eq!(route, RoutePath::Literal("/home".to_string()));
2002        }
2003
2004        #[test_log::test]
2005        fn test_from_slice() {
2006            let routes: &[&str] = &["/api/v1", "/api/v2"];
2007            let route: RoutePath = routes.into();
2008            assert_eq!(
2009                route,
2010                RoutePath::Literals(vec!["/api/v1".to_string(), "/api/v2".to_string()])
2011            );
2012        }
2013    }
2014
2015    mod route_request_tests {
2016        use super::*;
2017
2018        #[test_log::test]
2019        fn test_from_path_without_query() {
2020            let req = RouteRequest::from_path("/home", RequestInfo::default());
2021            assert_eq!(req.path, "/home");
2022            assert_eq!(req.method, Method::Get);
2023            assert!(req.query.is_empty());
2024        }
2025
2026        #[test_log::test]
2027        fn test_from_path_with_query() {
2028            let req = RouteRequest::from_path("/search?q=rust&lang=en", RequestInfo::default());
2029            assert_eq!(req.path, "/search");
2030            assert_eq!(req.method, Method::Get);
2031            assert_eq!(req.query.get("q"), Some(&"rust".to_string()));
2032            assert_eq!(req.query.get("lang"), Some(&"en".to_string()));
2033        }
2034
2035        #[test_log::test]
2036        fn test_from_path_with_empty_query() {
2037            let req = RouteRequest::from_path("/page?", RequestInfo::default());
2038            assert_eq!(req.path, "/page");
2039            assert!(req.query.is_empty());
2040        }
2041
2042        #[test_log::test]
2043        fn test_content_type_present() {
2044            let mut req = RouteRequest::from_path("/api", RequestInfo::default());
2045            req.headers
2046                .insert("content-type".to_string(), "application/json".to_string());
2047            assert_eq!(req.content_type(), Some("application/json"));
2048        }
2049
2050        #[test_log::test]
2051        fn test_content_type_missing() {
2052            let req = RouteRequest::from_path("/api", RequestInfo::default());
2053            assert_eq!(req.content_type(), None);
2054        }
2055
2056        #[cfg(feature = "serde")]
2057        #[test_log::test]
2058        fn test_parse_body_missing() {
2059            {
2060                use serde::Deserialize;
2061                #[derive(Deserialize)]
2062                struct Data {
2063                    #[allow(dead_code)]
2064                    value: String,
2065                }
2066
2067                let req = RouteRequest::from_path("/api", RequestInfo::default());
2068                let result: Result<Data, _> = req.parse_body();
2069                assert!(matches!(result, Err(ParseError::MissingBody)));
2070            }
2071        }
2072
2073        #[cfg(feature = "serde")]
2074        #[test_log::test]
2075        fn test_parse_body_valid_json() {
2076            {
2077                use serde::Deserialize;
2078                #[derive(Deserialize, PartialEq, Debug)]
2079                struct Data {
2080                    value: String,
2081                    count: u32,
2082                }
2083
2084                let mut req = RouteRequest::from_path("/api", RequestInfo::default());
2085                let json_data = r#"{"value":"test","count":42}"#;
2086                req.body = Some(Arc::new(Bytes::from(json_data)));
2087
2088                let result: Result<Data, _> = req.parse_body();
2089                assert!(result.is_ok());
2090                let data = result.unwrap();
2091                assert_eq!(data.value, "test");
2092                assert_eq!(data.count, 42);
2093            }
2094        }
2095
2096        #[cfg(feature = "serde")]
2097        #[test_log::test]
2098        fn test_parse_body_invalid_json() {
2099            {
2100                use serde::Deserialize;
2101                #[allow(dead_code)]
2102                #[derive(Deserialize)]
2103                struct Data {
2104                    value: String,
2105                }
2106
2107                let mut req = RouteRequest::from_path("/api", RequestInfo::default());
2108                req.body = Some(Arc::new(Bytes::from("not valid json")));
2109
2110                let result: Result<Data, _> = req.parse_body();
2111                assert!(matches!(result, Err(ParseError::SerdeJson(_))));
2112            }
2113        }
2114
2115        #[test_log::test]
2116        fn test_from_string() {
2117            let req: RouteRequest = "/home".to_string().into();
2118            assert_eq!(req.path, "/home");
2119            assert_eq!(req.method, Method::Get);
2120        }
2121
2122        #[test_log::test]
2123        fn test_from_tuple_with_client_info() {
2124            let client_info = ClientInfo {
2125                os: ClientOs {
2126                    name: "TestOS".to_string(),
2127                },
2128            };
2129            let req: RouteRequest = ("/api".to_string(), client_info).into();
2130            assert_eq!(req.path, "/api");
2131            assert_eq!(req.info.client.os.name, "TestOS");
2132        }
2133
2134        #[test_log::test]
2135        fn test_from_tuple_with_request_info() {
2136            let client_info = Arc::new(ClientInfo {
2137                os: ClientOs {
2138                    name: "TestOS".to_string(),
2139                },
2140            });
2141            let req_info = RequestInfo {
2142                client: client_info,
2143            };
2144            let req: RouteRequest = ("/search?q=test".to_string(), req_info).into();
2145            assert_eq!(req.path, "/search");
2146            assert_eq!(req.query.get("q"), Some(&"test".to_string()));
2147            assert_eq!(req.info.client.os.name, "TestOS");
2148        }
2149    }
2150
2151    mod router_tests {
2152        use super::*;
2153
2154        #[test_log::test(switchy_async::test)]
2155        async fn test_router_new() {
2156            let router = Router::new();
2157            assert_eq!(router.routes.read().unwrap().len(), 0);
2158        }
2159
2160        #[test_log::test(switchy_async::test)]
2161        async fn test_with_route_simple() {
2162            let router =
2163                Router::new().with_route("/home", |_req| async { "Home Page".to_string() });
2164
2165            let content = router.navigate("/home").await.unwrap();
2166            assert!(content.is_some());
2167        }
2168
2169        #[test_log::test(switchy_async::test)]
2170        async fn test_with_route_not_found() {
2171            let router = Router::new().with_route("/home", |_req| async { "Home".to_string() });
2172
2173            let result = router.navigate("/about").await;
2174            assert!(matches!(result, Err(NavigateError::InvalidPath)));
2175        }
2176
2177        #[test_log::test(switchy_async::test)]
2178        async fn test_multiple_routes() {
2179            let router = Router::new()
2180                .with_route("/home", |_req| async { "Home".to_string() })
2181                .with_route("/about", |_req| async { "About".to_string() });
2182
2183            let home = router.navigate("/home").await.unwrap();
2184            let about = router.navigate("/about").await.unwrap();
2185            assert!(home.is_some());
2186            assert!(about.is_some());
2187        }
2188
2189        #[test_log::test(switchy_async::test)]
2190        async fn test_route_with_prefix() {
2191            let router = Router::new().with_route(
2192                RoutePath::LiteralPrefix("/static/".to_string()),
2193                |req| async move { format!("Static file: {}", req.path) },
2194            );
2195
2196            let result = router.navigate("/static/css/style.css").await.unwrap();
2197            assert!(result.is_some());
2198        }
2199
2200        #[test_log::test(switchy_async::test)]
2201        async fn test_route_with_multiple_paths() {
2202            let router = Router::new().with_route(&["/api/v1", "/api/v2"][..], |_req| async {
2203                "API".to_string()
2204            });
2205
2206            let v1 = router.navigate("/api/v1").await.unwrap();
2207            let v2 = router.navigate("/api/v2").await.unwrap();
2208            assert!(v1.is_some());
2209            assert!(v2.is_some());
2210
2211            let v3 = router.navigate("/api/v3").await;
2212            assert!(matches!(v3, Err(NavigateError::InvalidPath)));
2213        }
2214
2215        #[test_log::test(switchy_async::test)]
2216        async fn test_with_route_result_success() {
2217            let router = Router::new().with_route_result("/data", |_req| async {
2218                Ok::<_, Box<dyn std::error::Error>>("Success".to_string())
2219            });
2220
2221            let result = router.navigate("/data").await.unwrap();
2222            assert!(result.is_some());
2223        }
2224
2225        #[test_log::test(switchy_async::test)]
2226        async fn test_with_route_result_error() {
2227            let router = Router::new().with_route_result("/error", |_req| async {
2228                Err::<String, _>(
2229                    Box::new(std::io::Error::other("Test error")) as Box<dyn std::error::Error>
2230                )
2231            });
2232
2233            let result = router.navigate("/error").await;
2234            assert!(matches!(result, Err(NavigateError::Handler(_))));
2235        }
2236
2237        #[test_log::test(switchy_async::test)]
2238        async fn test_with_no_content_result() {
2239            let router = Router::new().with_no_content_result("/action", |_req| async {
2240                Ok::<_, Box<dyn std::error::Error>>(())
2241            });
2242
2243            let result = router.navigate("/action").await.unwrap();
2244            assert!(result.is_none());
2245        }
2246
2247        #[test_log::test(switchy_async::test)]
2248        async fn test_has_route() {
2249            let router = Router::new()
2250                .with_route("/home", |_req| async { "Home".to_string() })
2251                .with_route("/about", |_req| async { "About".to_string() });
2252
2253            assert!(router.has_route("/home"));
2254            assert!(router.has_route("/about"));
2255            assert!(!router.has_route("/contact"));
2256        }
2257
2258        #[cfg(feature = "static-routes")]
2259        #[test_log::test(switchy_async::test)]
2260        async fn test_with_static_route() {
2261            let router = Router::new()
2262                .with_static_route("/static", |_req| async { "Static".to_string() })
2263                .with_route("/dynamic", |_req| async { "Dynamic".to_string() });
2264
2265            assert!(!router.has_route("/static"));
2266            assert!(router.has_static_route("/static"));
2267            assert!(router.has_route("/dynamic"));
2268        }
2269
2270        #[cfg(feature = "static-routes")]
2271        #[test_log::test(switchy_async::test)]
2272        async fn test_static_route_navigation() {
2273            let router = Router::new()
2274                .with_static_route("/page", |_req| async { "Static Page".to_string() });
2275
2276            let result = router.navigate("/page").await.unwrap();
2277            assert!(result.is_some());
2278        }
2279
2280        #[test_log::test(switchy_async::test)]
2281        async fn test_get_route_func_present() {
2282            let router = Router::new().with_route("/home", |_req| async { "Home".to_string() });
2283
2284            let route_func = router.get_route_func("/home");
2285            assert!(route_func.is_some());
2286        }
2287
2288        #[test_log::test(switchy_async::test)]
2289        async fn test_get_route_func_missing() {
2290            let router = Router::new().with_route("/home", |_req| async { "Home".to_string() });
2291
2292            let route_func = router.get_route_func("/about");
2293            assert!(route_func.is_none());
2294        }
2295
2296        #[test_log::test(switchy_async::test)]
2297        async fn test_add_route_result() {
2298            let router = Router::new();
2299            router.add_route_result("/dynamic", |_req| async {
2300                Ok::<_, Box<dyn std::error::Error>>("Added".to_string())
2301            });
2302
2303            let result = router.navigate("/dynamic").await.unwrap();
2304            assert!(result.is_some());
2305        }
2306
2307        #[test_log::test(switchy_async::test)]
2308        async fn test_navigate_with_query_params() {
2309            let router = Router::new().with_route("/search", |req| async move {
2310                let query = req.query.get("q").cloned().unwrap_or_default();
2311                format!("Search: {query}")
2312            });
2313
2314            let req = RouteRequest::from_path("/search?q=rust", RequestInfo::default());
2315            let result = router.navigate(req).await.unwrap();
2316            assert!(result.is_some());
2317        }
2318
2319        #[test_log::test(switchy_async::test)]
2320        async fn test_navigate_with_different_methods() {
2321            let router = Router::new().with_route("/api", |req| async move {
2322                match req.method {
2323                    Method::Get => "GET request".to_string(),
2324                    Method::Post => "POST request".to_string(),
2325                    _ => "Other method".to_string(),
2326                }
2327            });
2328
2329            let mut get_req = RouteRequest::from_path("/api", RequestInfo::default());
2330            get_req.method = Method::Get;
2331            let get_result = router.navigate(get_req).await.unwrap();
2332            assert!(get_result.is_some());
2333
2334            let mut post_req = RouteRequest::from_path("/api", RequestInfo::default());
2335            post_req.method = Method::Post;
2336            let post_result = router.navigate(post_req).await.unwrap();
2337            assert!(post_result.is_some());
2338        }
2339
2340        #[test_log::test(switchy_async::test)]
2341        async fn test_router_clone() {
2342            let router = Router::new().with_route("/home", |_req| async { "Home".to_string() });
2343
2344            let cloned = router.clone();
2345            let result = cloned.navigate("/home").await.unwrap();
2346            assert!(result.is_some());
2347        }
2348    }
2349
2350    mod navigation_tests {
2351        use super::*;
2352
2353        #[test_log::test]
2354        fn test_navigation_from_str() {
2355            let nav: Navigation = "/home".into();
2356            assert_eq!(nav.0, "/home");
2357        }
2358
2359        #[test_log::test]
2360        fn test_navigation_from_string() {
2361            let nav: Navigation = "/about".to_string().into();
2362            assert_eq!(nav.0, "/about");
2363        }
2364
2365        #[test_log::test]
2366        fn test_navigation_from_tuple() {
2367            let client = ClientInfo {
2368                os: ClientOs {
2369                    name: "TestOS".to_string(),
2370                },
2371            };
2372            let nav: Navigation = ("/page".to_string(), client).into();
2373            assert_eq!(nav.0, "/page");
2374            assert_eq!(nav.1.os.name, "TestOS");
2375        }
2376
2377        #[test_log::test]
2378        fn test_navigation_from_route_request() {
2379            let mut req = RouteRequest::from_path("/search", RequestInfo::default());
2380            req.query.insert("q".to_string(), "test".to_string());
2381            req.query.insert("limit".to_string(), "10".to_string());
2382
2383            let nav: Navigation = req.into();
2384            assert!(nav.0.starts_with("/search?"));
2385            assert!(nav.0.contains("q=test"));
2386            assert!(nav.0.contains("limit=10"));
2387        }
2388
2389        #[test_log::test]
2390        fn test_route_request_from_navigation() {
2391            let nav = Navigation("/page".to_string(), DEFAULT_CLIENT_INFO.clone());
2392            let req: RouteRequest = nav.into();
2393            assert_eq!(req.path, "/page");
2394            assert_eq!(req.method, Method::Get);
2395        }
2396
2397        #[test_log::test]
2398        fn test_navigation_roundtrip() {
2399            let original = Navigation("/test".to_string(), DEFAULT_CLIENT_INFO.clone());
2400            let req: RouteRequest = original.clone().into();
2401            let nav: Navigation = req.into();
2402            assert_eq!(original.0, nav.0);
2403        }
2404
2405        #[test_log::test]
2406        fn test_navigation_from_route_request_without_query() {
2407            let req = RouteRequest::from_path("/page", RequestInfo::default());
2408            // Query map is empty
2409            assert!(req.query.is_empty());
2410
2411            let nav: Navigation = req.into();
2412            // Path should NOT contain '?' when query is empty
2413            assert_eq!(nav.0, "/page");
2414            assert!(!nav.0.contains('?'));
2415        }
2416
2417        #[test_log::test]
2418        fn test_navigation_from_route_request_with_single_query_param() {
2419            let mut req = RouteRequest::from_path("/search", RequestInfo::default());
2420            req.query.insert("q".to_string(), "rust".to_string());
2421
2422            let nav: Navigation = req.into();
2423            // Should have exactly one '?' and the parameter
2424            assert_eq!(nav.0.matches('?').count(), 1);
2425            assert!(nav.0.contains("q=rust"));
2426        }
2427    }
2428
2429    mod client_info_tests {
2430        use super::*;
2431
2432        #[test_log::test]
2433        fn test_client_os_default() {
2434            let os = ClientOs::default();
2435            assert_eq!(os.name, "");
2436        }
2437
2438        #[test_log::test]
2439        fn test_client_info_default() {
2440            let info = ClientInfo::default();
2441            assert!(!info.os.name.is_empty());
2442        }
2443
2444        #[test_log::test]
2445        fn test_default_client_info_static() {
2446            let info = DEFAULT_CLIENT_INFO.clone();
2447            assert!(!info.os.name.is_empty());
2448        }
2449
2450        #[test_log::test]
2451        fn test_request_info_default() {
2452            let info = RequestInfo::default();
2453            assert!(!info.client.os.name.is_empty());
2454        }
2455    }
2456
2457    #[cfg(feature = "form")]
2458    mod form_deserializer_tests {
2459        use super::*;
2460        use serde::Deserialize;
2461        use std::collections::BTreeMap;
2462
2463        #[test_log::test]
2464        fn test_deserialize_primitives() {
2465            {
2466                #[derive(Debug, Deserialize, PartialEq)]
2467                struct TestForm {
2468                    age: u64,
2469                    score: i32,
2470                    ratio: f64,
2471                    active: bool,
2472                    letter: char,
2473                }
2474
2475                let mut data = BTreeMap::new();
2476                data.insert("age".to_string(), "2445072108".to_string());
2477                data.insert("score".to_string(), "-42".to_string());
2478                data.insert("ratio".to_string(), "5.5".to_string());
2479                data.insert("active".to_string(), "true".to_string());
2480                data.insert("letter".to_string(), "A".to_string());
2481
2482                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2483                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2484
2485                assert!(result.is_ok());
2486                let form = result.unwrap();
2487                assert_eq!(form.age, 2_445_072_108);
2488                assert_eq!(form.score, -42);
2489                assert!((form.ratio - 5.5).abs() < f64::EPSILON);
2490                assert!(form.active);
2491                assert_eq!(form.letter, 'A');
2492            }
2493        }
2494
2495        #[test_log::test]
2496        fn test_deserialize_strings() {
2497            {
2498                #[derive(Debug, Deserialize, PartialEq)]
2499                struct TestForm {
2500                    name: String,
2501                    email: String,
2502                }
2503
2504                let mut data = BTreeMap::new();
2505                data.insert("name".to_string(), "Alice".to_string());
2506                data.insert("email".to_string(), "alice@example.com".to_string());
2507
2508                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2509                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2510
2511                assert!(result.is_ok());
2512                let form = result.unwrap();
2513                assert_eq!(form.name, "Alice");
2514                assert_eq!(form.email, "alice@example.com");
2515            }
2516        }
2517
2518        #[test_log::test]
2519        fn test_deserialize_options() {
2520            {
2521                #[allow(clippy::struct_field_names)]
2522                #[derive(Debug, Deserialize, PartialEq)]
2523                struct TestForm {
2524                    optional_field: Option<u64>,
2525                    empty_field: Option<String>,
2526                    null_field: Option<i32>,
2527                    present_field: Option<String>,
2528                }
2529
2530                let mut data = BTreeMap::new();
2531                data.insert("optional_field".to_string(), "123".to_string());
2532                data.insert("empty_field".to_string(), String::new());
2533                data.insert("null_field".to_string(), "null".to_string());
2534                data.insert("present_field".to_string(), "value".to_string());
2535
2536                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2537                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2538
2539                assert!(result.is_ok());
2540                let form = result.unwrap();
2541                assert_eq!(form.optional_field, Some(123));
2542                assert_eq!(form.empty_field, None);
2543                assert_eq!(form.null_field, None);
2544                assert_eq!(form.present_field, Some("value".to_string()));
2545            }
2546        }
2547
2548        #[test_log::test]
2549        fn test_deserialize_all_integer_types() {
2550            {
2551                #[allow(clippy::struct_field_names)]
2552                #[derive(Debug, Deserialize, PartialEq)]
2553                struct TestForm {
2554                    u8_field: u8,
2555                    u16_field: u16,
2556                    u32_field: u32,
2557                    u64_field: u64,
2558                    i8_field: i8,
2559                    i16_field: i16,
2560                    i32_field: i32,
2561                    i64_field: i64,
2562                }
2563
2564                let mut data = BTreeMap::new();
2565                data.insert("u8_field".to_string(), "255".to_string());
2566                data.insert("u16_field".to_string(), "65535".to_string());
2567                data.insert("u32_field".to_string(), "4294967295".to_string());
2568                data.insert("u64_field".to_string(), "18446744073709551615".to_string());
2569                data.insert("i8_field".to_string(), "-128".to_string());
2570                data.insert("i16_field".to_string(), "-32768".to_string());
2571                data.insert("i32_field".to_string(), "-2147483648".to_string());
2572                data.insert("i64_field".to_string(), "-9223372036854775808".to_string());
2573
2574                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2575                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2576
2577                assert!(result.is_ok());
2578                let form = result.unwrap();
2579                assert_eq!(form.u8_field, 255);
2580                assert_eq!(form.u16_field, 65_535);
2581                assert_eq!(form.u32_field, 4_294_967_295);
2582                assert_eq!(form.u64_field, 18_446_744_073_709_551_615);
2583                assert_eq!(form.i8_field, -128);
2584                assert_eq!(form.i16_field, -32_768);
2585                assert_eq!(form.i32_field, -2_147_483_648);
2586                assert_eq!(form.i64_field, -9_223_372_036_854_775_808);
2587            }
2588        }
2589
2590        #[test_log::test]
2591        fn test_deserialize_booleans() {
2592            {
2593                #[derive(Debug, Deserialize, PartialEq)]
2594                struct TestForm {
2595                    bool1: bool,
2596                    bool2: bool,
2597                }
2598
2599                let mut data = BTreeMap::new();
2600                data.insert("bool1".to_string(), "true".to_string());
2601                data.insert("bool2".to_string(), "false".to_string());
2602
2603                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2604                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2605
2606                assert!(result.is_ok());
2607                let form = result.unwrap();
2608                assert!(form.bool1);
2609                assert!(!form.bool2);
2610            }
2611        }
2612
2613        #[test_log::test]
2614        fn test_deserialize_with_serde_rename() {
2615            {
2616                #[derive(Debug, Deserialize, PartialEq)]
2617                struct TestForm {
2618                    #[serde(rename = "user_age")]
2619                    age: u64,
2620                    #[serde(rename = "user_name")]
2621                    name: String,
2622                }
2623
2624                let mut data = BTreeMap::new();
2625                data.insert("user_age".to_string(), "30".to_string());
2626                data.insert("user_name".to_string(), "Bob".to_string());
2627
2628                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2629                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2630
2631                assert!(result.is_ok());
2632                let form = result.unwrap();
2633                assert_eq!(form.age, 30);
2634                assert_eq!(form.name, "Bob");
2635            }
2636        }
2637
2638        #[test_log::test]
2639        fn test_deserialize_with_default() {
2640            {
2641                #[derive(Debug, Deserialize, PartialEq)]
2642                struct TestForm {
2643                    required: String,
2644                    #[serde(default)]
2645                    optional: String,
2646                }
2647
2648                let mut data = BTreeMap::new();
2649                data.insert("required".to_string(), "value".to_string());
2650
2651                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2652                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2653
2654                assert!(result.is_ok());
2655                let form = result.unwrap();
2656                assert_eq!(form.required, "value");
2657                assert_eq!(form.optional, "");
2658            }
2659        }
2660
2661        #[test_log::test]
2662        fn test_invalid_integer_format() {
2663            {
2664                #[allow(dead_code)]
2665                #[derive(Debug, Deserialize)]
2666                struct TestForm {
2667                    age: u64,
2668                }
2669
2670                let mut data = BTreeMap::new();
2671                data.insert("age".to_string(), "not_a_number".to_string());
2672
2673                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2674                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2675
2676                assert!(result.is_err());
2677            }
2678        }
2679
2680        #[test_log::test]
2681        fn test_integer_overflow() {
2682            {
2683                #[allow(dead_code)]
2684                #[derive(Debug, Deserialize)]
2685                struct TestForm {
2686                    small: u8,
2687                }
2688
2689                let mut data = BTreeMap::new();
2690                data.insert("small".to_string(), "999999".to_string());
2691
2692                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2693                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2694
2695                assert!(result.is_err());
2696            }
2697        }
2698
2699        #[test_log::test]
2700        fn test_original_error_case() {
2701            {
2702                #[derive(Debug, Deserialize, PartialEq)]
2703                struct TestForm {
2704                    id: u64,
2705                }
2706
2707                let mut data = BTreeMap::new();
2708                data.insert("id".to_string(), "2445072108".to_string());
2709
2710                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2711                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
2712
2713                assert!(result.is_ok());
2714                let form = result.unwrap();
2715                assert_eq!(form.id, 2_445_072_108);
2716            }
2717        }
2718
2719        #[test_log::test]
2720        fn test_flatten_with_tagged_enum() {
2721            {
2722                #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
2723                #[serde(tag = "comment_type")]
2724                enum CommentType {
2725                    General,
2726                    Reply { in_reply_to: u64 },
2727                }
2728                #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
2729                struct CreateComment {
2730                    body: String,
2731                    #[serde(flatten)]
2732                    comment_type: CommentType,
2733                }
2734
2735                let mut data = BTreeMap::new();
2736                data.insert("body".to_string(), "test comment".to_string());
2737                data.insert("comment_type".to_string(), "Reply".to_string());
2738                data.insert("in_reply_to".to_string(), "2445072108".to_string());
2739
2740                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2741                let result: Result<CreateComment, _> = CreateComment::deserialize(deserializer);
2742
2743                assert!(result.is_ok());
2744                let comment = result.unwrap();
2745                assert_eq!(comment.body, "test comment");
2746                assert_eq!(
2747                    comment.comment_type,
2748                    CommentType::Reply {
2749                        in_reply_to: 2_445_072_108
2750                    }
2751                );
2752            }
2753        }
2754
2755        #[test_log::test]
2756        fn test_flatten_with_tagged_enum_general_variant() {
2757            {
2758                #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
2759                #[serde(tag = "comment_type")]
2760                enum CommentType {
2761                    General,
2762                    Reply { in_reply_to: u64 },
2763                }
2764                #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
2765                struct CreateComment {
2766                    body: String,
2767                    #[serde(flatten)]
2768                    comment_type: CommentType,
2769                }
2770
2771                let mut data = BTreeMap::new();
2772                data.insert("body".to_string(), "test comment".to_string());
2773                data.insert("comment_type".to_string(), "General".to_string());
2774
2775                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2776                let result: Result<CreateComment, _> = CreateComment::deserialize(deserializer);
2777
2778                assert!(result.is_ok());
2779                let comment = result.unwrap();
2780                assert_eq!(comment.body, "test comment");
2781                assert_eq!(comment.comment_type, CommentType::General);
2782            }
2783        }
2784
2785        #[test_log::test]
2786        fn test_flatten_with_multiple_integer_fields() {
2787            {
2788                #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
2789                #[serde(tag = "action_type")]
2790                enum Action {
2791                    Transfer { from: u64, to: u64, amount: u64 },
2792                }
2793                #[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
2794                struct Request {
2795                    user_id: u64,
2796                    #[serde(flatten)]
2797                    action: Action,
2798                }
2799
2800                let mut data = BTreeMap::new();
2801                data.insert("user_id".to_string(), "100".to_string());
2802                data.insert("action_type".to_string(), "Transfer".to_string());
2803                data.insert("from".to_string(), "200".to_string());
2804                data.insert("to".to_string(), "300".to_string());
2805                data.insert("amount".to_string(), "1000".to_string());
2806
2807                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2808                let result: Result<Request, _> = Request::deserialize(deserializer);
2809
2810                assert!(result.is_ok());
2811                let request = result.unwrap();
2812                assert_eq!(request.user_id, 100);
2813                assert_eq!(
2814                    request.action,
2815                    Action::Transfer {
2816                        from: 200,
2817                        to: 300,
2818                        amount: 1000
2819                    }
2820                );
2821            }
2822        }
2823
2824        #[test_log::test]
2825        fn test_deserialize_any_type_inference() {
2826            {
2827                use serde::de::Deserialize;
2828
2829                let bool_true = form_deserializer::StringValueDeserializer::new("true".to_string());
2830                let result: Result<bool, _> = bool::deserialize(bool_true);
2831                assert!(result.unwrap());
2832
2833                let bool_false =
2834                    form_deserializer::StringValueDeserializer::new("false".to_string());
2835                let result: Result<bool, _> = bool::deserialize(bool_false);
2836                assert!(!result.unwrap());
2837
2838                let number = form_deserializer::StringValueDeserializer::new("42".to_string());
2839                let result: Result<u64, _> = u64::deserialize(number);
2840                assert_eq!(result.unwrap(), 42);
2841
2842                let negative = form_deserializer::StringValueDeserializer::new("-42".to_string());
2843                let result: Result<i64, _> = i64::deserialize(negative);
2844                assert_eq!(result.unwrap(), -42);
2845
2846                let float_val = form_deserializer::StringValueDeserializer::new("2.5".to_string());
2847                let result: Result<f64, _> = f64::deserialize(float_val);
2848                assert!((result.unwrap() - 2.5).abs() < f64::EPSILON);
2849
2850                let string_val =
2851                    form_deserializer::StringValueDeserializer::new("hello".to_string());
2852                let result: Result<String, _> = String::deserialize(string_val);
2853                assert_eq!(result.unwrap(), "hello");
2854            }
2855        }
2856
2857        #[test_log::test]
2858        fn test_flatten_with_mixed_types() {
2859            {
2860                #[derive(Debug, Clone, Deserialize, PartialEq)]
2861                #[serde(tag = "type")]
2862                enum Metadata {
2863                    Numeric { count: u64, ratio: f64 },
2864                    Text { description: String },
2865                }
2866                #[derive(Debug, Clone, Deserialize, PartialEq)]
2867                struct Item {
2868                    name: String,
2869                    active: bool,
2870                    #[serde(flatten)]
2871                    metadata: Metadata,
2872                }
2873
2874                let mut data = BTreeMap::new();
2875                data.insert("name".to_string(), "Test Item".to_string());
2876                data.insert("active".to_string(), "true".to_string());
2877                data.insert("type".to_string(), "Numeric".to_string());
2878                data.insert("count".to_string(), "42".to_string());
2879                data.insert("ratio".to_string(), "0.75".to_string());
2880
2881                let deserializer = form_deserializer::FormDataDeserializer::new(data);
2882                let result: Result<Item, _> = Item::deserialize(deserializer);
2883
2884                assert!(result.is_ok());
2885                let item = result.unwrap();
2886                assert_eq!(item.name, "Test Item");
2887                assert!(item.active);
2888                if let Metadata::Numeric { count, ratio } = item.metadata {
2889                    assert_eq!(count, 42);
2890                    assert!((ratio - 0.75).abs() < f64::EPSILON);
2891                } else {
2892                    panic!("Expected Numeric variant");
2893                }
2894            }
2895        }
2896
2897        #[test_log::test]
2898        fn test_deserialize_invalid_char_multi_character_string() {
2899            let deserializer = form_deserializer::StringValueDeserializer::new("abc".to_string());
2900            let result: Result<char, _> = char::deserialize(deserializer);
2901            assert!(result.is_err());
2902        }
2903
2904        #[test_log::test]
2905        fn test_deserialize_bytes_with_visitor() {
2906            {
2907                use serde::de::Deserializer;
2908                struct ByteBufVisitor;
2909                impl serde::de::Visitor<'_> for ByteBufVisitor {
2910                    type Value = Vec<u8>;
2911
2912                    fn expecting(
2913                        &self,
2914                        formatter: &mut std::fmt::Formatter<'_>,
2915                    ) -> std::fmt::Result {
2916                        write!(formatter, "byte buffer")
2917                    }
2918
2919                    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E> {
2920                        Ok(v)
2921                    }
2922                }
2923
2924                let deserializer =
2925                    form_deserializer::StringValueDeserializer::new("test data".to_string());
2926                let result = deserializer.deserialize_byte_buf(ByteBufVisitor);
2927                assert!(result.is_ok());
2928                assert_eq!(result.unwrap(), b"test data".to_vec());
2929            }
2930        }
2931
2932        #[test_log::test]
2933        fn test_deserialize_unit_from_string() {
2934            let deserializer =
2935                form_deserializer::StringValueDeserializer::new("anything".to_string());
2936            let result: Result<(), _> = serde::Deserialize::deserialize(deserializer);
2937            assert!(result.is_ok());
2938        }
2939
2940        #[test_log::test]
2941        fn test_deserialize_null_value_as_unit() {
2942            {
2943                use serde::de::Deserializer;
2944                struct UnitVisitor;
2945                impl serde::de::Visitor<'_> for UnitVisitor {
2946                    type Value = ();
2947
2948                    fn expecting(
2949                        &self,
2950                        formatter: &mut std::fmt::Formatter<'_>,
2951                    ) -> std::fmt::Result {
2952                        write!(formatter, "null")
2953                    }
2954
2955                    fn visit_unit<E>(self) -> Result<Self::Value, E> {
2956                        Ok(())
2957                    }
2958                }
2959
2960                let deserializer =
2961                    form_deserializer::StringValueDeserializer::new("null".to_string());
2962                let result = deserializer.deserialize_any(UnitVisitor);
2963                assert!(result.is_ok());
2964            }
2965        }
2966
2967        #[test_log::test]
2968        fn test_deserialize_any_case_insensitive_booleans() {
2969            {
2970                use serde::de::Deserializer;
2971                struct BoolVisitor;
2972                impl serde::de::Visitor<'_> for BoolVisitor {
2973                    type Value = bool;
2974
2975                    fn expecting(
2976                        &self,
2977                        formatter: &mut std::fmt::Formatter<'_>,
2978                    ) -> std::fmt::Result {
2979                        write!(formatter, "boolean")
2980                    }
2981
2982                    fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> {
2983                        Ok(v)
2984                    }
2985                }
2986
2987                // The `deserialize_any` method supports case-insensitive booleans
2988                // This is used when struct fields use serde's untagged or default deserialization
2989                let true_upper =
2990                    form_deserializer::StringValueDeserializer::new("TRUE".to_string());
2991                let result = true_upper.deserialize_any(BoolVisitor);
2992                assert!(result.is_ok());
2993                assert!(result.unwrap());
2994
2995                let false_mixed =
2996                    form_deserializer::StringValueDeserializer::new("False".to_string());
2997                let result = false_mixed.deserialize_any(BoolVisitor);
2998                assert!(result.is_ok());
2999                assert!(!result.unwrap());
3000            }
3001        }
3002
3003        #[test_log::test]
3004        fn test_deserialize_f32() {
3005            let deserializer = form_deserializer::StringValueDeserializer::new("1.234".to_string());
3006            let result: Result<f32, _> = f32::deserialize(deserializer);
3007            assert!(result.is_ok());
3008            assert!((result.unwrap() - 1.234).abs() < f32::EPSILON);
3009        }
3010
3011        #[test_log::test]
3012        fn test_deserialize_i128_and_u128() {
3013            let i128_deser = form_deserializer::StringValueDeserializer::new(
3014                "-170141183460469231731687303715884105728".to_string(),
3015            );
3016            let result: Result<i128, _> = i128::deserialize(i128_deser);
3017            assert!(result.is_ok());
3018            assert_eq!(result.unwrap(), i128::MIN);
3019
3020            let u128_deser = form_deserializer::StringValueDeserializer::new(
3021                "340282366920938463463374607431768211455".to_string(),
3022            );
3023            let result: Result<u128, _> = u128::deserialize(u128_deser);
3024            assert!(result.is_ok());
3025            assert_eq!(result.unwrap(), u128::MAX);
3026        }
3027
3028        #[test_log::test]
3029        fn test_form_data_deserializer_error_on_primitive_types() {
3030            {
3031                use serde::de::Deserializer;
3032                struct BoolVisitor;
3033                impl serde::de::Visitor<'_> for BoolVisitor {
3034                    type Value = bool;
3035                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3036                        write!(f, "bool")
3037                    }
3038                }
3039
3040                let data = BTreeMap::new();
3041                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3042                let result = deserializer.deserialize_bool(BoolVisitor);
3043                assert!(result.is_err());
3044            }
3045        }
3046
3047        #[test_log::test]
3048        fn test_form_data_deserializer_error_on_seq() {
3049            {
3050                use serde::de::Deserializer;
3051                struct SeqVisitor;
3052                impl serde::de::Visitor<'_> for SeqVisitor {
3053                    type Value = Vec<String>;
3054                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3055                        write!(f, "seq")
3056                    }
3057                }
3058
3059                let data = BTreeMap::new();
3060                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3061                let result = deserializer.deserialize_seq(SeqVisitor);
3062                assert!(result.is_err());
3063            }
3064        }
3065
3066        #[test_log::test]
3067        fn test_form_data_deserializer_error_on_tuple() {
3068            {
3069                use serde::de::Deserializer;
3070                struct TupleVisitor;
3071                impl serde::de::Visitor<'_> for TupleVisitor {
3072                    type Value = (String, i32);
3073                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3074                        write!(f, "tuple")
3075                    }
3076                }
3077
3078                let data = BTreeMap::new();
3079                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3080                let result = deserializer.deserialize_tuple(2, TupleVisitor);
3081                assert!(result.is_err());
3082            }
3083        }
3084
3085        #[test_log::test]
3086        fn test_form_data_deserializer_error_on_enum() {
3087            {
3088                use serde::de::Deserializer;
3089                struct EnumVisitor;
3090                impl serde::de::Visitor<'_> for EnumVisitor {
3091                    type Value = String;
3092                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3093                        write!(f, "enum")
3094                    }
3095                }
3096
3097                let data = BTreeMap::new();
3098                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3099                let result = deserializer.deserialize_enum("Test", &["A", "B"], EnumVisitor);
3100                assert!(result.is_err());
3101            }
3102        }
3103
3104        #[test_log::test]
3105        fn test_deserialize_newtype_struct() {
3106            {
3107                #[derive(Debug, Deserialize, PartialEq)]
3108                struct UserId(u64);
3109                #[derive(Debug, Deserialize, PartialEq)]
3110                struct TestForm {
3111                    user_id: UserId,
3112                }
3113
3114                let mut data = BTreeMap::new();
3115                data.insert("user_id".to_string(), "12345".to_string());
3116
3117                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3118                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
3119
3120                assert!(result.is_ok());
3121                let form = result.unwrap();
3122                assert_eq!(form.user_id, UserId(12345));
3123            }
3124        }
3125
3126        #[test_log::test]
3127        fn test_deserialize_newtype_string_wrapper() {
3128            {
3129                #[derive(Debug, Deserialize, PartialEq)]
3130                struct Email(String);
3131                #[derive(Debug, Deserialize, PartialEq)]
3132                struct TestForm {
3133                    email: Email,
3134                }
3135
3136                let mut data = BTreeMap::new();
3137                data.insert("email".to_string(), "test@example.com".to_string());
3138
3139                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3140                let result: Result<TestForm, _> = TestForm::deserialize(deserializer);
3141
3142                assert!(result.is_ok());
3143                let form = result.unwrap();
3144                assert_eq!(form.email, Email("test@example.com".to_string()));
3145            }
3146        }
3147
3148        #[test_log::test]
3149        fn test_form_data_deserializer_unit_struct() {
3150            {
3151                use serde::de::Deserializer;
3152                struct UnitVisitor;
3153                impl serde::de::Visitor<'_> for UnitVisitor {
3154                    type Value = ();
3155                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3156                        write!(f, "unit")
3157                    }
3158                    fn visit_unit<E>(self) -> Result<Self::Value, E> {
3159                        Ok(())
3160                    }
3161                }
3162
3163                let data = BTreeMap::new();
3164                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3165                let result = deserializer.deserialize_unit_struct("TestUnit", UnitVisitor);
3166                assert!(result.is_ok());
3167            }
3168        }
3169
3170        #[test_log::test]
3171        fn test_form_data_deserializer_ignored_any() {
3172            {
3173                use serde::de::Deserializer;
3174                struct IgnoredVisitor;
3175                impl serde::de::Visitor<'_> for IgnoredVisitor {
3176                    type Value = ();
3177                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3178                        write!(f, "ignored")
3179                    }
3180                    fn visit_unit<E>(self) -> Result<Self::Value, E> {
3181                        Ok(())
3182                    }
3183                }
3184
3185                let mut data = BTreeMap::new();
3186                data.insert("field1".to_string(), "value1".to_string());
3187                data.insert("field2".to_string(), "value2".to_string());
3188
3189                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3190                let result = deserializer.deserialize_ignored_any(IgnoredVisitor);
3191                assert!(result.is_ok());
3192            }
3193        }
3194
3195        #[test_log::test]
3196        fn test_form_data_deserializer_option_visits_some() {
3197            {
3198                use serde::de::Deserializer;
3199                struct OptionVisitor;
3200
3201                impl<'de> serde::de::Visitor<'de> for OptionVisitor {
3202                    type Value = Option<BTreeMap<String, String>>;
3203                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3204                        write!(f, "option")
3205                    }
3206                    fn visit_some<D>(self, _deserializer: D) -> Result<Self::Value, D::Error>
3207                    where
3208                        D: serde::de::Deserializer<'de>,
3209                    {
3210                        Ok(Some(BTreeMap::new()))
3211                    }
3212                }
3213
3214                let mut data = BTreeMap::new();
3215                data.insert("field".to_string(), "value".to_string());
3216
3217                let deserializer = form_deserializer::FormDataDeserializer::new(data);
3218                let result = deserializer.deserialize_option(OptionVisitor);
3219                assert!(result.is_ok());
3220                assert!(result.unwrap().is_some());
3221            }
3222        }
3223    }
3224
3225    #[cfg(feature = "form")]
3226    mod parse_form_tests {
3227        use super::*;
3228        use bytes::Bytes;
3229        use serde::Deserialize;
3230        use std::sync::Arc;
3231
3232        #[derive(Debug, Deserialize, PartialEq)]
3233        struct TestForm {
3234            name: String,
3235            age: u32,
3236        }
3237
3238        #[test_log::test]
3239        fn test_parse_urlencoded_form() {
3240            let body = b"name=Alice&age=30";
3241            let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3242            req.body = Some(Arc::new(Bytes::from_static(body)));
3243            req.headers.insert(
3244                "content-type".to_string(),
3245                "application/x-www-form-urlencoded".to_string(),
3246            );
3247
3248            let result: Result<TestForm, _> = req.parse_form();
3249
3250            assert!(result.is_ok());
3251            let form = result.unwrap();
3252            assert_eq!(form.name, "Alice");
3253            assert_eq!(form.age, 30);
3254        }
3255
3256        #[test_log::test]
3257        fn test_parse_urlencoded_form_with_special_chars() {
3258            // URL-encoded: name=Hello%20World&age=25
3259            let body = b"name=Hello%20World&age=25";
3260            let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3261            req.body = Some(Arc::new(Bytes::from_static(body)));
3262            req.headers.insert(
3263                "content-type".to_string(),
3264                "application/x-www-form-urlencoded".to_string(),
3265            );
3266
3267            let result: Result<TestForm, _> = req.parse_form();
3268
3269            assert!(result.is_ok());
3270            let form = result.unwrap();
3271            assert_eq!(form.name, "Hello World");
3272            assert_eq!(form.age, 25);
3273        }
3274
3275        #[test_log::test]
3276        fn test_parse_urlencoded_form_with_charset() {
3277            let body = b"name=Bob&age=42";
3278            let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3279            req.body = Some(Arc::new(Bytes::from_static(body)));
3280            req.headers.insert(
3281                "content-type".to_string(),
3282                "application/x-www-form-urlencoded; charset=UTF-8".to_string(),
3283            );
3284
3285            let result: Result<TestForm, _> = req.parse_form();
3286
3287            assert!(result.is_ok());
3288            let form = result.unwrap();
3289            assert_eq!(form.name, "Bob");
3290            assert_eq!(form.age, 42);
3291        }
3292
3293        #[test_log::test]
3294        fn test_parse_form_missing_body() {
3295            let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3296            req.headers.insert(
3297                "content-type".to_string(),
3298                "application/x-www-form-urlencoded".to_string(),
3299            );
3300
3301            let result: Result<TestForm, _> = req.parse_form();
3302
3303            assert!(matches!(result, Err(ParseError::MissingBody)));
3304        }
3305
3306        #[test_log::test]
3307        fn test_parse_form_missing_content_type() {
3308            let body = b"name=Alice&age=30";
3309            let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3310            req.body = Some(Arc::new(Bytes::from_static(body)));
3311
3312            let result: Result<TestForm, _> = req.parse_form();
3313
3314            assert!(matches!(result, Err(ParseError::InvalidContentType)));
3315        }
3316
3317        #[test_log::test]
3318        fn test_parse_form_unsupported_content_type() {
3319            let body = b"name=Alice&age=30";
3320            let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3321            req.body = Some(Arc::new(Bytes::from_static(body)));
3322            req.headers
3323                .insert("content-type".to_string(), "text/plain".to_string());
3324
3325            let result: Result<TestForm, _> = req.parse_form();
3326
3327            assert!(matches!(result, Err(ParseError::InvalidContentType)));
3328        }
3329
3330        #[test_log::test]
3331        fn test_parse_urlencoded_form_with_optional_fields() {
3332            {
3333                #[derive(Debug, Deserialize, PartialEq)]
3334                struct FormWithOptional {
3335                    name: String,
3336                    email: Option<String>,
3337                }
3338
3339                let body = b"name=Alice";
3340                let mut req = RouteRequest::from_path("/submit", RequestInfo::default());
3341                req.body = Some(Arc::new(Bytes::from_static(body)));
3342                req.headers.insert(
3343                    "content-type".to_string(),
3344                    "application/x-www-form-urlencoded".to_string(),
3345                );
3346
3347                let result: Result<FormWithOptional, _> = req.parse_form();
3348
3349                assert!(result.is_ok());
3350                let form = result.unwrap();
3351                assert_eq!(form.name, "Alice");
3352                assert_eq!(form.email, None);
3353            }
3354        }
3355    }
3356
3357    mod channel_tests {
3358        use super::*;
3359
3360        #[test_log::test(switchy_async::test)]
3361        async fn test_navigate_send_success() {
3362            let router =
3363                Router::new().with_route("/home", |_req| async { "Home Content".to_string() });
3364
3365            let result = router.navigate_send("/home").await;
3366            assert!(result.is_ok());
3367
3368            // Content should be available on receiver
3369            let content = router.receiver.try_recv();
3370            assert!(content.is_ok());
3371        }
3372
3373        #[test_log::test(switchy_async::test)]
3374        async fn test_navigate_send_invalid_path() {
3375            let router = Router::new().with_route("/home", |_req| async { "Home".to_string() });
3376
3377            let result = router.navigate_send("/nonexistent").await;
3378            assert!(matches!(result, Err(NavigateError::InvalidPath)));
3379        }
3380
3381        #[test_log::test(switchy_async::test)]
3382        async fn test_navigate_send_handler_error() {
3383            let router = Router::new().with_route_result("/error", |_req| async {
3384                Err::<String, _>(
3385                    Box::new(std::io::Error::other("Handler failed")) as Box<dyn std::error::Error>
3386                )
3387            });
3388
3389            let result = router.navigate_send("/error").await;
3390            assert!(matches!(result, Err(NavigateError::Handler(_))));
3391        }
3392
3393        #[test_log::test(switchy_async::test)]
3394        async fn test_navigate_send_no_content() {
3395            let router = Router::new().with_no_content_result("/action", |_req| async {
3396                Ok::<_, Box<dyn std::error::Error>>(())
3397            });
3398
3399            let result = router.navigate_send("/action").await;
3400            assert!(result.is_ok());
3401
3402            // No content should be sent since handler returned None
3403            let content = router.receiver.try_recv();
3404            assert!(content.is_err());
3405        }
3406
3407        #[test_log::test(switchy_async::test)]
3408        async fn test_wait_for_navigation() {
3409            let router =
3410                Router::new().with_route("/page", |_req| async { "Page Content".to_string() });
3411
3412            // Send content in background
3413            let router_clone = router.clone();
3414            switchy_async::task::spawn(async move {
3415                router_clone.navigate_send("/page").await.unwrap();
3416            });
3417
3418            // Wait for the content
3419            let content = router.wait_for_navigation().await;
3420            assert!(content.is_some());
3421        }
3422
3423        #[test_log::test(switchy_async::test)]
3424        async fn test_navigate_spawn_success() {
3425            let router = Router::new().with_route("/spawn", |_req| async { "Spawned".to_string() });
3426
3427            let handle = router.navigate_spawn("/spawn");
3428            let result = handle.await.unwrap();
3429            assert!(result.is_ok());
3430
3431            // Content should be on receiver
3432            let content = router.receiver.try_recv();
3433            assert!(content.is_ok());
3434        }
3435
3436        #[test_log::test(switchy_async::test)]
3437        async fn test_navigate_spawn_invalid_path() {
3438            let router = Router::new();
3439
3440            let handle = router.navigate_spawn("/nonexistent");
3441            let result = handle.await.unwrap();
3442            assert!(result.is_err());
3443        }
3444
3445        #[test_log::test(switchy_async::test)]
3446        async fn test_navigate_spawn_on_with_handle() {
3447            let router =
3448                Router::new().with_route("/handle", |_req| async { "Handle Test".to_string() });
3449
3450            let handle = switchy_async::runtime::Handle::current();
3451            let join_handle = router.navigate_spawn_on(&handle, "/handle");
3452            let result = join_handle.await.unwrap();
3453            assert!(result.is_ok());
3454        }
3455    }
3456
3457    mod static_route_tests {
3458        use super::*;
3459
3460        #[cfg(feature = "static-routes")]
3461        #[test_log::test(switchy_async::test)]
3462        async fn test_static_route_result_success() {
3463            let router = Router::new().with_static_route_result("/static", |_req| async {
3464                Ok::<_, Box<dyn std::error::Error>>("Static Content".to_string())
3465            });
3466
3467            let result = router.navigate("/static").await.unwrap();
3468            assert!(result.is_some());
3469        }
3470
3471        #[cfg(feature = "static-routes")]
3472        #[test_log::test(switchy_async::test)]
3473        async fn test_static_route_result_error() {
3474            let router = Router::new().with_static_route_result("/static_err", |_req| async {
3475                Err::<String, _>(
3476                    Box::new(std::io::Error::other("Static error")) as Box<dyn std::error::Error>
3477                )
3478            });
3479
3480            let result = router.navigate("/static_err").await;
3481            assert!(matches!(result, Err(NavigateError::Handler(_))));
3482        }
3483
3484        #[cfg(feature = "static-routes")]
3485        #[test_log::test(switchy_async::test)]
3486        async fn test_dynamic_route_takes_precedence() {
3487            let router = Router::new()
3488                .with_static_route("/page", |_req| async { "Static".to_string() })
3489                .with_route("/page", |_req| async { "Dynamic".to_string() });
3490
3491            // Dynamic route should match since it was added after static
3492            // and get_route_func checks dynamic routes first
3493            let func = router.get_route_func("/page");
3494            assert!(func.is_some());
3495        }
3496
3497        #[cfg(feature = "static-routes")]
3498        #[test_log::test]
3499        fn test_has_static_route_with_prefix() {
3500            let router = Router::new().with_static_route(
3501                RoutePath::LiteralPrefix("/assets/".to_string()),
3502                |req| async move { format!("Asset: {}", req.path) },
3503            );
3504
3505            assert!(router.has_static_route("/assets/css/style.css"));
3506            assert!(router.has_static_route("/assets/js/app.js"));
3507            assert!(!router.has_static_route("/api/data"));
3508        }
3509
3510        #[cfg(not(feature = "static-routes"))]
3511        #[test_log::test]
3512        fn test_has_static_route_returns_false_without_feature() {
3513            let router = Router::new();
3514            assert!(!router.has_static_route("/any/path"));
3515        }
3516    }
3517
3518    mod route_path_edge_cases {
3519        use super::*;
3520
3521        #[test_log::test]
3522        fn test_empty_literals_vec() {
3523            let route = RoutePath::Literals(vec![]);
3524            assert!(!route.matches("/any"));
3525            assert!(route.strip_match("/any").is_none());
3526        }
3527
3528        #[test_log::test]
3529        fn test_literal_prefix_empty_string() {
3530            let route = RoutePath::LiteralPrefix(String::new());
3531            // Empty prefix matches everything
3532            assert!(route.matches("/any/path"));
3533            assert!(route.matches(""));
3534            assert_eq!(route.strip_match("/test"), Some("/test"));
3535        }
3536
3537        #[test_log::test]
3538        fn test_from_vec_string() {
3539            let paths: Vec<String> = vec!["/a".to_string(), "/b".to_string()];
3540            let route: RoutePath = paths.into();
3541            assert!(route.matches("/a"));
3542            assert!(route.matches("/b"));
3543        }
3544
3545        #[test_log::test]
3546        fn test_from_slice_ref_string() {
3547            let path_a = "/x".to_string();
3548            let path_b = "/y".to_string();
3549            let paths: &[&String] = &[&path_a, &path_b];
3550            let route: RoutePath = paths.into();
3551            assert!(route.matches("/x"));
3552            assert!(route.matches("/y"));
3553        }
3554
3555        #[test_log::test]
3556        fn test_from_array_size_variants() {
3557            // Test the array-specific From impls
3558            let arr3: &[&str; 3] = &["/a", "/b", "/c"];
3559            let route3: RoutePath = arr3.into();
3560            assert!(route3.matches("/a"));
3561            assert!(route3.matches("/c"));
3562
3563            let arr5: &[&str; 5] = &["/1", "/2", "/3", "/4", "/5"];
3564            let route5: RoutePath = arr5.into();
3565            assert!(route5.matches("/1"));
3566            assert!(route5.matches("/5"));
3567        }
3568    }
3569
3570    mod request_conversion_tests {
3571        use super::*;
3572
3573        #[test_log::test]
3574        fn test_route_request_from_ref_str() {
3575            let req: RouteRequest = "/path".into();
3576            assert_eq!(req.path, "/path");
3577        }
3578
3579        #[test_log::test]
3580        fn test_route_request_from_ref_string() {
3581            let path = "/path".to_string();
3582            let req: RouteRequest = (&path).into();
3583            assert_eq!(req.path, "/path");
3584        }
3585
3586        #[test_log::test]
3587        fn test_route_request_from_ref_str_with_arc_client_info() {
3588            let client = Arc::new(ClientInfo {
3589                os: ClientOs {
3590                    name: "TestOS".to_string(),
3591                },
3592            });
3593            let req: RouteRequest = ("/test", client).into();
3594            assert_eq!(req.path, "/test");
3595            assert_eq!(req.info.client.os.name, "TestOS");
3596        }
3597
3598        #[test_log::test]
3599        fn test_route_request_from_ref_string_with_arc_client_info() {
3600            let path = "/test".to_string();
3601            let client = Arc::new(ClientInfo {
3602                os: ClientOs {
3603                    name: "TestOS".to_string(),
3604                },
3605            });
3606            let req: RouteRequest = (&path, client).into();
3607            assert_eq!(req.path, "/test");
3608        }
3609
3610        #[test_log::test]
3611        fn test_route_request_from_ref_string_with_client_info() {
3612            let path = "/test".to_string();
3613            let client = ClientInfo {
3614                os: ClientOs {
3615                    name: "TestOS".to_string(),
3616                },
3617            };
3618            let req: RouteRequest = (&path, client).into();
3619            assert_eq!(req.path, "/test");
3620        }
3621
3622        #[test_log::test]
3623        fn test_route_request_from_ref_str_with_request_info() {
3624            let info = RequestInfo::default();
3625            let req: RouteRequest = ("/test", info).into();
3626            assert_eq!(req.path, "/test");
3627        }
3628
3629        #[test_log::test]
3630        fn test_route_request_from_ref_string_with_request_info() {
3631            let path = "/test".to_string();
3632            let info = RequestInfo::default();
3633            let req: RouteRequest = (&path, info).into();
3634            assert_eq!(req.path, "/test");
3635        }
3636
3637        #[test_log::test]
3638        fn test_navigation_from_ref_route_request() {
3639            let req = RouteRequest::from_path("/test", RequestInfo::default());
3640            let nav: Navigation = (&req).into();
3641            assert_eq!(nav.0, "/test");
3642        }
3643
3644        #[test_log::test]
3645        fn test_navigation_from_ref_string() {
3646            let path = "/test".to_string();
3647            let nav: Navigation = (&path).into();
3648            assert_eq!(nav.0, "/test");
3649        }
3650
3651        #[test_log::test]
3652        fn test_navigation_from_ref_str_with_client_info() {
3653            let client = ClientInfo {
3654                os: ClientOs {
3655                    name: "TestOS".to_string(),
3656                },
3657            };
3658            let nav: Navigation = ("/test", client).into();
3659            assert_eq!(nav.0, "/test");
3660            assert_eq!(nav.1.os.name, "TestOS");
3661        }
3662
3663        #[test_log::test]
3664        fn test_navigation_from_ref_string_with_client_info() {
3665            let path = "/test".to_string();
3666            let client = ClientInfo {
3667                os: ClientOs {
3668                    name: "TestOS".to_string(),
3669                },
3670            };
3671            let nav: Navigation = (&path, client).into();
3672            assert_eq!(nav.0, "/test");
3673        }
3674
3675        #[test_log::test]
3676        fn test_navigation_from_ref_str_with_arc_client_info() {
3677            let client = Arc::new(ClientInfo {
3678                os: ClientOs {
3679                    name: "TestOS".to_string(),
3680                },
3681            });
3682            let nav: Navigation = ("/test", client).into();
3683            assert_eq!(nav.0, "/test");
3684        }
3685
3686        #[test_log::test]
3687        fn test_navigation_from_ref_string_with_arc_client_info() {
3688            let path = "/test".to_string();
3689            let client = Arc::new(ClientInfo {
3690                os: ClientOs {
3691                    name: "TestOS".to_string(),
3692                },
3693            });
3694            let nav: Navigation = (&path, client).into();
3695            assert_eq!(nav.0, "/test");
3696        }
3697
3698        #[test_log::test]
3699        fn test_navigation_from_ref_str_with_request_info() {
3700            let info = RequestInfo::default();
3701            let nav: Navigation = ("/test", info).into();
3702            assert_eq!(nav.0, "/test");
3703        }
3704
3705        #[test_log::test]
3706        fn test_navigation_from_ref_string_with_request_info() {
3707            let path = "/test".to_string();
3708            let info = RequestInfo::default();
3709            let nav: Navigation = (&path, info).into();
3710            assert_eq!(nav.0, "/test");
3711        }
3712    }
3713}