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}