Skip to main content

next_rs_middleware/
request.rs

1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
4pub struct NextRequest {
5    pub method: String,
6    pub url: String,
7    pub path: String,
8    pub query: HashMap<String, String>,
9    pub headers: HashMap<String, String>,
10    pub cookies: HashMap<String, String>,
11    pub geo: Option<GeoData>,
12    pub ip: Option<String>,
13}
14
15#[derive(Debug, Clone)]
16pub struct GeoData {
17    pub country: Option<String>,
18    pub region: Option<String>,
19    pub city: Option<String>,
20}
21
22impl NextRequest {
23    pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
24        let url_str: String = url.into();
25        let (path, query) = Self::parse_url(&url_str);
26
27        Self {
28            method: method.into(),
29            url: url_str,
30            path,
31            query,
32            headers: HashMap::new(),
33            cookies: HashMap::new(),
34            geo: None,
35            ip: None,
36        }
37    }
38
39    fn parse_url(url: &str) -> (String, HashMap<String, String>) {
40        let parts: Vec<&str> = url.splitn(2, '?').collect();
41        let path = parts[0].to_string();
42        let mut query = HashMap::new();
43
44        if parts.len() > 1 {
45            for pair in parts[1].split('&') {
46                let kv: Vec<&str> = pair.splitn(2, '=').collect();
47                if kv.len() == 2 {
48                    query.insert(kv[0].to_string(), kv[1].to_string());
49                }
50            }
51        }
52
53        (path, query)
54    }
55
56    pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
57        self.headers.insert(key.into(), value.into());
58        self
59    }
60
61    pub fn with_cookie(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
62        self.cookies.insert(key.into(), value.into());
63        self
64    }
65
66    pub fn with_ip(mut self, ip: impl Into<String>) -> Self {
67        self.ip = Some(ip.into());
68        self
69    }
70
71    pub fn header(&self, key: &str) -> Option<&String> {
72        self.headers.get(key)
73    }
74
75    pub fn cookie(&self, key: &str) -> Option<&String> {
76        self.cookies.get(key)
77    }
78
79    pub fn query_param(&self, key: &str) -> Option<&String> {
80        self.query.get(key)
81    }
82
83    pub fn next_url(&self) -> NextUrl {
84        NextUrl {
85            pathname: self.path.clone(),
86            search: if self.query.is_empty() {
87                String::new()
88            } else {
89                format!(
90                    "?{}",
91                    self.query
92                        .iter()
93                        .map(|(k, v)| format!("{}={}", k, v))
94                        .collect::<Vec<_>>()
95                        .join("&")
96                )
97            },
98            origin: self
99                .headers
100                .get("host")
101                .map(|h| format!("https://{}", h))
102                .unwrap_or_default(),
103        }
104    }
105}
106
107#[derive(Debug, Clone)]
108pub struct NextUrl {
109    pub pathname: String,
110    pub search: String,
111    pub origin: String,
112}
113
114impl NextUrl {
115    pub fn clone_with_pathname(&self, pathname: impl Into<String>) -> Self {
116        Self {
117            pathname: pathname.into(),
118            search: self.search.clone(),
119            origin: self.origin.clone(),
120        }
121    }
122
123    pub fn href(&self) -> String {
124        format!("{}{}{}", self.origin, self.pathname, self.search)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_request_creation() {
134        let req = NextRequest::new("GET", "/api/users?page=1&limit=10");
135
136        assert_eq!(req.method, "GET");
137        assert_eq!(req.path, "/api/users");
138        assert_eq!(req.query_param("page"), Some(&"1".to_string()));
139        assert_eq!(req.query_param("limit"), Some(&"10".to_string()));
140    }
141
142    #[test]
143    fn test_request_headers_cookies() {
144        let req = NextRequest::new("POST", "/login")
145            .with_header("Content-Type", "application/json")
146            .with_cookie("session", "abc123");
147
148        assert_eq!(
149            req.header("Content-Type"),
150            Some(&"application/json".to_string())
151        );
152        assert_eq!(req.cookie("session"), Some(&"abc123".to_string()));
153    }
154
155    #[test]
156    fn test_next_url() {
157        let req = NextRequest::new("GET", "/blog/post?id=123").with_header("host", "example.com");
158
159        let url = req.next_url();
160        assert_eq!(url.pathname, "/blog/post");
161        assert!(url.search.contains("id=123"));
162        assert_eq!(url.origin, "https://example.com");
163    }
164
165    #[test]
166    fn test_next_url_clone_with_pathname() {
167        let url = NextUrl {
168            pathname: "/old".to_string(),
169            search: "?foo=bar".to_string(),
170            origin: "https://test.com".to_string(),
171        };
172
173        let new_url = url.clone_with_pathname("/new");
174        assert_eq!(new_url.pathname, "/new");
175        assert_eq!(new_url.search, "?foo=bar");
176    }
177}