1use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub enum OptLevel {
11 #[default]
13 Debug,
14 Release,
16 Size,
18 MinSize,
20}
21
22impl OptLevel {
23 #[must_use]
25 pub fn as_str(&self) -> &'static str {
26 match self {
27 OptLevel::Debug => "0",
28 OptLevel::Release => "3",
29 OptLevel::Size => "s",
30 OptLevel::MinSize => "z",
31 }
32 }
33
34 #[must_use]
36 pub fn is_release(&self) -> bool {
37 !matches!(self, OptLevel::Debug)
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct WasmRunnerConfig {
44 pub http_port: u16,
46 pub ws_port: u16,
48 pub hot_reload: bool,
50 pub preserve_state: bool,
52 pub source_maps: bool,
54 pub opt_level: OptLevel,
56 pub watch_patterns: Vec<String>,
58 pub output_dir: PathBuf,
60 pub static_dir: Option<PathBuf>,
62 pub wasm_filename: String,
64}
65
66impl Default for WasmRunnerConfig {
67 fn default() -> Self {
68 Self {
69 http_port: super::DEFAULT_HTTP_PORT,
70 ws_port: super::DEFAULT_WS_PORT,
71 hot_reload: true,
72 preserve_state: true,
73 source_maps: true,
74 opt_level: OptLevel::Debug,
75 watch_patterns: vec!["src/**/*.rs".to_string(), "Cargo.toml".to_string()],
76 output_dir: PathBuf::from("target/wasm32-unknown-unknown/debug"),
77 static_dir: None,
78 wasm_filename: "app.wasm".to_string(),
79 }
80 }
81}
82
83impl WasmRunnerConfig {
84 #[must_use]
86 pub fn builder() -> WasmRunnerConfigBuilder {
87 WasmRunnerConfigBuilder::default()
88 }
89
90 #[must_use]
92 pub fn wasm_path(&self) -> PathBuf {
93 self.output_dir.join(&self.wasm_filename)
94 }
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct WasmRunnerConfigBuilder {
100 config: WasmRunnerConfig,
101}
102
103impl WasmRunnerConfigBuilder {
104 #[must_use]
106 pub fn http_port(mut self, port: u16) -> Self {
107 self.config.http_port = port;
108 self
109 }
110
111 #[must_use]
113 pub fn ws_port(mut self, port: u16) -> Self {
114 self.config.ws_port = port;
115 self
116 }
117
118 #[must_use]
120 pub fn hot_reload(mut self, enabled: bool) -> Self {
121 self.config.hot_reload = enabled;
122 self
123 }
124
125 #[must_use]
127 pub fn preserve_state(mut self, enabled: bool) -> Self {
128 self.config.preserve_state = enabled;
129 self
130 }
131
132 #[must_use]
134 pub fn source_maps(mut self, enabled: bool) -> Self {
135 self.config.source_maps = enabled;
136 self
137 }
138
139 #[must_use]
141 pub fn opt_level(mut self, level: OptLevel) -> Self {
142 self.config.opt_level = level;
143 self
144 }
145
146 #[must_use]
148 pub fn watch_patterns(mut self, patterns: Vec<String>) -> Self {
149 self.config.watch_patterns = patterns;
150 self
151 }
152
153 #[must_use]
155 pub fn output_dir(mut self, dir: PathBuf) -> Self {
156 self.config.output_dir = dir;
157 self
158 }
159
160 #[must_use]
162 pub fn static_dir(mut self, dir: PathBuf) -> Self {
163 self.config.static_dir = Some(dir);
164 self
165 }
166
167 #[must_use]
169 pub fn wasm_filename(mut self, name: impl Into<String>) -> Self {
170 self.config.wasm_filename = name.into();
171 self
172 }
173
174 #[must_use]
176 pub fn build(self) -> WasmRunnerConfig {
177 self.config
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct RunnerConfig {
184 pub project_root: PathBuf,
186 pub target: String,
188 pub package: Option<String>,
190 pub features: Vec<String>,
192 pub env: std::collections::HashMap<String, String>,
194}
195
196impl Default for RunnerConfig {
197 fn default() -> Self {
198 Self {
199 project_root: PathBuf::from("."),
200 target: "wasm32-unknown-unknown".to_string(),
201 package: None,
202 features: Vec::new(),
203 env: std::collections::HashMap::new(),
204 }
205 }
206}
207
208#[cfg(test)]
209#[allow(clippy::unwrap_used, clippy::expect_used)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_opt_level_as_str() {
215 assert_eq!(OptLevel::Debug.as_str(), "0");
216 assert_eq!(OptLevel::Release.as_str(), "3");
217 assert_eq!(OptLevel::Size.as_str(), "s");
218 assert_eq!(OptLevel::MinSize.as_str(), "z");
219 }
220
221 #[test]
222 fn test_opt_level_is_release() {
223 assert!(!OptLevel::Debug.is_release());
224 assert!(OptLevel::Release.is_release());
225 assert!(OptLevel::Size.is_release());
226 assert!(OptLevel::MinSize.is_release());
227 }
228
229 #[test]
230 fn test_wasm_config_default() {
231 let config = WasmRunnerConfig::default();
232 assert!(config.hot_reload);
233 assert!(config.preserve_state);
234 assert!(config.source_maps);
235 assert_eq!(config.opt_level, OptLevel::Debug);
236 }
237
238 #[test]
239 fn test_wasm_config_builder_chain() {
240 let config = WasmRunnerConfig::builder()
241 .http_port(9000)
242 .ws_port(9001)
243 .hot_reload(false)
244 .opt_level(OptLevel::Release)
245 .build();
246
247 assert_eq!(config.http_port, 9000);
248 assert_eq!(config.ws_port, 9001);
249 assert!(!config.hot_reload);
250 assert_eq!(config.opt_level, OptLevel::Release);
251 }
252
253 #[test]
254 fn test_wasm_path() {
255 let config = WasmRunnerConfig::builder()
256 .output_dir(PathBuf::from("target/wasm"))
257 .wasm_filename("myapp.wasm")
258 .build();
259
260 assert_eq!(config.wasm_path(), PathBuf::from("target/wasm/myapp.wasm"));
261 }
262
263 #[test]
264 fn test_runner_config_default() {
265 let config = RunnerConfig::default();
266 assert_eq!(config.target, "wasm32-unknown-unknown");
267 assert!(config.features.is_empty());
268 }
269
270 #[test]
271 fn test_opt_level_default() {
272 let opt = OptLevel::default();
273 assert_eq!(opt, OptLevel::Debug);
274 assert!(!opt.is_release());
275 }
276
277 #[test]
278 fn test_wasm_config_builder_preserve_state() {
279 let config = WasmRunnerConfig::builder().preserve_state(false).build();
280 assert!(!config.preserve_state);
281
282 let config2 = WasmRunnerConfig::builder().preserve_state(true).build();
283 assert!(config2.preserve_state);
284 }
285
286 #[test]
287 fn test_wasm_config_builder_source_maps() {
288 let config = WasmRunnerConfig::builder().source_maps(false).build();
289 assert!(!config.source_maps);
290
291 let config2 = WasmRunnerConfig::builder().source_maps(true).build();
292 assert!(config2.source_maps);
293 }
294
295 #[test]
296 fn test_wasm_config_builder_watch_patterns() {
297 let patterns = vec![
298 "src/**/*.rs".to_string(),
299 "tests/**/*.rs".to_string(),
300 "Cargo.toml".to_string(),
301 ];
302 let config = WasmRunnerConfig::builder()
303 .watch_patterns(patterns.clone())
304 .build();
305
306 assert_eq!(config.watch_patterns.len(), 3);
307 assert_eq!(config.watch_patterns, patterns);
308 }
309
310 #[test]
311 fn test_wasm_config_builder_static_dir() {
312 let config = WasmRunnerConfig::builder()
313 .static_dir(PathBuf::from("public"))
314 .build();
315
316 assert_eq!(config.static_dir, Some(PathBuf::from("public")));
317 }
318
319 #[test]
320 fn test_wasm_config_default_static_dir_none() {
321 let config = WasmRunnerConfig::default();
322 assert!(config.static_dir.is_none());
323 }
324
325 #[test]
326 fn test_runner_config_with_all_fields() {
327 let mut config = RunnerConfig::default();
328 config.project_root = PathBuf::from("/home/user/project");
329 config.target = "wasm32-wasi".to_string();
330 config.package = Some("my-wasm-lib".to_string());
331 config.features = vec!["feature1".to_string(), "feature2".to_string()];
332 config
333 .env
334 .insert("RUST_LOG".to_string(), "debug".to_string());
335 config
336 .env
337 .insert("CARGO_TERM_COLOR".to_string(), "always".to_string());
338
339 assert_eq!(config.project_root, PathBuf::from("/home/user/project"));
340 assert_eq!(config.target, "wasm32-wasi");
341 assert_eq!(config.package, Some("my-wasm-lib".to_string()));
342 assert_eq!(config.features.len(), 2);
343 assert_eq!(config.env.len(), 2);
344 assert_eq!(config.env.get("RUST_LOG"), Some(&"debug".to_string()));
345 }
346
347 #[test]
348 fn test_wasm_config_builder_all_options() {
349 let config = WasmRunnerConfig::builder()
350 .http_port(3000)
351 .ws_port(3001)
352 .hot_reload(true)
353 .preserve_state(false)
354 .source_maps(true)
355 .opt_level(OptLevel::MinSize)
356 .watch_patterns(vec!["*.rs".to_string()])
357 .output_dir(PathBuf::from("dist"))
358 .static_dir(PathBuf::from("assets"))
359 .wasm_filename("game.wasm")
360 .build();
361
362 assert_eq!(config.http_port, 3000);
363 assert_eq!(config.ws_port, 3001);
364 assert!(config.hot_reload);
365 assert!(!config.preserve_state);
366 assert!(config.source_maps);
367 assert_eq!(config.opt_level, OptLevel::MinSize);
368 assert_eq!(config.watch_patterns, vec!["*.rs".to_string()]);
369 assert_eq!(config.output_dir, PathBuf::from("dist"));
370 assert_eq!(config.static_dir, Some(PathBuf::from("assets")));
371 assert_eq!(config.wasm_filename, "game.wasm");
372 }
373
374 #[test]
375 fn test_wasm_path_with_nested_output_dir() {
376 let config = WasmRunnerConfig::builder()
377 .output_dir(PathBuf::from("target/wasm32-unknown-unknown/release"))
378 .wasm_filename("my_game.wasm")
379 .build();
380
381 assert_eq!(
382 config.wasm_path(),
383 PathBuf::from("target/wasm32-unknown-unknown/release/my_game.wasm")
384 );
385 }
386
387 #[test]
388 fn test_opt_level_serialization() {
389 for opt in [
390 OptLevel::Debug,
391 OptLevel::Release,
392 OptLevel::Size,
393 OptLevel::MinSize,
394 ] {
395 let json = serde_json::to_string(&opt).unwrap();
396 let deserialized: OptLevel = serde_json::from_str(&json).unwrap();
397 assert_eq!(opt, deserialized);
398 }
399 }
400
401 #[test]
402 fn test_wasm_runner_config_serialization() {
403 let config = WasmRunnerConfig::builder()
404 .http_port(8000)
405 .ws_port(8001)
406 .hot_reload(true)
407 .opt_level(OptLevel::Release)
408 .wasm_filename("app.wasm")
409 .build();
410
411 let json = serde_json::to_string(&config).unwrap();
412 let deserialized: WasmRunnerConfig = serde_json::from_str(&json).unwrap();
413
414 assert_eq!(deserialized.http_port, 8000);
415 assert_eq!(deserialized.ws_port, 8001);
416 assert!(deserialized.hot_reload);
417 assert_eq!(deserialized.opt_level, OptLevel::Release);
418 assert_eq!(deserialized.wasm_filename, "app.wasm");
419 }
420
421 #[test]
422 fn test_runner_config_serialization() {
423 let mut config = RunnerConfig::default();
424 config.package = Some("test-package".to_string());
425 config.features = vec!["web".to_string()];
426
427 let json = serde_json::to_string(&config).unwrap();
428 let deserialized: RunnerConfig = serde_json::from_str(&json).unwrap();
429
430 assert_eq!(deserialized.target, "wasm32-unknown-unknown");
431 assert_eq!(deserialized.package, Some("test-package".to_string()));
432 assert_eq!(deserialized.features, vec!["web".to_string()]);
433 }
434
435 #[test]
436 fn test_wasm_config_default_watch_patterns() {
437 let config = WasmRunnerConfig::default();
438 assert!(config.watch_patterns.contains(&"src/**/*.rs".to_string()));
439 assert!(config.watch_patterns.contains(&"Cargo.toml".to_string()));
440 }
441
442 #[test]
443 fn test_wasm_config_default_output_dir() {
444 let config = WasmRunnerConfig::default();
445 assert_eq!(
446 config.output_dir,
447 PathBuf::from("target/wasm32-unknown-unknown/debug")
448 );
449 }
450
451 #[test]
452 fn test_wasm_config_default_wasm_filename() {
453 let config = WasmRunnerConfig::default();
454 assert_eq!(config.wasm_filename, "app.wasm");
455 }
456
457 #[test]
458 fn test_wasm_config_builder_default_values() {
459 let builder = WasmRunnerConfigBuilder::default();
461 let config = builder.build();
462
463 let default_config = WasmRunnerConfig::default();
465 assert_eq!(config.http_port, default_config.http_port);
466 assert_eq!(config.ws_port, default_config.ws_port);
467 assert_eq!(config.hot_reload, default_config.hot_reload);
468 assert_eq!(config.preserve_state, default_config.preserve_state);
469 assert_eq!(config.source_maps, default_config.source_maps);
470 assert_eq!(config.opt_level, default_config.opt_level);
471 }
472
473 #[test]
474 fn test_runner_config_empty_env() {
475 let config = RunnerConfig::default();
476 assert!(config.env.is_empty());
477 }
478
479 #[test]
480 fn test_runner_config_project_root_default() {
481 let config = RunnerConfig::default();
482 assert_eq!(config.project_root, PathBuf::from("."));
483 }
484
485 #[test]
486 fn test_wasm_config_builder_chaining() {
487 let config1 = WasmRunnerConfig::builder()
489 .http_port(9000)
490 .opt_level(OptLevel::Size)
491 .hot_reload(false)
492 .build();
493
494 let config2 = WasmRunnerConfig::builder()
495 .hot_reload(false)
496 .http_port(9000)
497 .opt_level(OptLevel::Size)
498 .build();
499
500 assert_eq!(config1.http_port, config2.http_port);
501 assert_eq!(config1.opt_level, config2.opt_level);
502 assert_eq!(config1.hot_reload, config2.hot_reload);
503 }
504
505 #[test]
506 fn test_opt_level_all_variants_as_str() {
507 let variants = [
509 (OptLevel::Debug, "0"),
510 (OptLevel::Release, "3"),
511 (OptLevel::Size, "s"),
512 (OptLevel::MinSize, "z"),
513 ];
514
515 for (opt, expected) in variants {
516 assert_eq!(opt.as_str(), expected);
517 }
518 }
519
520 #[test]
521 fn test_opt_level_is_release_all_variants() {
522 assert!(!OptLevel::Debug.is_release());
524
525 assert!(OptLevel::Release.is_release());
527 assert!(OptLevel::Size.is_release());
528 assert!(OptLevel::MinSize.is_release());
529 }
530}