1use std::time::Duration;
2
3use camel_component_api::{
4 CamelError, ComponentMetadata, NetworkRetryPolicy, UriConfig, UriOption,
5};
6
7#[derive(Debug, Clone, Default, serde::Deserialize)]
8pub struct WsConfig {
9 pub max_connections: Option<u32>,
10 pub max_message_size: Option<u32>,
11 pub heartbeat_interval_ms: Option<u64>,
12 pub idle_timeout_ms: Option<u64>,
13 pub connect_timeout_ms: Option<u64>,
14 pub response_timeout_ms: Option<u64>,
15 pub send_timeout_ms: Option<u64>,
16 pub binary_payload: Option<bool>,
17 pub subprotocols: Option<Vec<String>>,
18}
19
20#[derive(Debug, Clone, UriConfig)]
27#[allow(dead_code)]
28#[uri_scheme = "ws"]
29#[uri_config(
30 skip_impl,
31 metadata(
32 scheme = "ws",
33 description = "WebSocket client/server endpoint",
34 producer,
35 consumer
36 ),
37 crate = "camel_component_api"
38)]
39struct WsUriConfig {
40 #[allow(dead_code)]
41 _path: String,
42 #[uri_param(
43 name = "maxConnections",
44 default = "100",
45 desc = "Maximum concurrent WebSocket connections"
46 )]
47 max_connections: u32,
48 #[uri_param(
49 name = "maxMessageSize",
50 default = "65536",
51 desc = "Maximum WebSocket message size in bytes"
52 )]
53 max_message_size: u32,
54 #[uri_param(
55 name = "sendToAll",
56 default = "false",
57 desc = "Broadcast messages to all connected clients"
58 )]
59 send_to_all: bool,
60 #[uri_param(
61 name = "heartbeatIntervalMs",
62 default = "0",
63 desc = "Heartbeat ping interval in milliseconds"
64 )]
65 heartbeat_interval_ms: u64,
66 #[uri_param(
67 name = "idleTimeoutMs",
68 default = "0",
69 desc = "Connection idle timeout in milliseconds"
70 )]
71 idle_timeout_ms: u64,
72 #[uri_param(
73 name = "connectTimeoutMs",
74 default = "10000",
75 desc = "Connection timeout in milliseconds"
76 )]
77 connect_timeout_ms: u64,
78 #[uri_param(
79 name = "responseTimeoutMs",
80 default = "30000",
81 desc = "Response timeout in milliseconds"
82 )]
83 response_timeout_ms: u64,
84 #[uri_param(
85 name = "allowOrigin",
86 default = "*",
87 desc = "CORS origin allowed for WebSocket upgrade"
88 )]
89 allow_origin: String,
90 #[uri_param(name = "tlsCert", desc = "Path to TLS certificate file")]
91 tls_cert: Option<String>,
92 #[uri_param(name = "tlsKey", desc = "Path to TLS private key file")]
93 tls_key: Option<String>,
94 #[uri_param(
95 name = "reconnect",
96 default = "true",
97 desc = "Enable reconnection for client producer"
98 )]
99 reconnect: bool,
100 #[uri_param(
101 name = "reconnectMaxAttempts",
102 default = "5",
103 desc = "Maximum reconnection attempts"
104 )]
105 reconnect_max_attempts: u32,
106 #[uri_param(
107 name = "reconnectDelayMs",
108 default = "1000",
109 desc = "Delay between reconnection attempts"
110 )]
111 reconnect_delay_ms: u64,
112 #[uri_param(
113 name = "sendTimeoutMs",
114 default = "30000",
115 desc = "Send timeout in milliseconds"
116 )]
117 send_timeout_ms: u64,
118 #[uri_param(
119 name = "binaryPayload",
120 default = "false",
121 desc = "Use binary payload mode"
122 )]
123 binary_payload: bool,
124 #[uri_param(name = "subprotocols", desc = "Comma-separated WebSocket subprotocols")]
125 subprotocols_str: String,
126}
127
128#[derive(Clone)]
129pub struct WsEndpointConfig {
130 pub scheme: String,
131 pub host: String,
132 pub port: u16,
133 pub path: String,
134 pub max_connections: u32,
135 pub max_message_size: u32,
136 pub send_to_all: bool,
137 pub heartbeat_interval: Duration,
138 pub idle_timeout: Duration,
139 pub connect_timeout: Duration,
140 pub response_timeout: Duration,
141 pub allow_origin: String,
142 pub tls_cert: Option<String>,
143 pub tls_key: Option<String>,
144 pub reconnect: bool,
145 pub reconnect_max_attempts: u32,
146 pub reconnect_delay_ms: u64,
147 pub send_timeout: Duration,
148 pub binary_payload: bool,
149 pub subprotocols: Vec<String>,
150 pub reconnect_policy: NetworkRetryPolicy,
155}
156
157fn redacted_opt(opt: &Option<String>) -> Option<&'static str> {
158 if opt.is_some() { Some("***") } else { None }
159}
160
161impl std::fmt::Debug for WsEndpointConfig {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.debug_struct("WsEndpointConfig")
164 .field("scheme", &self.scheme)
165 .field("host", &self.host)
166 .field("port", &self.port)
167 .field("path", &self.path)
168 .field("max_connections", &self.max_connections)
169 .field("max_message_size", &self.max_message_size)
170 .field("send_to_all", &self.send_to_all)
171 .field("heartbeat_interval", &self.heartbeat_interval)
172 .field("idle_timeout", &self.idle_timeout)
173 .field("connect_timeout", &self.connect_timeout)
174 .field("response_timeout", &self.response_timeout)
175 .field("allow_origin", &self.allow_origin)
176 .field("tls_cert", &redacted_opt(&self.tls_cert))
177 .field("tls_key", &redacted_opt(&self.tls_key))
178 .field("reconnect", &self.reconnect)
179 .field("reconnect_max_attempts", &self.reconnect_max_attempts)
180 .field("reconnect_delay_ms", &self.reconnect_delay_ms)
181 .field("reconnect_policy", &self.reconnect_policy)
182 .field("send_timeout", &self.send_timeout)
183 .field("binary_payload", &self.binary_payload)
184 .field("subprotocols", &self.subprotocols)
185 .finish()
186 }
187}
188
189impl Default for WsEndpointConfig {
190 fn default() -> Self {
191 Self {
192 scheme: "ws".into(),
193 host: "0.0.0.0".into(),
194 port: 8080,
195 path: "/".into(),
196 max_connections: 100,
197 max_message_size: 65536,
198 send_to_all: false,
199 heartbeat_interval: Duration::ZERO,
200 idle_timeout: Duration::ZERO,
201 connect_timeout: Duration::from_secs(10),
202 response_timeout: Duration::from_secs(30),
203 allow_origin: "*".into(),
204 tls_cert: None,
205 tls_key: None,
206 reconnect: true,
207 reconnect_max_attempts: 5,
208 reconnect_delay_ms: 1000,
209 send_timeout: Duration::from_secs(30),
210 binary_payload: false,
211 subprotocols: Vec::new(),
212 reconnect_policy: NetworkRetryPolicy {
213 enabled: true,
214 max_attempts: 5,
215 initial_delay: Duration::from_millis(1000),
216 multiplier: 2.0,
217 max_delay: Duration::from_secs(30),
218 jitter_factor: 0.0, max_attempts_absolute: None,
220 },
221 }
222 }
223}
224
225#[derive(Debug, Clone)]
226pub struct WsServerConfig {
227 pub inner: WsEndpointConfig,
228}
229
230#[derive(Debug, Clone)]
231pub struct WsClientConfig {
232 pub inner: WsEndpointConfig,
233}
234
235impl WsConfig {
236 pub fn validate(&self) -> Result<(), CamelError> {
241 if let Some(0) = self.max_connections {
242 return Err(CamelError::Config(
243 "maxConnections must be >= 1 when specified".into(),
244 ));
245 }
246 if let Some(0) = self.max_message_size {
247 return Err(CamelError::Config(
248 "maxMessageSize must be >= 1 when specified".into(),
249 ));
250 }
251 Ok(())
252 }
253}
254
255impl WsEndpointConfig {
256 pub fn from_uri(uri: &str) -> Result<Self, CamelError> {
257 let parsed = camel_component_api::parse_uri(uri)
258 .map_err(|e| CamelError::EndpointCreationFailed(e.to_string()))?;
259
260 let scheme = parsed.scheme;
261 if scheme != "ws" && scheme != "wss" {
262 return Err(CamelError::EndpointCreationFailed(format!(
263 "Invalid WebSocket scheme: {scheme}"
264 )));
265 }
266
267 let host_port_path = parsed.path;
268 let host_port_path = host_port_path.strip_prefix("//").unwrap_or(&host_port_path);
269 let (host_port, path) = match host_port_path.split_once('/') {
270 Some((hp, p)) => (hp, format!("/{p}")),
271 None => (host_port_path, "/".to_string()),
272 };
273
274 let (host, port) = match host_port.rsplit_once(':') {
275 Some((h, p)) if p.parse::<u16>().is_ok() => {
276 let parsed_port = p.parse::<u16>().unwrap(); (h.to_string(), parsed_port)
278 }
279 _ => (
280 host_port.to_string(),
281 if scheme == "wss" { 443 } else { 80 },
282 ),
283 };
284
285 let mut cfg = Self {
286 scheme,
287 host: if host.is_empty() {
288 "0.0.0.0".to_string()
289 } else {
290 host
291 },
292 port,
293 path,
294 ..Self::default()
295 };
296
297 let params = parsed.params;
298 if let Some(raw) = params.get("maxConnections") {
300 let v = raw.parse::<u32>().map_err(|_| {
301 CamelError::InvalidUri(format!(
302 "maxConnections must be an unsigned integer, got '{raw}'"
303 ))
304 })?;
305 if v == 0 {
306 return Err(CamelError::InvalidUri("maxConnections must be >= 1".into()));
307 }
308 cfg.max_connections = v;
309 }
310 if let Some(raw) = params.get("maxMessageSize") {
312 let v = raw.parse::<u32>().map_err(|_| {
313 CamelError::InvalidUri(format!(
314 "maxMessageSize must be an unsigned integer, got '{raw}'"
315 ))
316 })?;
317 if v == 0 {
318 return Err(CamelError::InvalidUri("maxMessageSize must be > 0".into()));
319 }
320 cfg.max_message_size = v;
321 }
322 if let Some(raw) = params.get("sendToAll") {
323 let v = raw.parse::<bool>().map_err(|_| {
324 CamelError::InvalidUri(format!(
325 "sendToAll must be a boolean ('true' or 'false'), got '{raw}'"
326 ))
327 })?;
328 cfg.send_to_all = v;
329 }
330 if let Some(raw) = params.get("heartbeatIntervalMs") {
331 let v = raw.parse::<u64>().map_err(|_| {
332 CamelError::InvalidUri(format!(
333 "heartbeatIntervalMs must be an unsigned integer, got '{raw}'"
334 ))
335 })?;
336 cfg.heartbeat_interval = Duration::from_millis(v);
337 }
338 if let Some(raw) = params.get("idleTimeoutMs") {
339 let v = raw.parse::<u64>().map_err(|_| {
340 CamelError::InvalidUri(format!(
341 "idleTimeoutMs must be an unsigned integer, got '{raw}'"
342 ))
343 })?;
344 cfg.idle_timeout = Duration::from_millis(v);
345 }
346 if let Some(raw) = params.get("connectTimeoutMs") {
347 let v = raw.parse::<u64>().map_err(|_| {
348 CamelError::InvalidUri(format!(
349 "connectTimeoutMs must be an unsigned integer, got '{raw}'"
350 ))
351 })?;
352 cfg.connect_timeout = Duration::from_millis(v);
353 }
354 if let Some(raw) = params.get("responseTimeoutMs") {
355 let v = raw.parse::<u64>().map_err(|_| {
356 CamelError::InvalidUri(format!(
357 "responseTimeoutMs must be an unsigned integer, got '{raw}'"
358 ))
359 })?;
360 cfg.response_timeout = Duration::from_millis(v);
361 }
362 if let Some(v) = params.get("allowOrigin") {
363 if v.is_empty() {
364 return Err(CamelError::InvalidUri(
365 "allowOrigin must not be empty when specified".into(),
366 ));
367 }
368 cfg.allow_origin = v.to_string();
369 }
370 if let Some(v) = params.get("tlsCert") {
371 cfg.tls_cert = Some(v.to_string());
372 }
373 if let Some(v) = params.get("tlsKey") {
374 cfg.tls_key = Some(v.to_string());
375 }
376 let mut reconnect_explicit = false;
381 let mut reconnect_max_attempts_explicit = false;
382 let mut reconnect_delay_ms_explicit = false;
383
384 if let Some(raw) = params.get("reconnect") {
385 cfg.reconnect = raw.parse::<bool>().map_err(|_| {
386 CamelError::InvalidUri(format!(
387 "reconnect must be a boolean ('true' or 'false'), got '{raw}'"
388 ))
389 })?;
390 reconnect_explicit = true;
391 }
392 if let Some(raw) = params.get("reconnectMaxAttempts") {
393 cfg.reconnect_max_attempts = raw.parse::<u32>().map_err(|_| {
394 CamelError::InvalidUri(format!(
395 "reconnectMaxAttempts must be an unsigned integer, got '{raw}'"
396 ))
397 })?;
398 reconnect_max_attempts_explicit = true;
399 }
400 if let Some(raw) = params.get("reconnectDelayMs") {
401 cfg.reconnect_delay_ms = raw.parse::<u64>().map_err(|_| {
402 CamelError::InvalidUri(format!(
403 "reconnectDelayMs must be an unsigned integer, got '{raw}'"
404 ))
405 })?;
406 reconnect_delay_ms_explicit = true;
407 }
408 if let Some(raw) = params.get("sendTimeoutMs") {
409 let v = raw.parse::<u64>().map_err(|_| {
410 CamelError::InvalidUri(format!(
411 "sendTimeoutMs must be an unsigned integer, got '{raw}'"
412 ))
413 })?;
414 cfg.send_timeout = Duration::from_millis(v);
415 }
416 if let Some(raw) = params.get("binaryPayload") {
417 cfg.binary_payload = raw.parse::<bool>().map_err(|_| {
418 CamelError::InvalidUri(format!(
419 "binaryPayload must be a boolean ('true' or 'false'), got '{raw}'"
420 ))
421 })?;
422 }
423 if let Some(raw) = params.get("subprotocols") {
424 cfg.subprotocols = raw
425 .split(',')
426 .map(|s| s.trim().to_string())
427 .filter(|s| !s.is_empty())
428 .collect();
429 }
430
431 if reconnect_explicit {
440 cfg.reconnect_policy.enabled = cfg.reconnect;
441 }
442 if reconnect_max_attempts_explicit {
443 cfg.reconnect_policy.max_attempts = cfg.reconnect_max_attempts;
444 }
445 if reconnect_delay_ms_explicit {
446 cfg.reconnect_policy.initial_delay = Duration::from_millis(cfg.reconnect_delay_ms);
447 }
448
449 Ok(cfg)
450 }
451
452 pub fn server_config(&self) -> WsServerConfig {
453 WsServerConfig {
454 inner: self.clone(),
455 }
456 }
457
458 pub fn client_config(&self) -> WsClientConfig {
459 WsClientConfig {
460 inner: self.clone(),
461 }
462 }
463
464 pub fn canonical_host(&self) -> String {
465 match self.host.as_str() {
466 "0.0.0.0" | "localhost" => "127.0.0.1".to_string(),
467 h => h.to_string(),
468 }
469 }
470
471 pub fn metadata() -> ComponentMetadata {
474 WsUriConfig::metadata()
475 }
476
477 pub fn uri_options() -> Vec<UriOption> {
480 WsUriConfig::uri_options()
481 }
482}
483
484#[cfg(test)]
485mod config_validation_tests {
486 use super::*;
487
488 #[test]
489 fn test_rejects_zero_max_connections() {
490 let cfg = WsConfig {
491 max_connections: Some(0),
492 ..WsConfig::default()
493 };
494 assert!(cfg.validate().is_err());
495 }
496
497 #[test]
498 fn test_rejects_zero_max_message_size() {
499 let cfg = WsConfig {
500 max_message_size: Some(0),
501 ..WsConfig::default()
502 };
503 assert!(cfg.validate().is_err());
504 }
505
506 #[test]
507 fn test_accepts_valid_config() {
508 let cfg = WsConfig::default();
509 assert!(cfg.validate().is_ok());
510 }
511
512 #[test]
513 fn test_accepts_nonzero_max_connections() {
514 let cfg = WsConfig {
515 max_connections: Some(50),
516 ..WsConfig::default()
517 };
518 assert!(cfg.validate().is_ok());
519 }
520
521 #[test]
522 fn test_accepts_nonzero_max_message_size() {
523 let cfg = WsConfig {
524 max_message_size: Some(1024),
525 ..WsConfig::default()
526 };
527 assert!(cfg.validate().is_ok());
528 }
529
530 #[test]
531 fn test_from_uri_rejects_invalid_send_to_all() {
532 let err = WsEndpointConfig::from_uri("ws://localhost:8080?sendToAll=yes").unwrap_err();
533 assert!(err.to_string().contains("sendToAll"));
534 }
535
536 #[test]
537 fn test_from_uri_rejects_invalid_max_connections_numeric() {
538 let err = WsEndpointConfig::from_uri("ws://localhost:8080?maxConnections=abc").unwrap_err();
539 assert!(err.to_string().contains("maxConnections"));
540 }
541
542 #[test]
543 fn test_from_uri_rejects_invalid_max_message_size_numeric() {
544 let err = WsEndpointConfig::from_uri("ws://localhost:8080?maxMessageSize=abc").unwrap_err();
545 assert!(err.to_string().contains("maxMessageSize"));
546 }
547
548 #[test]
549 fn test_from_uri_rejects_invalid_heartbeat_interval_numeric() {
550 let err =
551 WsEndpointConfig::from_uri("ws://localhost:8080?heartbeatIntervalMs=abc").unwrap_err();
552 assert!(err.to_string().contains("heartbeatIntervalMs"));
553 }
554
555 #[test]
556 fn test_from_uri_rejects_invalid_idle_timeout_numeric() {
557 let err = WsEndpointConfig::from_uri("ws://localhost:8080?idleTimeoutMs=abc").unwrap_err();
558 assert!(err.to_string().contains("idleTimeoutMs"));
559 }
560
561 #[test]
562 fn test_from_uri_rejects_invalid_connect_timeout_numeric() {
563 let err =
564 WsEndpointConfig::from_uri("ws://localhost:8080?connectTimeoutMs=abc").unwrap_err();
565 assert!(err.to_string().contains("connectTimeoutMs"));
566 }
567
568 #[test]
569 fn test_from_uri_rejects_invalid_response_timeout_numeric() {
570 let err =
571 WsEndpointConfig::from_uri("ws://localhost:8080?responseTimeoutMs=abc").unwrap_err();
572 assert!(err.to_string().contains("responseTimeoutMs"));
573 }
574
575 #[test]
577 fn test_from_uri_parses_send_timeout_ms() {
578 let cfg = WsEndpointConfig::from_uri("ws://localhost:8080?sendTimeoutMs=7500").unwrap();
579 assert_eq!(cfg.send_timeout, Duration::from_millis(7500));
580 }
581
582 #[test]
583 fn test_from_uri_rejects_invalid_send_timeout_ms() {
584 let err = WsEndpointConfig::from_uri("ws://localhost:8080?sendTimeoutMs=xyz").unwrap_err();
585 assert!(err.to_string().contains("sendTimeoutMs"));
586 }
587
588 #[test]
590 fn test_from_uri_parses_binary_payload_true() {
591 let cfg = WsEndpointConfig::from_uri("ws://localhost:8080?binaryPayload=true").unwrap();
592 assert!(cfg.binary_payload);
593 }
594
595 #[test]
596 fn test_from_uri_parses_binary_payload_false() {
597 let cfg = WsEndpointConfig::from_uri("ws://localhost:8080?binaryPayload=false").unwrap();
598 assert!(!cfg.binary_payload);
599 }
600
601 #[test]
602 fn test_from_uri_rejects_invalid_binary_payload() {
603 let err = WsEndpointConfig::from_uri("ws://localhost:8080?binaryPayload=sure").unwrap_err();
604 assert!(err.to_string().contains("binaryPayload"));
605 }
606
607 #[test]
609 fn test_from_uri_parses_subprotocols() {
610 let cfg =
611 WsEndpointConfig::from_uri("ws://localhost:8080?subprotocols=json,protobuf").unwrap();
612 assert_eq!(cfg.subprotocols, vec!["json", "protobuf"]);
613 }
614
615 #[test]
616 fn test_from_uri_subprotocols_trims_whitespace() {
617 let cfg = WsEndpointConfig::from_uri("ws://localhost:8080?subprotocols=a, b").unwrap();
618 assert_eq!(cfg.subprotocols, vec!["a", "b"]);
619 }
620
621 #[test]
622 fn test_from_uri_subprotocols_empty_when_not_specified() {
623 let cfg = WsEndpointConfig::from_uri("ws://localhost:8080").unwrap();
624 assert!(cfg.subprotocols.is_empty());
625 }
626
627 #[test]
628 fn ws_endpoint_config_debug_redacts_tls() {
629 let config = WsEndpointConfig {
630 tls_cert: Some("/secret/cert.pem".to_string()),
631 tls_key: Some("/secret/key.pem".to_string()),
632 ..WsEndpointConfig::default()
633 };
634 let debug = format!("{:?}", config);
635 assert!(
636 !debug.contains("/secret/"),
637 "TLS paths must be redacted: {debug}"
638 );
639 assert!(
640 debug.contains("tls_cert"),
641 "field name should appear: {debug}"
642 );
643 assert!(
644 debug.contains("tls_key"),
645 "field name should appear: {debug}"
646 );
647 }
648
649 #[test]
650 fn ws_endpoint_config_has_reconnect_policy_field() {
651 let cfg = WsEndpointConfig::default();
652 assert!(cfg.reconnect_policy.enabled);
653 assert_eq!(cfg.reconnect_policy.max_attempts, 5);
654 assert_eq!(
655 cfg.reconnect_policy.initial_delay,
656 std::time::Duration::from_millis(1000)
657 );
658 }
659
660 #[test]
661 fn ws_endpoint_uri_bridges_flat_fields_to_policy() {
662 let uri =
664 "ws://localhost:9001/test?reconnect=false&reconnectMaxAttempts=7&reconnectDelayMs=250";
665 let cfg = WsEndpointConfig::from_uri(uri).unwrap();
666 assert!(!cfg.reconnect);
667 assert_eq!(cfg.reconnect_max_attempts, 7);
668 assert_eq!(cfg.reconnect_delay_ms, 250);
669 assert!(!cfg.reconnect_policy.enabled);
671 assert_eq!(cfg.reconnect_policy.max_attempts, 7);
672 assert_eq!(
673 cfg.reconnect_policy.initial_delay,
674 std::time::Duration::from_millis(250)
675 );
676 }
677
678 #[test]
679 fn ws_endpoint_policy_defaults_match_old_flat_defaults() {
680 let cfg = WsEndpointConfig::default();
681 assert!(cfg.reconnect_policy.enabled);
682 assert_eq!(cfg.reconnect_policy.max_attempts, 5);
683 assert_eq!(
684 cfg.reconnect_policy.initial_delay,
685 std::time::Duration::from_millis(1000)
686 );
687 assert!((cfg.reconnect_policy.multiplier - 2.0).abs() < f64::EPSILON);
688 assert_eq!(
689 cfg.reconnect_policy.max_delay,
690 std::time::Duration::from_secs(30)
691 );
692 assert!((cfg.reconnect_policy.jitter_factor - 0.0).abs() < f64::EPSILON);
693 }
694
695 #[test]
701 fn ws_endpoint_policy_preserved_when_no_flat_fields() {
702 let cfg = WsEndpointConfig::from_uri("ws://localhost:8080/echo").unwrap();
703 assert!(cfg.reconnect_policy.enabled);
705 assert_eq!(cfg.reconnect_policy.max_attempts, 5);
706 assert_eq!(
707 cfg.reconnect_policy.initial_delay,
708 std::time::Duration::from_millis(1000)
709 );
710 }
711
712 #[test]
722 fn ws_endpoint_policy_from_toml_preserved_across_from_uri() {
723 let toml_policy = NetworkRetryPolicy {
726 enabled: true,
727 max_attempts: 10,
728 initial_delay: std::time::Duration::from_millis(250),
729 multiplier: 3.0,
730 max_delay: std::time::Duration::from_secs(60),
731 jitter_factor: 0.1,
732 max_attempts_absolute: None,
733 };
734 let mut cfg = WsEndpointConfig::from_uri("ws://localhost:8080/echo").unwrap();
739 cfg.reconnect_policy = toml_policy.clone();
740 let cfg2 = WsEndpointConfig::from_uri("ws://localhost:8080/echo").unwrap();
743 let default_policy = WsEndpointConfig::default().reconnect_policy;
748 assert_eq!(cfg2.reconnect_policy, default_policy);
749 cfg.reconnect_policy = toml_policy.clone();
752 assert_eq!(cfg.reconnect_policy.max_attempts, 10);
753 assert_eq!(
754 cfg.reconnect_policy.initial_delay,
755 std::time::Duration::from_millis(250)
756 );
757 }
758
759 #[test]
762 fn ws_endpoint_policy_partial_bridge() {
763 let uri = "ws://localhost:9001/test?reconnectMaxAttempts=10";
764 let cfg = WsEndpointConfig::from_uri(uri).unwrap();
765 assert_eq!(cfg.reconnect_policy.max_attempts, 10);
767 assert!(cfg.reconnect_policy.enabled);
769 assert_eq!(
770 cfg.reconnect_policy.initial_delay,
771 std::time::Duration::from_millis(1000)
772 );
773 }
774
775 #[test]
778 fn network_retry_policy_from_toml() {
779 let toml_str = r#"
780 enabled = true
781 max_attempts = 10
782 initial_delay_ms = 250
783 multiplier = 3.0
784 max_delay_ms = 60000
785 jitter_factor = 0.1
786 "#;
787 let policy: NetworkRetryPolicy = toml::from_str(toml_str).unwrap();
788 assert!(policy.enabled);
789 assert_eq!(policy.max_attempts, 10);
790 assert_eq!(policy.initial_delay, std::time::Duration::from_millis(250));
791 assert!((policy.multiplier - 3.0).abs() < f64::EPSILON);
792 assert_eq!(policy.max_delay, std::time::Duration::from_millis(60_000));
793 assert!((policy.jitter_factor - 0.1).abs() < f64::EPSILON);
794 }
795}
796
797#[cfg(test)]
798mod uri_parity_tests {
799 #[test]
800 fn uri_options_count_parity() {
801 assert_eq!(
802 super::WsUriConfig::uri_options().len(),
803 16,
804 "WsUriConfig #[uri_param] count drifted from parser"
805 );
806 }
807}