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}