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