1use snafu::ResultExt;
24use snafu::ensure;
25use url::Url;
26
27use crate::Result;
28use crate::error::InvalidUrlSnafu;
29use crate::error::UnknownSchemeSnafu;
30
31pub const KNOWN_SCHEMES: &[&str] = &[
39 "inproc", "mock", "tcp", "tcp+tls", "noise", "quic", "quic4", "quic6", "ws", "wss",
40];
41
42#[derive(Debug, Clone)]
47pub struct TransportUrl {
48 inner: Url,
49}
50
51impl TransportUrl {
52 pub fn parse(input: &str) -> Result<Self> {
54 let url = Url::parse(input).context(InvalidUrlSnafu {
55 url: input.to_string(),
56 })?;
57 ensure!(
58 KNOWN_SCHEMES.contains(&url.scheme()),
59 UnknownSchemeSnafu {
60 scheme: url.scheme().to_string(),
61 }
62 );
63 Ok(Self { inner: url })
64 }
65
66 pub fn scheme(&self) -> &str {
68 self.inner.scheme()
69 }
70
71 pub fn host(&self) -> Option<&str> {
74 self.inner.host_str()
75 }
76
77 pub fn port(&self) -> Option<u16> {
79 self.inner.port()
80 }
81
82 pub fn path(&self) -> &str {
85 self.inner.path()
86 }
87
88 pub fn channel_name(&self) -> Option<&str> {
92 match self.scheme() {
93 "inproc" | "mock" => self.inner.host_str(),
94 _ => None,
95 }
96 }
97
98 pub fn as_url(&self) -> &Url {
100 &self.inner
101 }
102}
103
104impl std::fmt::Display for TransportUrl {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 self.inner.fmt(f)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn parses_inproc_url() {
116 let u = TransportUrl::parse("inproc://session-42").unwrap();
117 assert_eq!(u.scheme(), "inproc");
118 assert_eq!(u.channel_name(), Some("session-42"));
119 assert_eq!(u.port(), None);
120 }
121
122 #[test]
123 fn parses_mock_url() {
124 let u = TransportUrl::parse("mock://round-3").unwrap();
125 assert_eq!(u.scheme(), "mock");
126 assert_eq!(u.channel_name(), Some("round-3"));
127 }
128
129 #[test]
130 fn parses_tcp_url_with_port() {
131 let u = TransportUrl::parse("tcp://1.2.3.4:443").unwrap();
132 assert_eq!(u.scheme(), "tcp");
133 assert_eq!(u.host(), Some("1.2.3.4"));
134 assert_eq!(u.port(), Some(443));
135 assert!(u.channel_name().is_none());
136 }
137
138 #[test]
139 fn parses_tcp_tls_url() {
140 let u = TransportUrl::parse("tcp+tls://example.com:443").unwrap();
141 assert_eq!(u.scheme(), "tcp+tls");
142 }
143
144 #[test]
145 fn parses_quic_url() {
146 let u = TransportUrl::parse("quic://node.example:8443").unwrap();
147 assert_eq!(u.scheme(), "quic");
148 assert_eq!(u.port(), Some(8443));
149 }
150
151 #[test]
152 fn parses_ws_and_wss_urls() {
153 let ws = TransportUrl::parse("ws://example.com:80/sess").unwrap();
154 assert_eq!(ws.scheme(), "ws");
155 assert_eq!(ws.path(), "/sess");
156 let wss = TransportUrl::parse("wss://example.com/sess").unwrap();
157 assert_eq!(wss.scheme(), "wss");
158 }
159
160 #[test]
161 fn rejects_unknown_scheme() {
162 let err = TransportUrl::parse("ftp://example.com").unwrap_err();
163 assert!(matches!(
164 err,
165 crate::error::Error::UnknownScheme { ref scheme, .. } if scheme == "ftp"
166 ));
167 }
168
169 #[test]
170 fn rejects_malformed_url() {
171 let err = TransportUrl::parse("not a url at all").unwrap_err();
172 assert!(matches!(err, crate::error::Error::InvalidUrl { .. }));
173 }
174
175 #[test]
176 fn accepts_inproc_with_empty_host() {
177 let u = TransportUrl::parse("inproc://").unwrap();
182 assert_eq!(u.channel_name(), None);
183 }
184
185 #[test]
186 fn display_round_trips() {
187 let s = "inproc://session-42";
188 let u = TransportUrl::parse(s).unwrap();
189 assert_eq!(u.to_string(), s);
190 }
191}