mockforge_core/
server_utils.rs

1//! Common server utilities for MockForge
2
3use std::net::SocketAddr;
4
5/// Create a SocketAddr for server binding from host and port
6///
7/// # Arguments
8/// * `host` - Host address string (e.g., "127.0.0.1", "0.0.0.0", "example.com")
9/// * `port` - Port number
10///
11/// # Returns
12/// * `Ok(SocketAddr)` - Parsed socket address
13/// * `Err(String)` - Error message if parsing fails
14pub fn create_socket_addr(host: &str, port: u16) -> Result<SocketAddr, String> {
15    format!("{}:{}", host, port)
16        .parse()
17        .map_err(|e| format!("Invalid socket address {}:{}: {}", host, port, e))
18}
19
20/// Create a standard IPv4 localhost SocketAddr (127.0.0.1:port)
21///
22/// # Arguments
23/// * `port` - Port number to bind to
24pub fn localhost_socket_addr(port: u16) -> SocketAddr {
25    SocketAddr::from(([127, 0, 0, 1], port))
26}
27
28/// Create a standard IPv4 wildcard SocketAddr (0.0.0.0:port) to listen on all interfaces
29///
30/// # Arguments
31/// * `port` - Port number to bind to
32pub fn wildcard_socket_addr(port: u16) -> SocketAddr {
33    SocketAddr::from(([0, 0, 0, 0], port))
34}
35
36/// Server startup configuration for binding and listening
37#[derive(Debug, Clone)]
38pub struct ServerConfig {
39    /// Host address to bind to (e.g., "0.0.0.0" or "127.0.0.1")
40    pub host: String,
41    /// Port number to bind to
42    pub port: u16,
43    /// Type of server to start
44    pub server_type: ServerType,
45}
46
47/// Server type enumeration
48#[derive(Debug, Clone)]
49pub enum ServerType {
50    /// HTTP/REST server
51    HTTP,
52    /// WebSocket server
53    WebSocket,
54    /// gRPC server
55    GRPC,
56}
57
58impl ServerConfig {
59    /// Create a new server configuration
60    pub fn new(host: String, port: u16, server_type: ServerType) -> Self {
61        Self {
62            host,
63            port,
64            server_type,
65        }
66    }
67
68    /// Create HTTP server configuration
69    pub fn http(port: u16) -> Self {
70        Self::new("0.0.0.0".to_string(), port, ServerType::HTTP)
71    }
72
73    /// Create WebSocket server configuration
74    pub fn websocket(port: u16) -> Self {
75        Self::new("0.0.0.0".to_string(), port, ServerType::WebSocket)
76    }
77
78    /// Create gRPC server configuration
79    pub fn grpc(port: u16) -> Self {
80        Self::new("0.0.0.0".to_string(), port, ServerType::GRPC)
81    }
82
83    /// Get the socket address for this configuration
84    pub fn socket_addr(&self) -> Result<SocketAddr, String> {
85        create_socket_addr(&self.host, self.port)
86    }
87
88    /// Get a formatted server description
89    pub fn description(&self) -> String {
90        match self.server_type {
91            ServerType::HTTP => format!("HTTP server on {}:{}", self.host, self.port),
92            ServerType::WebSocket => format!("WebSocket server on {}:{}", self.host, self.port),
93            ServerType::GRPC => format!("gRPC server on {}:{}", self.host, self.port),
94        }
95    }
96}
97
98/// Common server traits for consistent startup behavior
99///
100/// This trait allows different server implementations (HTTP, WebSocket, gRPC)
101/// to be started using a unified interface.
102pub trait ServerStarter {
103    /// Get the server type
104    fn server_type(&self) -> ServerType;
105
106    /// Get the port this server will bind to
107    fn port(&self) -> u16;
108
109    /// Start the server (implementation-specific)
110    ///
111    /// Returns a future that resolves when the server is running or fails to start.
112    fn start_server(
113        self,
114    ) -> impl std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send;
115}
116
117/// Helper function to start any server that implements ServerStarter
118///
119/// Logs server startup information and handles server initialization.
120///
121/// # Arguments
122/// * `server` - Server instance implementing ServerStarter
123///
124/// # Returns
125/// Result indicating success or failure of server startup
126pub async fn start_server<S: ServerStarter>(
127    server: S,
128) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
129    let port = server.port();
130    let server_type = server.server_type();
131
132    match server_type {
133        ServerType::HTTP => tracing::info!("HTTP listening on port {}", port),
134        ServerType::WebSocket => tracing::info!("WebSocket listening on port {}", port),
135        ServerType::GRPC => tracing::info!("gRPC listening on port {}", port),
136    }
137
138    server.start_server().await
139}
140
141/// Server health check utilities
142pub mod health {
143    use serde::{Deserialize, Serialize};
144
145    /// Server health status information
146    #[derive(Debug, Serialize, Deserialize)]
147    pub struct HealthStatus {
148        /// Health status string (e.g., "healthy", "unhealthy: reason")
149        pub status: String,
150        /// ISO 8601 timestamp of the health check
151        pub timestamp: String,
152        /// Server uptime in seconds
153        pub uptime_seconds: u64,
154        /// Server version string
155        pub version: String,
156    }
157
158    impl HealthStatus {
159        /// Create a healthy status response
160        pub fn healthy(uptime_seconds: u64, version: &str) -> Self {
161            Self {
162                status: "healthy".to_string(),
163                timestamp: chrono::Utc::now().to_rfc3339(),
164                uptime_seconds,
165                version: version.to_string(),
166            }
167        }
168
169        /// Create an unhealthy status response with a reason
170        pub fn unhealthy(reason: &str, uptime_seconds: u64, version: &str) -> Self {
171            Self {
172                status: format!("unhealthy: {}", reason),
173                timestamp: chrono::Utc::now().to_rfc3339(),
174                uptime_seconds,
175                version: version.to_string(),
176            }
177        }
178    }
179}
180
181/// Common error response utilities
182pub mod errors {
183    use axum::{http::StatusCode, Json};
184    use serde_json::json;
185
186    /// Create a standard JSON error response for HTTP handlers
187    ///
188    /// # Arguments
189    /// * `status` - HTTP status code (e.g., 400, 500)
190    /// * `message` - Error message
191    ///
192    /// # Returns
193    /// Tuple of (status_code, JSON response) for use with Axum handlers
194    pub fn json_error(status: StatusCode, message: &str) -> (StatusCode, Json<serde_json::Value>) {
195        let error_response = json!({
196            "error": {
197                "message": message,
198                "status_code": status.as_u16()
199            },
200            "timestamp": chrono::Utc::now().to_rfc3339()
201        });
202
203        (status, Json(error_response))
204    }
205
206    /// Create a standard JSON success response for HTTP handlers
207    ///
208    /// # Arguments
209    /// * `data` - Serializable data to include in the response
210    ///
211    /// # Returns
212    /// Tuple of (HTTP 200 OK, JSON response) for use with Axum handlers
213    pub fn json_success<T: serde::Serialize>(data: T) -> (StatusCode, Json<serde_json::Value>) {
214        let success_response = json!({
215            "success": true,
216            "data": data,
217            "timestamp": chrono::Utc::now().to_rfc3339()
218        });
219
220        (StatusCode::OK, Json(success_response))
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_create_socket_addr() {
230        let addr = create_socket_addr("127.0.0.1", 9080).unwrap();
231        assert_eq!(addr.to_string(), "127.0.0.1:9080");
232    }
233
234    #[test]
235    fn test_server_config() {
236        let config = ServerConfig::http(3000);
237        assert_eq!(config.port, 3000);
238        assert_eq!(config.host, "0.0.0.0");
239        matches!(config.server_type, ServerType::HTTP);
240    }
241
242    #[test]
243    fn test_localhost_socket_addr() {
244        let addr = localhost_socket_addr(9080);
245        assert_eq!(addr.to_string(), "127.0.0.1:9080");
246    }
247
248    #[test]
249    fn test_wildcard_socket_addr() {
250        let addr = wildcard_socket_addr(9080);
251        assert_eq!(addr.to_string(), "0.0.0.0:9080");
252    }
253}