1use thiserror::Error;
2
3#[derive(Debug, Error)]
5pub enum UrlError {
6 #[error("cannot derive WebSocket URL from `{url}`: expected http:// or https:// scheme")]
7 InvalidScheme { url: String },
8}
9
10pub fn http_to_ws_url(url: &str) -> Result<String, UrlError> {
12 let scheme = if url.starts_with("https://") {
13 "wss"
14 } else if url.starts_with("http://") {
15 "ws"
16 } else {
17 return Err(UrlError::InvalidScheme {
18 url: url.to_string(),
19 });
20 };
21 let rest = url.split_once("://").map(|x| x.1).unwrap_or(url);
22 Ok(format!("{scheme}://{rest}"))
23}
24
25pub fn backtest_ws_url(url: &str) -> String {
32 if url.starts_with("ws://") || url.starts_with("wss://") {
33 let trimmed = url.trim_end_matches('/');
34 if trimmed.ends_with("/backtest") {
35 trimmed.to_string()
36 } else {
37 format!("{trimmed}/backtest")
38 }
39 } else if url.contains("://") {
40 url.to_string()
41 } else {
42 format!("wss://{}/backtest", url.trim_end_matches('/'))
43 }
44}
45
46pub fn http_base_from_ws_url(ws_url: &str) -> String {
49 let http = ws_url
50 .replacen("wss://", "https://", 1)
51 .replacen("ws://", "http://", 1);
52 if let Some(start) = http.find("://").map(|i| i + 3)
53 && let Some(slash) = http[start..].find('/')
54 {
55 return http[..start + slash].to_string();
56 }
57 http
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn http_to_ws_converts_http() {
66 assert_eq!(
67 http_to_ws_url("http://localhost:8899").unwrap(),
68 "ws://localhost:8899"
69 );
70 }
71
72 #[test]
73 fn http_to_ws_converts_https() {
74 assert_eq!(
75 http_to_ws_url("https://api.mainnet-beta.solana.com").unwrap(),
76 "wss://api.mainnet-beta.solana.com"
77 );
78 }
79
80 #[test]
81 fn http_to_ws_rejects_other_schemes() {
82 assert!(matches!(
83 http_to_ws_url("ws://example.com"),
84 Err(UrlError::InvalidScheme { .. })
85 ));
86 assert!(matches!(
87 http_to_ws_url("ftp://example.com"),
88 Err(UrlError::InvalidScheme { .. })
89 ));
90 assert!(matches!(
91 http_to_ws_url("example.com"),
92 Err(UrlError::InvalidScheme { .. })
93 ));
94 }
95
96 #[test]
97 fn backtest_ws_url_wraps_bare_hosts() {
98 assert_eq!(
99 backtest_ws_url("simulator.termina.technology"),
100 "wss://simulator.termina.technology/backtest"
101 );
102 }
103
104 #[test]
105 fn backtest_ws_url_keeps_explicit_schemes() {
106 assert_eq!(
107 backtest_ws_url("ws://localhost:8900"),
108 "ws://localhost:8900/backtest"
109 );
110 assert_eq!(
111 backtest_ws_url("wss://staging.simulator.termina.technology/"),
112 "wss://staging.simulator.termina.technology/backtest"
113 );
114 }
115
116 #[test]
117 fn backtest_ws_url_is_idempotent() {
118 for url in [
119 "simulator.termina.technology",
120 "ws://localhost:8900",
121 "wss://staging.simulator.termina.technology/",
122 ] {
123 let normalized = backtest_ws_url(url);
124 assert_eq!(backtest_ws_url(&normalized), normalized);
125 }
126 }
127
128 #[test]
129 fn backtest_ws_url_passes_other_schemes_through() {
130 assert_eq!(
131 backtest_ws_url("http://127.0.0.1:8900"),
132 "http://127.0.0.1:8900"
133 );
134 assert_eq!(
135 backtest_ws_url("https://host/backtest"),
136 "https://host/backtest"
137 );
138 }
139
140 #[test]
141 fn http_base_strips_path() {
142 assert_eq!(
143 http_base_from_ws_url("wss://host:8900/backtest"),
144 "https://host:8900"
145 );
146 assert_eq!(
147 http_base_from_ws_url("ws://localhost:8900/backtest"),
148 "http://localhost:8900"
149 );
150 }
151
152 #[test]
153 fn http_base_no_path() {
154 assert_eq!(
155 http_base_from_ws_url("wss://host:8900"),
156 "https://host:8900"
157 );
158 }
159}