front_line_router/
router.rs

1use crate::http_version::HttpVersion;
2use crate::method::Method;
3use crate::RouterResult;
4use memchr::memmem;
5
6#[derive(thiserror::Error, PartialEq, Debug)]
7pub enum Error {
8    #[error("the http request line was not valid")]
9    InvalidRequestLine,
10}
11
12/// A trait that encapsulates routing logic for an HTTP request.
13///
14/// Implementers of this trait can specify custom logic to handle parsed method and path segments,
15/// allowing for flexible routing mechanisms. However, for most common use cases, it's recommended
16/// to use the `front_line::FrontLine` proc-macro to auto-generate `Router` instances for enums.
17///
18/// The provided `route` method processes an HTTP request byte slice, parsing its method, path, and
19/// query components. If parsing is successful, it constructs a `RouterResult` that encapsulates
20/// these parsed components.
21pub trait Router<'de>: Sized {
22    /// Handle the parsed method and path segment.
23    ///
24    /// Implementers can provide custom logic to identify routes based on the parsed method and
25    /// path. This allows for the identification of specific application routes, endpoints, etc.
26    ///
27    /// # Arguments
28    ///
29    /// * `method` - The parsed HTTP method (e.g., GET, POST).
30    /// * `remaining_path` - The parsed path segment from the HTTP request.
31    ///
32    /// # Returns
33    ///
34    /// Returns an instance of the implementing type if a route is identified. Otherwise,
35    /// returns `None`.
36    fn handle_parsed(method: Method, remaining_path: &'de str) -> Option<Self>;
37
38    /// Parse and route an HTTP request.
39    ///
40    /// This method provides the core logic to process an HTTP request byte slice, extract its
41    /// components, and identify a route if possible.
42    ///
43    /// # Arguments
44    ///
45    /// * `request` - The raw byte slice of the HTTP request.
46    ///
47    /// # Returns
48    ///
49    /// Returns a `Result` containing the `RouterResult` if routing is successful. If any parsing
50    /// or validation errors occur, returns an `Error`.
51    fn resolve(request: &'de [u8]) -> Result<RouterResult<'de, Self>, Error> {
52        let end = memmem::find(request, b"\r\n\r\n").ok_or(Error::InvalidRequestLine)?;
53        let request_line = &request[..end];
54        let (method, after_method) =
55            Method::parse(request_line).ok_or(Error::InvalidRequestLine)?;
56        let full_path_end = memchr::memchr(b' ', after_method).ok_or(Error::InvalidRequestLine)?;
57        let after_path = &after_method[full_path_end + 1..];
58        let version = HttpVersion::parse(after_path).ok_or(Error::InvalidRequestLine)?;
59        let full_path = &after_method[..full_path_end];
60        let query_start = memchr::memchr(b'?', full_path).unwrap_or(full_path.len());
61        let query_bytes = &full_path[full_path.len().min(query_start + 1)..];
62        let query = std::str::from_utf8(query_bytes).map_err(|_| Error::InvalidRequestLine)?;
63        let path_bytes = &full_path[..query_start];
64        let path = std::str::from_utf8(path_bytes).map_err(|_| Error::InvalidRequestLine)?;
65        let route = Self::handle_parsed(method, path);
66        let head_and_body = &request[end + 4..];
67        let result = RouterResult {
68            route,
69            query,
70            version,
71            head_and_body,
72        };
73        Ok(result)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use rstest::rstest;
81
82    #[derive(PartialEq, Debug)]
83    enum TestRoute {
84        Test,
85    }
86
87    impl<'de> Router<'de> for TestRoute {
88        fn handle_parsed(method: Method, remaining_path: &'de str) -> Option<Self> {
89            match (method, remaining_path) {
90                (Method::Get, "/test") => Some(TestRoute::Test),
91                _ => None,
92            }
93        }
94    }
95
96    #[rstest]
97    #[case(
98        b"GET /test HTTP/1.1\r\n\r\nSome data",
99        Ok(RouterResult {
100            route: Some(TestRoute::Test),
101            query: "",
102            version: HttpVersion::OneOne,
103            head_and_body: b"Some data",
104        })
105    )]
106    #[case(
107        b"GET /test?query=value HTTP/1.1\r\n\r\n",
108        Ok(RouterResult {
109            route: Some(TestRoute::Test),
110            query: "query=value",
111            version: HttpVersion::OneOne,
112            head_and_body: b"",
113        })
114    )]
115    #[case(
116        b"GET /test HTTP/1.0\r\n\r\n",
117        Ok(RouterResult {
118            route: Some(TestRoute::Test),
119            query: "",
120            version: HttpVersion::OneZero,
121            head_and_body: b"",
122        })
123    )]
124    #[case(
125        b"POST /test HTTP/1.1\r\n\r\n",
126        Ok(RouterResult {
127            route: None,
128            query: "",
129            version: HttpVersion::OneOne,
130            head_and_body: b"",
131        })
132    )]
133    #[case(
134        b"GET /invalid HTTP/1.1\r\n\r\n",
135        Ok(RouterResult {
136            route: None,
137            query: "",
138            version: HttpVersion::OneOne,
139            head_and_body: b"",
140        })
141    )]
142    #[case(
143        b"GET /invalid?key=value HTTP/1.1\r\n\r\n",
144        Ok(RouterResult {
145            route: None,
146            query: "key=value",
147            version: HttpVersion::OneOne,
148            head_and_body: b"",
149        })
150    )]
151    #[case(
152        b"GET /invalid?key=value HTTP/1.1\r\n\r\nheader-section",
153        Ok(RouterResult {
154            route: None,
155            query: "key=value",
156            version: HttpVersion::OneOne,
157            head_and_body: b"header-section",
158        })
159    )]
160    #[case(b"GET /test HTT/1.1\r\n\r\n", Err(Error::InvalidRequestLine))]
161    #[case(b"GET /test", Err(Error::InvalidRequestLine))]
162    #[case(b"GET/test HTTP/1.1\r\n\r\n", Err(Error::InvalidRequestLine))]
163    #[case(b"GET /test HTTP/1.1\r\nSome data", Err(Error::InvalidRequestLine))]
164    fn test_route(
165        #[case] input: &[u8],
166        #[case] expected_result: Result<RouterResult<'_, TestRoute>, Error>,
167    ) {
168        let result = TestRoute::resolve(input);
169        assert_eq!(result, expected_result);
170    }
171}