sz_rust_core/
websocket_route.rs1use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
38use std::sync::Arc;
39
40pub trait WsHandler: Send + Sync + 'static {
49 fn on_connect(&self) {}
53
54 fn on_message(&self, _msg: Message) -> Option<Message> {
59 None
60 }
61
62 fn on_close(&self) {}
66}
67
68pub fn ws_handler<H: WsHandler>(handler: H) -> axum::routing::MethodRouter<()> {
82 let handler = Arc::new(handler);
83 axum::routing::get(move |ws: WebSocketUpgrade| async move {
84 let handler = handler.clone();
85 ws.on_upgrade(move |socket| handle_ws_connection(socket, handler))
86 })
87}
88
89async fn handle_ws_connection(mut socket: WebSocket, handler: Arc<dyn WsHandler>) {
93 handler.on_connect();
94
95 while let Some(Ok(msg)) = socket.recv().await {
97 if matches!(msg, Message::Close(_)) {
99 break;
100 }
101
102 if let Some(reply) = handler.on_message(msg) {
103 if socket.send(reply).await.is_err() {
105 break;
106 }
107 }
108 }
109
110 handler.on_close();
111}
112
113#[derive(Debug, Default, Clone)]
121pub struct EchoWsHandler;
122
123impl EchoWsHandler {
124 pub fn new() -> Self {
126 Self
127 }
128}
129
130impl WsHandler for EchoWsHandler {
131 fn on_message(&self, msg: Message) -> Option<Message> {
132 Some(msg)
133 }
134}
135
136#[cfg(test)]
141mod tests {
142 use super::*;
143 use axum::body::Body;
144 use axum::http::{Method, Request, StatusCode};
145 use tower::ServiceExt;
146
147 #[test]
149 fn test_echo_handler_returns_message() {
150 let handler = EchoWsHandler::new();
151 let msg = Message::text("hello");
152 let result = handler.on_message(msg);
153 assert!(result.is_some());
154 }
155
156 #[test]
158 fn test_echo_handler_default() {
159 let handler = EchoWsHandler;
160 let msg = Message::text("test");
161 assert!(handler.on_message(msg).is_some());
162 }
163
164 struct NoReplyHandler;
166 impl WsHandler for NoReplyHandler {
167 fn on_message(&self, _msg: Message) -> Option<Message> {
168 None
169 }
170 }
171
172 #[test]
173 fn test_custom_handler_no_reply() {
174 let handler = NoReplyHandler;
175 let msg = Message::text("hello");
176 assert!(handler.on_message(msg).is_none());
177 }
178
179 struct PrefixHandler;
181 impl WsHandler for PrefixHandler {
182 fn on_message(&self, _msg: Message) -> Option<Message> {
183 Some(Message::text("prefix: reply"))
184 }
185 }
186
187 #[test]
188 fn test_custom_handler_with_reply() {
189 let handler = PrefixHandler;
190 let msg = Message::text("input");
191 let reply = handler.on_message(msg).unwrap();
192 assert_eq!(reply.to_text().unwrap(), "prefix: reply");
193 }
194
195 #[test]
197 fn test_default_lifecycle_hooks_no_panic() {
198 let handler = EchoWsHandler::new();
199 handler.on_connect();
200 handler.on_close();
201 }
202
203 #[tokio::test]
208 async fn test_ws_route_registered_as_get() {
209 let router = axum::Router::new().route("/ws/echo", ws_handler(EchoWsHandler::new()));
210
211 let request = Request::builder()
213 .method(Method::GET)
214 .uri("/ws/echo")
215 .body(Body::empty())
216 .unwrap();
217 let response = router.oneshot(request).await.unwrap();
218 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
220 }
221
222 #[tokio::test]
224 async fn test_ws_route_not_found() {
225 let router = axum::Router::new().route("/ws/echo", ws_handler(EchoWsHandler::new()));
226
227 let request = Request::builder()
228 .method(Method::GET)
229 .uri("/ws/nonexistent")
230 .body(Body::empty())
231 .unwrap();
232 let response = router.oneshot(request).await.unwrap();
233 assert_eq!(response.status(), StatusCode::NOT_FOUND);
234 }
235
236 #[tokio::test]
238 async fn test_ws_route_rejects_post() {
239 let router = axum::Router::new().route("/ws/echo", ws_handler(EchoWsHandler::new()));
240
241 let request = Request::builder()
242 .method(Method::POST)
243 .uri("/ws/echo")
244 .body(Body::empty())
245 .unwrap();
246 let response = router.oneshot(request).await.unwrap();
247 assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
248 }
249}