1#[derive(Debug, Clone)]
2pub struct Url {
3 pub scheme: String,
4 pub host: String,
5 pub port: u16,
6 pub path: String,
7 pub query: String,
8 pub full: String,
9}
10
11impl Url {
12 pub fn parse(input: &str) -> Result<Self, String> {
13 let input = input.trim();
14
15 let (scheme, rest) = match input.find("://") {
16 Some(pos) => {
17 let s = &input[..pos].to_lowercase();
18 let rest = &input[pos + 3..];
19 (s.to_string(), rest)
20 }
21 None => return Err(format!("missing scheme in url: {input}")),
22 };
23
24 if scheme != "http" && scheme != "https" && scheme != "ws" && scheme != "wss" {
25 return Err(format!("unsupported scheme: {scheme}"));
26 }
27
28 let default_port: u16 = match scheme.as_str() {
29 "https" | "wss" => 443,
30 _ => 80,
31 };
32 let is_tls = scheme == "https" || scheme == "wss";
33
34 let (host_str, path_and_query) = match rest.find('/') {
35 Some(pos) => (&rest[..pos], &rest[pos..]),
36 None => (rest, "/"),
37 };
38
39 let (host, port) = if let Some(colon_pos) = host_str.rfind(':') {
40 let h = &host_str[..colon_pos];
41 let p_str = &host_str[colon_pos + 1..];
42 let p: u16 = p_str
43 .parse()
44 .map_err(|_| format!("invalid port in url: {input}"))?;
45 (h.to_string(), p)
46 } else {
47 (host_str.to_string(), default_port)
48 };
49
50 if host.is_empty() {
51 return Err(format!("empty host in url: {input}"));
52 }
53
54 let (path, query) = if let Some(qmark) = path_and_query.find('?') {
55 let p = if qmark == 0 {
56 ""
57 } else {
58 &path_and_query[..qmark]
59 };
60 let p = if p.is_empty() { "/" } else { p };
61 (p.to_string(), path_and_query[qmark..].to_string())
62 } else {
63 let p = if path_and_query.is_empty() {
64 "/"
65 } else {
66 path_and_query
67 };
68 (p.to_string(), String::new())
69 };
70
71 Ok(Url {
72 scheme: if is_tls {
73 "https".into()
74 } else {
75 "http".into()
76 },
77 host,
78 port,
79 path,
80 query,
81 full: input.to_string(),
82 })
83 }
84
85 pub fn origin(&self) -> String {
86 if self.port == 80 || self.port == 443 {
87 format!("{}://{}", self.scheme, self.host)
88 } else {
89 format!("{}://{}:{}", self.scheme, self.host, self.port)
90 }
91 }
92
93 pub fn is_tls(&self) -> bool {
94 self.port == 443 || self.scheme == "https"
95 }
96
97 pub fn request_target(&self) -> String {
98 if self.query.is_empty() {
99 self.path.clone()
100 } else {
101 format!("{}?{}", self.path, self.query)
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_parse_https() {
112 let u = Url::parse("https://example.com/path?a=1").unwrap();
113 assert_eq!(u.scheme, "https");
114 assert_eq!(u.host, "example.com");
115 assert_eq!(u.port, 443);
116 assert_eq!(u.path, "/path");
117 assert_eq!(u.query, "?a=1");
118 assert!(u.is_tls());
119 }
120
121 #[test]
122 fn test_parse_http_with_port() {
123 let u = Url::parse("http://localhost:8080/api").unwrap();
124 assert_eq!(u.scheme, "http");
125 assert_eq!(u.host, "localhost");
126 assert_eq!(u.port, 8080);
127 assert_eq!(u.path, "/api");
128 assert!(!u.is_tls());
129 }
130
131 #[test]
132 fn test_parse_ws() {
133 let u = Url::parse("ws://echo.example.com/chat").unwrap();
134 assert_eq!(u.host, "echo.example.com");
135 assert_eq!(u.port, 80);
136 }
137
138 #[test]
139 fn test_parse_root() {
140 let u = Url::parse("http://example.com").unwrap();
141 assert_eq!(u.path, "/");
142 assert_eq!(u.query, "");
143 }
144
145 #[test]
146 fn test_parse_no_scheme() {
147 assert!(Url::parse("example.com/path").is_err());
148 }
149
150 #[test]
151 fn test_parse_bad_port() {
152 assert!(Url::parse("http://example.com:abc/path").is_err());
153 }
154
155 #[test]
156 fn test_parse_empty_host() {
157 assert!(Url::parse("http:///path").is_err());
158 }
159}