Skip to main content

mockforge_core/
protocol_server.rs

1//! Protocol server lifecycle trait for uniform server startup and shutdown.
2//!
3//! This module provides the [`MockProtocolServer`] trait, which abstracts the
4//! lifecycle management of mock protocol servers. Each protocol crate (gRPC, FTP,
5//! TCP, SMTP, etc.) can implement this trait to provide a uniform startup interface,
6//! enabling the CLI to launch all protocols through a single, consistent code path.
7//!
8//! # Design
9//!
10//! The trait is intentionally minimal — it covers the core lifecycle operations
11//! (start, shutdown, identification) without imposing protocol-specific details.
12//! Protocol crates wrap their existing server startup logic in a struct that
13//! implements this trait; the actual server code remains unchanged.
14//!
15//! # Example
16//!
17//! ```rust,no_run
18//! use mockforge_core::protocol_server::MockProtocolServer;
19//! use mockforge_core::protocol_abstraction::Protocol;
20//! use async_trait::async_trait;
21//!
22//! struct MyServer { port: u16 }
23//!
24//! #[async_trait]
25//! impl MockProtocolServer for MyServer {
26//!     fn protocol(&self) -> Protocol { Protocol::Tcp }
27//!     async fn start(
28//!         &self,
29//!         shutdown: tokio::sync::watch::Receiver<()>,
30//!     ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
31//!         // Run until shutdown signal
32//!         let _ = shutdown;
33//!         Ok(())
34//!     }
35//!     fn port(&self) -> u16 { self.port }
36//!     fn description(&self) -> String {
37//!         format!("TCP server on port {}", self.port)
38//!     }
39//! }
40//! ```
41
42use crate::protocol_abstraction::Protocol;
43use async_trait::async_trait;
44
45/// Trait for mock protocol server lifecycle management.
46///
47/// Each protocol crate implements this to provide a uniform startup interface.
48/// The CLI can collect `Box<dyn MockProtocolServer>` instances and launch them
49/// all through a single code path, rather than having bespoke startup logic
50/// for each protocol.
51///
52/// Implementations should:
53/// - Wrap existing server startup code (not rewrite it)
54/// - Run until the shutdown signal is received in [`start`](MockProtocolServer::start)
55/// - Return errors from [`start`](MockProtocolServer::start) if the server fails to bind or encounters a fatal error
56#[async_trait]
57pub trait MockProtocolServer: Send + Sync {
58    /// Which protocol this server handles.
59    fn protocol(&self) -> Protocol;
60
61    /// Start the server, running until the shutdown signal is received.
62    ///
63    /// The server should listen on its configured address and handle requests
64    /// until the `shutdown` receiver signals (i.e., the sender is dropped or
65    /// a value is sent). Implementations should use `tokio::select!` to
66    /// combine the server's accept loop with the shutdown signal.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the server fails to bind, encounters a fatal I/O
71    /// error, or any other unrecoverable condition.
72    async fn start(
73        &self,
74        shutdown: tokio::sync::watch::Receiver<()>,
75    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
76
77    /// The port this server is listening on.
78    fn port(&self) -> u16;
79
80    /// Human-readable description for logging (e.g., "gRPC server on port 50051").
81    fn description(&self) -> String;
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    struct DummyServer {
89        port: u16,
90    }
91
92    #[async_trait]
93    impl MockProtocolServer for DummyServer {
94        fn protocol(&self) -> Protocol {
95            Protocol::Tcp
96        }
97
98        async fn start(
99            &self,
100            mut shutdown: tokio::sync::watch::Receiver<()>,
101        ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
102            // Wait for shutdown
103            let _ = shutdown.changed().await;
104            Ok(())
105        }
106
107        fn port(&self) -> u16 {
108            self.port
109        }
110
111        fn description(&self) -> String {
112            format!("Dummy TCP server on port {}", self.port)
113        }
114    }
115
116    #[test]
117    fn test_protocol_server_trait_object() {
118        let server: Box<dyn MockProtocolServer> = Box::new(DummyServer { port: 9999 });
119        assert_eq!(server.protocol(), Protocol::Tcp);
120        assert_eq!(server.port(), 9999);
121        assert_eq!(server.description(), "Dummy TCP server on port 9999");
122    }
123
124    #[tokio::test]
125    async fn test_protocol_server_shutdown() {
126        let server = DummyServer { port: 8080 };
127        let (tx, rx) = tokio::sync::watch::channel(());
128
129        let handle = tokio::spawn(async move { server.start(rx).await });
130
131        // Signal shutdown
132        drop(tx);
133
134        let result = handle.await.unwrap();
135        assert!(result.is_ok());
136    }
137
138    #[test]
139    fn test_protocol_server_description() {
140        let server = DummyServer { port: 50051 };
141        assert!(server.description().contains("50051"));
142    }
143}