feagi_io/protocol_implementations/websocket/
shared.rs1use crate::FeagiNetworkError;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct WebSocketUrl {
9 url: String,
10}
11
12impl WebSocketUrl {
13 pub fn new(url: &str) -> Result<Self, FeagiNetworkError> {
25 let normalized = normalize_ws_url(url);
26 validate_ws_url(&normalized)?;
27 Ok(WebSocketUrl { url: normalized })
28 }
29
30 #[allow(dead_code)]
32 pub fn as_str(&self) -> &str {
33 &self.url
34 }
35
36 pub fn host_port(&self) -> String {
42 let is_secure = self.url.starts_with("wss://");
43
44 let without_scheme = self
46 .url
47 .strip_prefix("ws://")
48 .or_else(|| self.url.strip_prefix("wss://"))
49 .unwrap_or(&self.url);
50
51 let host_port = without_scheme.split('/').next().unwrap_or(without_scheme);
53
54 if host_port.contains(':') {
56 host_port.to_string()
57 } else {
58 let default_port = if is_secure { 443 } else { 80 };
60 format!("{}:{}", host_port, default_port)
61 }
62 }
63
64 #[allow(dead_code)]
66 pub fn is_secure(&self) -> bool {
67 self.url.starts_with("wss://")
68 }
69}
70
71impl std::fmt::Display for WebSocketUrl {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "{}", self.url)
74 }
75}
76
77fn normalize_ws_url(url: &str) -> String {
89 if url.starts_with("ws://") || url.starts_with("wss://") {
90 url.to_string()
91 } else {
92 format!("ws://{}", url)
93 }
94}
95
96fn validate_ws_url(url: &str) -> Result<(), FeagiNetworkError> {
100 const VALID_PREFIXES: [&str; 2] = ["ws://", "wss://"];
102
103 if !VALID_PREFIXES.iter().any(|prefix| url.starts_with(prefix)) {
104 return Err(FeagiNetworkError::InvalidSocketProperties(format!(
105 "Invalid WebSocket URL '{}': must start with one of {:?}",
106 url, VALID_PREFIXES
107 )));
108 }
109
110 let addr_part = url
112 .strip_prefix("wss://")
113 .or_else(|| url.strip_prefix("ws://"))
114 .ok_or_else(|| {
115 FeagiNetworkError::InvalidSocketProperties(format!(
116 "Invalid WebSocket URL '{}': expected {:?}",
117 url, VALID_PREFIXES
118 ))
119 })?;
120
121 if addr_part.is_empty() {
122 return Err(FeagiNetworkError::InvalidSocketProperties(format!(
123 "Invalid WebSocket URL '{}': empty address after scheme",
124 url
125 )));
126 }
127
128 let host_port = addr_part.split('/').next().unwrap_or(addr_part);
130
131 if host_port.is_empty() {
132 return Err(FeagiNetworkError::InvalidSocketProperties(format!(
133 "Invalid WebSocket URL '{}': empty host",
134 url
135 )));
136 }
137
138 Ok(())
139}
140
141#[allow(dead_code)]
151pub fn validate_bind_address(bind_address: &str) -> Result<(), FeagiNetworkError> {
152 if bind_address.is_empty() {
153 return Err(FeagiNetworkError::InvalidSocketProperties(
154 "Invalid bind address: empty string".to_string(),
155 ));
156 }
157
158 if bind_address.starts_with("ws://")
160 || bind_address.starts_with("wss://")
161 || bind_address.starts_with("http://")
162 || bind_address.starts_with("https://")
163 {
164 return Err(FeagiNetworkError::InvalidSocketProperties(format!(
165 "Invalid bind address '{}': should be host:port without scheme",
166 bind_address
167 )));
168 }
169
170 if !bind_address.contains(':') {
172 return Err(FeagiNetworkError::InvalidSocketProperties(format!(
173 "Invalid bind address '{}': missing port (expected host:port)",
174 bind_address
175 )));
176 }
177
178 Ok(())
179}