1use rustlavel_core::{Error, Result};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Url {
7 pub secure: bool,
8 pub host: String,
9 pub port: u16,
10 pub target: String,
12}
13
14impl Url {
15 pub fn parse(input: &str) -> Result<Url> {
16 let (scheme, rest) = input
17 .split_once("://")
18 .ok_or_else(|| Error::msg(format!("`{input}` has no scheme; expected http:// or https://")))?;
19
20 let secure = match scheme.to_ascii_lowercase().as_str() {
21 "https" => true,
22 "http" => false,
23 other => {
24 return Err(Error::msg(format!(
25 "`{other}` is not a scheme this client speaks; use http or https"
26 )));
27 }
28 };
29
30 let (authority, path) = match rest.find('/') {
31 Some(at) => (&rest[..at], &rest[at..]),
32 None => (rest, "/"),
33 };
34
35 if authority.contains('@') {
38 return Err(Error::msg(
39 "credentials in a URL are not supported; send an Authorization header instead"
40 .to_string(),
41 ));
42 }
43
44 let (host, port) = match authority.rsplit_once(':') {
45 Some((host, port)) => {
46 let port = port
47 .parse()
48 .map_err(|_| Error::msg(format!("`{port}` is not a valid port")))?;
49 (host, port)
50 }
51 None => (authority, if secure { 443 } else { 80 }),
52 };
53
54 if host.is_empty() {
55 return Err(Error::msg(format!("`{input}` has no host")));
56 }
57
58 Ok(Url {
59 secure,
60 host: host.to_string(),
61 port,
62 target: if path.is_empty() { "/".to_string() } else { path.to_string() },
63 })
64 }
65
66 pub fn authority(&self) -> String {
68 let default = if self.secure { 443 } else { 80 };
69 if self.port == default {
70 self.host.clone()
71 } else {
72 format!("{}:{}", self.host, self.port)
73 }
74 }
75
76 pub fn socket_address(&self) -> String {
77 format!("{}:{}", self.host, self.port)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn parses_the_common_shapes() {
87 let url = Url::parse("https://api.anthropic.com/v1/messages").unwrap();
88 assert!(url.secure);
89 assert_eq!(url.host, "api.anthropic.com");
90 assert_eq!(url.port, 443);
91 assert_eq!(url.target, "/v1/messages");
92 assert_eq!(url.authority(), "api.anthropic.com");
93
94 let local = Url::parse("http://127.0.0.1:11434/api/chat?stream=true").unwrap();
95 assert!(!local.secure);
96 assert_eq!(local.port, 11434);
97 assert_eq!(local.target, "/api/chat?stream=true");
98 assert_eq!(local.authority(), "127.0.0.1:11434");
99 }
100
101 #[test]
102 fn a_bare_host_gets_a_root_path() {
103 assert_eq!(Url::parse("https://example.com").unwrap().target, "/");
104 }
105
106 #[test]
107 fn rejects_what_it_cannot_send_correctly() {
108 assert!(Url::parse("example.com/path").is_err());
109 assert!(Url::parse("ftp://example.com").is_err());
110 assert!(Url::parse("https://user:pass@example.com").is_err());
111 assert!(Url::parse("https://example.com:notaport/").is_err());
112 }
113}