suika_wasm/
lib.rs

1use suika_server::{
2    middleware::{Middleware, MiddlewareFuture, Next},
3    request::Request,
4    response::Response,
5};
6
7const WASM_BINARY: &[u8] = include_bytes!("../wasm/suika_ui_bg.wasm");
8const JS_FILE: &str = include_str!("../wasm/suika_ui.js");
9
10/// A middleware component for serving WebAssembly and JavaScript files.
11///
12/// This middleware serves the `suika_ui_bg.wasm` and `suika_ui.js` files when
13/// the request path matches the specified URL prefix. For other paths, it
14/// passes the request to the next middleware in the stack.
15///
16/// # Examples
17///
18/// ```
19/// use suika_server::request::Request;
20/// use suika_server::response::Response;
21/// use suika_server::middleware::{Middleware, Next, MiddlewareFuture};
22/// use suika_wasm::WasmFileMiddleware;
23/// use std::collections::HashMap;
24/// use std::sync::{Arc, Mutex};
25/// use tokio::sync::Mutex as TokioMutex;
26///
27/// #[derive(Clone)]
28/// struct MockNextMiddleware {
29///     called: Arc<TokioMutex<bool>>,
30/// }
31///
32/// impl MockNextMiddleware {
33///     fn new() -> Self {
34///         Self {
35///             called: Arc::new(TokioMutex::new(false)),
36///         }
37///     }
38/// }
39///
40/// impl Middleware for MockNextMiddleware {
41///     fn handle<'a>(
42///         &'a self,
43///         _req: &'a mut Request,
44///         _res: &'a mut Response,
45///         _next: Next<'a>,
46///     ) -> MiddlewareFuture<'a> {
47///         let called = Arc::clone(&self.called);
48///         Box::pin(async move {
49///             let mut called_lock = called.lock().await;
50///             *called_lock = true;
51///             Ok(())
52///         })
53///     }
54/// }
55///
56/// #[tokio::main]
57/// async fn main() {
58///     let mut req = Request::new(
59///         "GET /static/suika_ui_bg.wasm HTTP/1.1\r\n\r\n",
60///         Arc::new(Mutex::new(HashMap::new())),
61///     ).unwrap();
62/// 
63///     let mut res = Response::new(None);
64///
65///     let wasm_file_middleware = WasmFileMiddleware::new("/static", 3600);
66///     let next_middleware = MockNextMiddleware::new();
67///     let middleware_stack: Vec<Arc<dyn Middleware + Send + Sync>> = vec![Arc::new(next_middleware.clone())];
68///     let next = Next::new(middleware_stack.as_slice());
69///
70///     wasm_file_middleware.handle(&mut req, &mut res, next.clone()).await.unwrap();
71///     let inner = res.get_inner().await;
72///
73///     assert_eq!(inner.status_code(), Some(200));
74///     assert_eq!(inner.headers().get("Content-Type"), Some(&"application/wasm".to_string()));
75/// }
76/// ```
77pub struct WasmFileMiddleware {
78    url_prefix: &'static str,
79    cache_duration: u64,
80}
81
82impl WasmFileMiddleware {
83    /// Creates a new `WasmFileMiddleware`.
84    ///
85    /// # Arguments
86    ///
87    /// * `url_prefix` - The URL prefix for serving WebAssembly and JavaScript files.
88    /// * `cache_duration` - The cache duration in seconds.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use suika_wasm::WasmFileMiddleware;
94    ///
95    /// let wasm_file_middleware = WasmFileMiddleware::new("/static", 3600);
96    /// ```
97    pub fn new(url_prefix: &'static str, cache_duration: u64) -> Self {
98        Self {
99            url_prefix,
100            cache_duration,
101        }
102    }
103}
104
105impl Middleware for WasmFileMiddleware {
106    /// Handles an incoming HTTP request by serving the `.wasm` or `.js` file if the request path matches the URL prefix.
107    ///
108    /// # Arguments
109    ///
110    /// * `req` - A mutable reference to the incoming request.
111    /// * `res` - A mutable reference to the response to be sent.
112    /// * `next` - The next middleware in the stack.
113    ///
114    /// # Returns
115    ///
116    /// A future that resolves to a `Result<(), HttpError>`.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use suika_server::request::Request;
122    /// use suika_server::response::Response;
123    /// use suika_server::middleware::{Middleware, Next, MiddlewareFuture};
124    /// use suika_wasm::WasmFileMiddleware;
125    /// use std::collections::HashMap;
126    /// use std::sync::{Arc, Mutex};
127    /// use tokio::sync::Mutex as TokioMutex;
128    ///
129    /// #[derive(Clone)]
130    /// struct MockNextMiddleware {
131    ///     called: Arc<TokioMutex<bool>>,
132    /// }
133    ///
134    /// impl MockNextMiddleware {
135    ///     fn new() -> Self {
136    ///         Self {
137    ///             called: Arc::new(TokioMutex::new(false)),
138    ///         }
139    ///     }
140    /// }
141    ///
142    /// impl Middleware for MockNextMiddleware {
143    ///     fn handle<'a>(
144    ///         &'a self,
145    ///         _req: &'a mut Request,
146    ///         _res: &'a mut Response,
147    ///         _next: Next<'a>,
148    ///     ) -> MiddlewareFuture<'a> {
149    ///         let called = Arc::clone(&self.called);
150    ///         Box::pin(async move {
151    ///             let mut called_lock = called.lock().await;
152    ///             *called_lock = true;
153    ///             Ok(())
154    ///         })
155    ///     }
156    /// }
157    ///
158    /// #[tokio::main]
159    /// async fn main() {
160    ///     let mut req = Request::new(
161    ///         "GET /static/suika_ui.js HTTP/1.1\r\n\r\n",
162    ///         Arc::new(Mutex::new(HashMap::new())),
163    ///     ).unwrap();
164    /// 
165    ///     let mut res = Response::new(None);
166    ///
167    ///     let wasm_file_middleware = WasmFileMiddleware::new("/static", 3600);
168    ///     let next_middleware = MockNextMiddleware::new();
169    ///     let middleware_stack: Vec<Arc<dyn Middleware + Send + Sync>> = vec![Arc::new(next_middleware.clone())];
170    ///     let next = Next::new(middleware_stack.as_slice());
171    ///
172    ///     wasm_file_middleware.handle(&mut req, &mut res, next.clone()).await.unwrap();
173    ///     let inner = res.get_inner().await;
174    ///
175    ///     assert_eq!(inner.status_code(), Some(200));
176    ///     assert_eq!(inner.headers().get("Content-Type"), Some(&"application/javascript".to_string()));
177    /// }
178    /// ```
179    fn handle<'a>(
180        &'a self,
181        req: &'a mut Request,
182        res: &'a mut Response,
183        mut next: Next<'a>,
184    ) -> MiddlewareFuture<'a> {
185        let path = req.path().to_string();
186        let url_prefix = self.url_prefix.to_string();
187        let cache_duration = self.cache_duration;
188
189        Box::pin(async move {
190            if path == format!("{}/suika_ui_bg.wasm", url_prefix) {
191                res.header("Content-Type", "application/wasm").await;
192                res.header(
193                    "Cache-Control",
194                    &format!("public, max-age={}", cache_duration),
195                )
196                .await;
197                res.set_status(200).await;
198                res.body_bytes(WASM_BINARY.to_vec()).await;
199                Ok(())
200            } else if path == format!("{}/suika_ui.js", url_prefix) {
201                res.header("Content-Type", "application/javascript").await;
202                res.header(
203                    "Cache-Control",
204                    &format!("public, max-age={}", cache_duration),
205                )
206                .await;
207                res.set_status(200).await;
208                res.body(JS_FILE.to_string()).await;
209                Ok(())
210            } else {
211                next.run(req, res).await
212            }
213        })
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use std::collections::HashMap;
221    use std::sync::{Arc, Mutex};
222    use suika_server::middleware::{Middleware, Next};
223    use suika_server::request::Request;
224    use suika_server::response::{Body, Response};
225    use tokio::sync::Mutex as TokioMutex;
226
227    // Mock Next middleware
228    #[derive(Clone)]
229    struct MockNextMiddleware {
230        called: Arc<TokioMutex<bool>>,
231    }
232
233    impl MockNextMiddleware {
234        fn new() -> Self {
235            Self {
236                called: Arc::new(TokioMutex::new(false)),
237            }
238        }
239    }
240
241    impl Middleware for MockNextMiddleware {
242        fn handle<'a>(
243            &'a self,
244            _req: &'a mut Request,
245            _res: &'a mut Response,
246            _next: Next<'a>,
247        ) -> MiddlewareFuture<'a> {
248            let called = Arc::clone(&self.called);
249            Box::pin(async move {
250                let mut called_lock = called.lock().await;
251                *called_lock = true;
252                Ok(())
253            })
254        }
255    }
256
257    #[tokio::test]
258    async fn test_wasm_file_middleware_serves_wasm_file() {
259        let mut req = Request::new(
260            "GET /static/suika_ui_bg.wasm HTTP/1.1\r\n\r\n",
261            Arc::new(Mutex::new(HashMap::new())),
262        )
263        .unwrap();
264
265        let mut res = Response::new(None);
266
267        let wasm_file_middleware = WasmFileMiddleware::new("/static", 3600);
268        let next_middleware = MockNextMiddleware::new();
269        let middleware_stack: Vec<Arc<dyn Middleware + Send + Sync>> =
270            vec![Arc::new(next_middleware.clone())];
271        let next = Next::new(middleware_stack.as_slice());
272
273        wasm_file_middleware
274            .handle(&mut req, &mut res, next.clone())
275            .await
276            .unwrap();
277
278        let inner = res.get_inner().await;
279        assert_eq!(inner.status_code(), Some(200));
280        assert_eq!(
281            inner.headers().get("Content-Type"),
282            Some(&"application/wasm".to_string())
283        );
284        assert_eq!(inner.body(), &Some(Body::Binary(WASM_BINARY.to_vec())));
285
286        let next_called = *next_middleware.called.lock().await;
287        assert!(!next_called);
288    }
289
290    #[tokio::test]
291    async fn test_wasm_file_middleware_serves_js_file() {
292        let mut req = Request::new(
293            "GET /static/suika_ui.js HTTP/1.1\r\n\r\n",
294            Arc::new(Mutex::new(HashMap::new())),
295        )
296        .unwrap();
297
298        let mut res = Response::new(None);
299
300        let wasm_file_middleware = WasmFileMiddleware::new("/static", 3600);
301        let next_middleware = MockNextMiddleware::new();
302        let middleware_stack: Vec<Arc<dyn Middleware + Send + Sync>> =
303            vec![Arc::new(next_middleware.clone())];
304        let next = Next::new(middleware_stack.as_slice());
305
306        wasm_file_middleware
307            .handle(&mut req, &mut res, next.clone())
308            .await
309            .unwrap();
310
311        let inner = res.get_inner().await;
312        assert_eq!(inner.status_code(), Some(200));
313        assert_eq!(
314            inner.headers().get("Content-Type"),
315            Some(&"application/javascript".to_string())
316        );
317        assert_eq!(inner.body(), &Some(Body::Text(JS_FILE.to_string())));
318
319        let next_called = *next_middleware.called.lock().await;
320        assert!(!next_called);
321    }
322
323    #[tokio::test]
324    async fn test_wasm_file_middleware_passes_other_paths() {
325        let mut req = Request::new(
326            "GET /other/path HTTP/1.1\r\n\r\n",
327            Arc::new(Mutex::new(HashMap::new())),
328        )
329        .unwrap();
330
331        let mut res = Response::new(None);
332
333        let wasm_file_middleware = WasmFileMiddleware::new("/static", 3600);
334        let next_middleware = MockNextMiddleware::new();
335        let middleware_stack: Vec<Arc<dyn Middleware + Send + Sync>> =
336            vec![Arc::new(next_middleware.clone())];
337        let next = Next::new(middleware_stack.as_slice());
338
339        wasm_file_middleware
340            .handle(&mut req, &mut res, next.clone())
341            .await
342            .unwrap();
343
344        let next_called = *next_middleware.called.lock().await;
345        assert!(next_called);
346    }
347}