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}