openigtlink_rust/io/builder.rs
1//! Type-state builder pattern for OpenIGTLink clients
2//!
3//! This module provides a flexible, type-safe way to construct OpenIGTLink clients
4//! with exactly the features you need. The builder uses Rust's type system to prevent
5//! invalid configurations at compile time.
6//!
7//! # Design Philosophy
8//!
9//! Instead of creating separate types for every feature combination (which would lead
10//! to exponential growth: TcpAsync, TcpAsyncTls, TcpAsyncReconnect, TcpAsyncTlsReconnect...),
11//! this builder creates a single [`UnifiedAsyncClient`]
12//! with optional features.
13//!
14//! **Benefits:**
15//! - ✅ Compile-time safety: Invalid combinations caught at compile time
16//! - ✅ No variant explosion: Scales to any number of features
17//! - ✅ Zero runtime cost: `PhantomData` markers are optimized away
18//! - ✅ Ergonomic API: Method chaining with clear intent
19//!
20//! # Type-State Pattern
21//!
22//! The builder uses type states to enforce valid construction:
23//!
24//! ```text
25//! ClientBuilder<Unspecified, Unspecified>
26//! ├─ .tcp(addr) → ClientBuilder<TcpConfigured, Unspecified>
27//! │ ├─ .sync() → ClientBuilder<TcpConfigured, SyncMode>
28//! │ │ └─ .build() → Result<SyncIgtlClient>
29//! │ └─ .async_mode() → ClientBuilder<TcpConfigured, AsyncMode>
30//! │ ├─ .with_tls(config) → self
31//! │ ├─ .with_reconnect(cfg) → self
32//! │ ├─ .verify_crc(bool) → self
33//! │ └─ .build() → Result<UnifiedAsyncClient>
34//! └─ .udp(addr) → ClientBuilder<UdpConfigured, Unspecified>
35//! └─ .build() → Result<UdpClient>
36//! ```
37//!
38//! Invalid state transitions result in **compile errors**, not runtime errors!
39//!
40//! # Examples
41//!
42//! ## Basic TCP Clients
43//!
44//! ```no_run
45//! use openigtlink_rust::io::builder::ClientBuilder;
46//!
47//! # fn main() -> Result<(), openigtlink_rust::error::IgtlError> {
48//! // Synchronous TCP client (blocking I/O)
49//! let client = ClientBuilder::new()
50//! .tcp("127.0.0.1:18944")
51//! .sync()
52//! .build()?;
53//! # Ok(())
54//! # }
55//!
56//! // Asynchronous TCP client (Tokio)
57//! # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
58//! # use openigtlink_rust::io::builder::ClientBuilder;
59//! let client = ClientBuilder::new()
60//! .tcp("127.0.0.1:18944")
61//! .async_mode()
62//! .build()
63//! .await?;
64//! # Ok(())
65//! # }
66//! ```
67//!
68//! ## TLS-Encrypted Clients
69//!
70//! ```no_run
71//! use openigtlink_rust::io::builder::ClientBuilder;
72//! use std::sync::Arc;
73//!
74//! # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
75//! // TLS client for secure hospital networks
76//! let tls_config = rustls::ClientConfig::builder()
77//! .with_root_certificates(rustls::RootCertStore::empty())
78//! .with_no_client_auth();
79//!
80//! let client = ClientBuilder::new()
81//! .tcp("hospital-server.local:18944")
82//! .async_mode()
83//! .with_tls(Arc::new(tls_config))
84//! .build()
85//! .await?;
86//! # Ok(())
87//! # }
88//! ```
89//!
90//! ## Auto-Reconnecting Clients
91//!
92//! ```no_run
93//! use openigtlink_rust::io::builder::ClientBuilder;
94//! use openigtlink_rust::io::reconnect::ReconnectConfig;
95//!
96//! # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
97//! // Client that auto-reconnects on network failures
98//! let reconnect_config = ReconnectConfig::with_max_attempts(10);
99//! let client = ClientBuilder::new()
100//! .tcp("127.0.0.1:18944")
101//! .async_mode()
102//! .with_reconnect(reconnect_config)
103//! .build()
104//! .await?;
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! ## Combined Features (TLS + Auto-Reconnect)
110//!
111//! ```no_run
112//! use openigtlink_rust::io::builder::ClientBuilder;
113//! use openigtlink_rust::io::reconnect::ReconnectConfig;
114//! use std::sync::Arc;
115//!
116//! # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
117//! // Production-ready client with encryption AND auto-reconnect
118//! let tls_config = rustls::ClientConfig::builder()
119//! .with_root_certificates(rustls::RootCertStore::empty())
120//! .with_no_client_auth();
121//!
122//! let reconnect_config = ReconnectConfig::with_max_attempts(100);
123//!
124//! let client = ClientBuilder::new()
125//! .tcp("production-server:18944")
126//! .async_mode()
127//! .with_tls(Arc::new(tls_config))
128//! .with_reconnect(reconnect_config)
129//! .verify_crc(true)
130//! .build()
131//! .await?;
132//! # Ok(())
133//! # }
134//! ```
135//!
136//! ## UDP Client for Low-Latency Tracking
137//!
138//! ```no_run
139//! use openigtlink_rust::io::builder::ClientBuilder;
140//!
141//! // UDP client for real-time surgical tool tracking (120+ Hz)
142//! let client = ClientBuilder::new()
143//! .udp("127.0.0.1:18944")
144//! .build()?;
145//! # Ok::<(), openigtlink_rust::error::IgtlError>(())
146//! ```
147//!
148//! ## Compile-Time Error Prevention
149//!
150//! The following code will **not compile**:
151//!
152//! ```compile_fail
153//! use openigtlink_rust::io::builder::ClientBuilder;
154//!
155//! // ERROR: This library does not implement UDP + TLS (DTLS)
156//! let client = ClientBuilder::new()
157//! .udp("127.0.0.1:18944")
158//! .with_tls(config) // ← Compile error: method not found
159//! .build()?;
160//! ```
161//!
162//! **Note**: While DTLS (Datagram TLS) exists in theory, this library focuses on
163//! TCP-based TLS as it's the standard for OpenIGTLink secure communications.
164
165use crate::error::Result;
166use crate::io::reconnect::ReconnectConfig;
167use crate::io::sync_client::SyncTcpClient;
168use crate::io::unified_async_client::UnifiedAsyncClient;
169use crate::io::unified_client::{AsyncIgtlClient, SyncIgtlClient};
170use crate::io::UdpClient;
171use std::marker::PhantomData;
172use std::sync::Arc;
173use tokio_rustls::rustls;
174
175// ============================================================================
176// State Marker Types
177// ============================================================================
178
179/// Unspecified protocol or mode state
180///
181/// This is the initial state before protocol or mode selection.
182pub struct Unspecified;
183
184/// TCP protocol configured state
185///
186/// Contains the server address for TCP connection.
187#[allow(dead_code)]
188pub struct TcpConfigured {
189 pub(crate) addr: String,
190}
191
192/// UDP protocol configured state
193///
194/// Contains the server address for UDP communication.
195/// Note: UDP only supports synchronous mode.
196#[allow(dead_code)]
197pub struct UdpConfigured {
198 pub(crate) addr: String,
199}
200
201/// Synchronous (blocking) mode state
202pub struct SyncMode;
203
204/// Asynchronous (non-blocking) mode state
205pub struct AsyncMode;
206
207// ============================================================================
208// ClientBuilder - Type-State Pattern
209// ============================================================================
210
211/// Type-state builder for OpenIGTLink clients
212///
213/// Uses compile-time type checking to ensure only valid client configurations
214/// can be constructed.
215///
216/// # Type Parameters
217/// * `Protocol` - Protocol state (Unspecified, TcpConfigured, UdpConfigured)
218/// * `Mode` - Mode state (Unspecified, SyncMode, AsyncMode)
219///
220/// # Examples
221///
222/// ```no_run
223/// use openigtlink_rust::io::builder::ClientBuilder;
224///
225/// let client = ClientBuilder::new()
226/// .tcp("127.0.0.1:18944")
227/// .sync()
228/// .build()?;
229/// # Ok::<(), openigtlink_rust::error::IgtlError>(())
230/// ```
231pub struct ClientBuilder<Protocol = Unspecified, Mode = Unspecified> {
232 protocol: Protocol,
233 mode: PhantomData<Mode>,
234 tls_config: Option<Arc<rustls::ClientConfig>>,
235 reconnect_config: Option<ReconnectConfig>,
236 verify_crc: bool,
237}
238
239// ============================================================================
240// Initial Construction
241// ============================================================================
242
243impl ClientBuilder<Unspecified, Unspecified> {
244 /// Create a new client builder
245 ///
246 /// This is the starting point for building any OpenIGTLink client.
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// use openigtlink_rust::io::builder::ClientBuilder;
252 ///
253 /// let builder = ClientBuilder::new();
254 /// ```
255 pub fn new() -> Self {
256 Self {
257 protocol: Unspecified,
258 mode: PhantomData,
259 tls_config: None,
260 reconnect_config: None,
261 verify_crc: true,
262 }
263 }
264}
265
266impl Default for ClientBuilder<Unspecified, Unspecified> {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272// ============================================================================
273// Protocol Selection
274// ============================================================================
275
276impl ClientBuilder<Unspecified, Unspecified> {
277 /// Select TCP protocol
278 ///
279 /// # Arguments
280 /// * `addr` - Server address (e.g., "127.0.0.1:18944")
281 ///
282 /// # Examples
283 ///
284 /// ```
285 /// use openigtlink_rust::io::builder::ClientBuilder;
286 ///
287 /// let builder = ClientBuilder::new()
288 /// .tcp("127.0.0.1:18944");
289 /// ```
290 pub fn tcp(self, addr: impl Into<String>) -> ClientBuilder<TcpConfigured, Unspecified> {
291 ClientBuilder {
292 protocol: TcpConfigured { addr: addr.into() },
293 mode: PhantomData,
294 tls_config: self.tls_config,
295 reconnect_config: self.reconnect_config,
296 verify_crc: self.verify_crc,
297 }
298 }
299
300 /// Select UDP protocol
301 ///
302 /// Note: UDP automatically sets mode to SyncMode as UDP only supports
303 /// synchronous operation.
304 ///
305 /// # Arguments
306 /// * `addr` - Server address (e.g., "127.0.0.1:18944")
307 ///
308 /// # Examples
309 ///
310 /// ```
311 /// use openigtlink_rust::io::builder::ClientBuilder;
312 ///
313 /// let builder = ClientBuilder::new()
314 /// .udp("127.0.0.1:18944");
315 /// ```
316 pub fn udp(self, addr: impl Into<String>) -> ClientBuilder<UdpConfigured, SyncMode> {
317 ClientBuilder {
318 protocol: UdpConfigured { addr: addr.into() },
319 mode: PhantomData,
320 tls_config: self.tls_config,
321 reconnect_config: self.reconnect_config,
322 verify_crc: self.verify_crc,
323 }
324 }
325}
326
327// ============================================================================
328// Mode Selection (TCP only)
329// ============================================================================
330
331impl ClientBuilder<TcpConfigured, Unspecified> {
332 /// Select synchronous (blocking) mode
333 ///
334 /// # Examples
335 ///
336 /// ```
337 /// use openigtlink_rust::io::builder::ClientBuilder;
338 ///
339 /// let builder = ClientBuilder::new()
340 /// .tcp("127.0.0.1:18944")
341 /// .sync();
342 /// ```
343 pub fn sync(self) -> ClientBuilder<TcpConfigured, SyncMode> {
344 ClientBuilder {
345 protocol: self.protocol,
346 mode: PhantomData,
347 tls_config: self.tls_config,
348 reconnect_config: self.reconnect_config,
349 verify_crc: self.verify_crc,
350 }
351 }
352
353 /// Select asynchronous (non-blocking) mode
354 ///
355 /// # Examples
356 ///
357 /// ```
358 /// use openigtlink_rust::io::builder::ClientBuilder;
359 ///
360 /// let builder = ClientBuilder::new()
361 /// .tcp("127.0.0.1:18944")
362 /// .async_mode();
363 /// ```
364 pub fn async_mode(self) -> ClientBuilder<TcpConfigured, AsyncMode> {
365 ClientBuilder {
366 protocol: self.protocol,
367 mode: PhantomData,
368 tls_config: self.tls_config,
369 reconnect_config: self.reconnect_config,
370 verify_crc: self.verify_crc,
371 }
372 }
373}
374
375// ============================================================================
376// TCP Sync Mode Configuration and Build
377// ============================================================================
378
379impl ClientBuilder<TcpConfigured, SyncMode> {
380 /// Build a synchronous TCP client
381 ///
382 /// # Errors
383 ///
384 /// Returns error if connection fails
385 ///
386 /// # Examples
387 ///
388 /// ```no_run
389 /// use openigtlink_rust::io::builder::ClientBuilder;
390 ///
391 /// let client = ClientBuilder::new()
392 /// .tcp("127.0.0.1:18944")
393 /// .sync()
394 /// .build()?;
395 /// # Ok::<(), openigtlink_rust::error::IgtlError>(())
396 /// ```
397 pub fn build(self) -> Result<SyncIgtlClient> {
398 let mut client = SyncTcpClient::connect(&self.protocol.addr)?;
399 client.set_verify_crc(self.verify_crc);
400 Ok(SyncIgtlClient::TcpSync(client))
401 }
402}
403
404// ============================================================================
405// TCP Async Mode Configuration and Build
406// ============================================================================
407
408impl ClientBuilder<TcpConfigured, AsyncMode> {
409 /// Configure TLS encryption
410 ///
411 /// # Arguments
412 /// * `config` - TLS client configuration
413 ///
414 /// # Examples
415 ///
416 /// ```no_run
417 /// use openigtlink_rust::io::builder::ClientBuilder;
418 /// use std::sync::Arc;
419 ///
420 /// # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
421 /// let tls_config = rustls::ClientConfig::builder()
422 /// .with_root_certificates(rustls::RootCertStore::empty())
423 /// .with_no_client_auth();
424 ///
425 /// let client = ClientBuilder::new()
426 /// .tcp("127.0.0.1:18944")
427 /// .async_mode()
428 /// .with_tls(Arc::new(tls_config))
429 /// .build()
430 /// .await?;
431 /// # Ok(())
432 /// # }
433 /// ```
434 pub fn with_tls(mut self, config: Arc<rustls::ClientConfig>) -> Self {
435 self.tls_config = Some(config);
436 self
437 }
438
439 /// Configure automatic reconnection
440 ///
441 /// # Arguments
442 /// * `config` - Reconnection strategy configuration
443 ///
444 /// # Examples
445 ///
446 /// ```no_run
447 /// use openigtlink_rust::io::builder::ClientBuilder;
448 /// use openigtlink_rust::io::reconnect::ReconnectConfig;
449 ///
450 /// # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
451 /// let client = ClientBuilder::new()
452 /// .tcp("127.0.0.1:18944")
453 /// .async_mode()
454 /// .with_reconnect(ReconnectConfig::default())
455 /// .build()
456 /// .await?;
457 /// # Ok(())
458 /// # }
459 /// ```
460 pub fn with_reconnect(mut self, config: ReconnectConfig) -> Self {
461 self.reconnect_config = Some(config);
462 self
463 }
464
465 /// Build an asynchronous TCP client
466 ///
467 /// Creates the appropriate client variant based on configured options:
468 /// - No options: Plain async TCP client
469 /// - TLS only: TLS-encrypted async client
470 /// - Reconnect only: Auto-reconnecting async client
471 /// - TLS + Reconnect: TLS-encrypted auto-reconnecting client
472 ///
473 /// # Errors
474 ///
475 /// Returns error if connection fails
476 ///
477 /// # Examples
478 ///
479 /// ```no_run
480 /// use openigtlink_rust::io::builder::ClientBuilder;
481 ///
482 /// # async fn example() -> Result<(), openigtlink_rust::error::IgtlError> {
483 /// // Plain async client
484 /// let client = ClientBuilder::new()
485 /// .tcp("127.0.0.1:18944")
486 /// .async_mode()
487 /// .build()
488 /// .await?;
489 /// # Ok(())
490 /// # }
491 /// ```
492 pub async fn build(self) -> Result<AsyncIgtlClient> {
493 let addr = self.protocol.addr;
494
495 // Create base client (with or without TLS)
496 let mut client = if let Some(tls_config) = self.tls_config {
497 // TLS connection
498 let (hostname, port) = parse_addr(&addr)?;
499 UnifiedAsyncClient::connect_with_tls(&hostname, port, tls_config).await?
500 } else {
501 // Plain TCP connection
502 UnifiedAsyncClient::connect(&addr).await?
503 };
504
505 // Add reconnection if configured
506 if let Some(reconnect_config) = self.reconnect_config {
507 client = client.with_reconnect(reconnect_config);
508 }
509
510 // Set CRC verification
511 client.set_verify_crc(self.verify_crc);
512
513 Ok(AsyncIgtlClient::Unified(client))
514 }
515}
516
517// ============================================================================
518// Common Configuration Methods
519// ============================================================================
520
521impl<Protocol, Mode> ClientBuilder<Protocol, Mode> {
522 /// Enable or disable CRC verification for received messages
523 ///
524 /// Default: true (CRC verification enabled)
525 ///
526 /// # Arguments
527 /// * `verify` - true to enable CRC verification, false to disable
528 ///
529 /// # Examples
530 ///
531 /// ```
532 /// use openigtlink_rust::io::builder::ClientBuilder;
533 ///
534 /// let builder = ClientBuilder::new()
535 /// .tcp("127.0.0.1:18944")
536 /// .sync()
537 /// .verify_crc(false);
538 /// ```
539 pub fn verify_crc(mut self, verify: bool) -> Self {
540 self.verify_crc = verify;
541 self
542 }
543}
544
545// ============================================================================
546// Helper Functions
547// ============================================================================
548
549/// Parse address string into hostname and port
550///
551/// # Arguments
552/// * `addr` - Address string in format "hostname:port"
553///
554/// # Returns
555/// Tuple of (hostname, port)
556fn parse_addr(addr: &str) -> Result<(String, u16)> {
557 let parts: Vec<&str> = addr.rsplitn(2, ':').collect();
558 if parts.len() != 2 {
559 return Err(crate::error::IgtlError::Io(std::io::Error::new(
560 std::io::ErrorKind::InvalidInput,
561 format!("Invalid address format: {}", addr),
562 )));
563 }
564
565 let port = parts[0].parse::<u16>().map_err(|e| {
566 crate::error::IgtlError::Io(std::io::Error::new(
567 std::io::ErrorKind::InvalidInput,
568 format!("Invalid port number: {}", e),
569 ))
570 })?;
571
572 let hostname = parts[1].to_string();
573
574 // Validate hostname is not empty
575 if hostname.is_empty() {
576 return Err(crate::error::IgtlError::Io(std::io::Error::new(
577 std::io::ErrorKind::InvalidInput,
578 "Hostname cannot be empty",
579 )));
580 }
581
582 Ok((hostname, port))
583}
584
585// ============================================================================
586// UDP Configuration and Build
587// ============================================================================
588
589impl ClientBuilder<UdpConfigured, SyncMode> {
590 /// Build a UDP client
591 ///
592 /// UDP clients use a connectionless protocol and require specifying
593 /// the target address for each send operation using `send_to()`.
594 ///
595 /// # Errors
596 ///
597 /// Returns error if binding to local address fails
598 ///
599 /// # Examples
600 ///
601 /// ```no_run
602 /// use openigtlink_rust::io::builder::ClientBuilder;
603 /// use openigtlink_rust::protocol::types::TransformMessage;
604 /// use openigtlink_rust::protocol::message::IgtlMessage;
605 ///
606 /// let client = ClientBuilder::new()
607 /// .udp("0.0.0.0:0")
608 /// .build()?;
609 ///
610 /// let transform = TransformMessage::identity();
611 /// let msg = IgtlMessage::new(transform, "Device")?;
612 /// client.send_to(&msg, "127.0.0.1:18944")?;
613 /// # Ok::<(), openigtlink_rust::error::IgtlError>(())
614 /// ```
615 pub fn build(self) -> Result<UdpClient> {
616 UdpClient::bind(&self.protocol.addr)
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_phantom_data_is_zero_size() {
626 use std::mem::size_of;
627
628 // PhantomData should have zero size
629 assert_eq!(size_of::<PhantomData<SyncMode>>(), 0);
630 assert_eq!(size_of::<PhantomData<AsyncMode>>(), 0);
631
632 // Ensure the builder overhead is minimal
633 let base_size = size_of::<ClientBuilder<Unspecified, Unspecified>>();
634
635 // All builder variants should have similar size (protocol state adds addr String)
636 // but Mode type parameter should add zero cost
637 let tcp_unspecified = size_of::<ClientBuilder<TcpConfigured, Unspecified>>();
638 let tcp_sync = size_of::<ClientBuilder<TcpConfigured, SyncMode>>();
639 let tcp_async = size_of::<ClientBuilder<TcpConfigured, AsyncMode>>();
640
641 // Mode change should not increase size
642 assert_eq!(tcp_unspecified, tcp_sync);
643 assert_eq!(tcp_unspecified, tcp_async);
644
645 // Protocol state adds String, so should be larger than base
646 assert!(tcp_unspecified > base_size);
647 }
648
649 #[test]
650 fn test_builder_state_transitions() {
651 // Initial state
652 let builder = ClientBuilder::new();
653
654 // TCP protocol selection
655 let builder = builder.tcp("127.0.0.1:18944");
656
657 // Mode selection
658 let _sync_builder = builder.sync();
659
660 // Restart for async test
661 let builder = ClientBuilder::new().tcp("127.0.0.1:18944");
662 let _async_builder = builder.async_mode();
663
664 // UDP automatically sets sync mode
665 let _udp_builder = ClientBuilder::new().udp("127.0.0.1:18944");
666 }
667
668 #[test]
669 fn test_parse_addr() {
670 // Valid addresses
671 assert_eq!(
672 parse_addr("localhost:18944").unwrap(),
673 ("localhost".to_string(), 18944)
674 );
675 assert_eq!(
676 parse_addr("127.0.0.1:8080").unwrap(),
677 ("127.0.0.1".to_string(), 8080)
678 );
679 assert_eq!(
680 parse_addr("example.com:443").unwrap(),
681 ("example.com".to_string(), 443)
682 );
683
684 // Invalid addresses
685 assert!(parse_addr("invalid").is_err());
686 assert!(parse_addr("localhost:").is_err());
687 assert!(parse_addr(":18944").is_err());
688 assert!(parse_addr("localhost:abc").is_err());
689 }
690
691 #[test]
692 fn test_builder_options() {
693 // Verify CRC option
694 let builder = ClientBuilder::new()
695 .tcp("127.0.0.1:18944")
696 .sync()
697 .verify_crc(false);
698 assert!(!builder.verify_crc);
699
700 // TLS option
701 let tls_config = Arc::new(
702 rustls::ClientConfig::builder()
703 .with_root_certificates(rustls::RootCertStore::empty())
704 .with_no_client_auth(),
705 );
706 let builder = ClientBuilder::new()
707 .tcp("127.0.0.1:18944")
708 .async_mode()
709 .with_tls(tls_config.clone());
710 assert!(builder.tls_config.is_some());
711
712 // Reconnect option
713 let builder = ClientBuilder::new()
714 .tcp("127.0.0.1:18944")
715 .async_mode()
716 .with_reconnect(ReconnectConfig::default());
717 assert!(builder.reconnect_config.is_some());
718 }
719}