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}