1use regex::Regex;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum DioxusOptionType {
12 Flag,
14 Bool { default: Option<bool> },
16 String { default: Option<String> },
18 Port { default: Option<u16> },
20 Address { default: Option<String> },
22 Platform { default: Option<String> },
24 Architecture,
26 Profile { default: Option<String> },
28 Features,
30 Multiple,
32 Interval { default: Option<u64> },
34}
35
36#[derive(Debug, Clone)]
38pub struct DioxusOptionInfo {
39 pub long: &'static str,
40 pub short: Option<&'static str>,
41 pub option_type: DioxusOptionType,
42 pub description: &'static str,
43 pub possible_values: Option<Vec<&'static str>>,
44}
45
46#[derive(Debug, Clone)]
48pub enum ValidationResult {
49 Valid,
50 InvalidOption {
51 suggestion: Option<String>,
52 },
53 InvalidValue {
54 expected: String,
55 got: String,
56 suggestions: Vec<String>,
57 },
58 MissingValue {
59 expected: String,
60 },
61}
62
63pub struct FrameworkValidator {
65 dioxus_serve_options: HashMap<&'static str, DioxusOptionInfo>,
66 leptos_watch_options: HashMap<&'static str, DioxusOptionInfo>,
67 platform_values: Vec<&'static str>,
68 arch_values: Vec<&'static str>,
69 bool_values: Vec<&'static str>,
70}
71
72pub type DioxusValidator = FrameworkValidator;
74
75impl FrameworkValidator {
76 pub fn new() -> Self {
77 let mut dioxus_serve_options = HashMap::new();
78 let mut leptos_watch_options = HashMap::new();
79
80 dioxus_serve_options.insert(
84 "--port",
85 DioxusOptionInfo {
86 long: "--port",
87 short: None,
88 option_type: DioxusOptionType::Port { default: None },
89 description: "The port the server will run on",
90 possible_values: None,
91 },
92 );
93
94 dioxus_serve_options.insert(
96 "--addr",
97 DioxusOptionInfo {
98 long: "--addr",
99 short: None,
100 option_type: DioxusOptionType::Address {
101 default: Some("localhost".to_string()),
102 },
103 description: "The address the server will run on",
104 possible_values: None,
105 },
106 );
107
108 dioxus_serve_options.insert(
110 "--open",
111 DioxusOptionInfo {
112 long: "--open",
113 short: None,
114 option_type: DioxusOptionType::Bool {
115 default: Some(true),
116 },
117 description: "Open the app in the default browser",
118 possible_values: Some(vec!["true", "false"]),
119 },
120 );
121
122 dioxus_serve_options.insert(
124 "--hot-reload",
125 DioxusOptionInfo {
126 long: "--hot-reload",
127 short: None,
128 option_type: DioxusOptionType::Bool {
129 default: Some(true),
130 },
131 description: "Enable full hot reloading for the app",
132 possible_values: Some(vec!["true", "false"]),
133 },
134 );
135
136 dioxus_serve_options.insert(
138 "--always-on-top",
139 DioxusOptionInfo {
140 long: "--always-on-top",
141 short: None,
142 option_type: DioxusOptionType::Bool {
143 default: Some(true),
144 },
145 description: "Configure always-on-top for desktop apps",
146 possible_values: Some(vec!["true", "false"]),
147 },
148 );
149
150 dioxus_serve_options.insert(
152 "--cross-origin-policy",
153 DioxusOptionInfo {
154 long: "--cross-origin-policy",
155 short: None,
156 option_type: DioxusOptionType::Flag,
157 description: "Set cross-origin-policy to same-origin",
158 possible_values: None,
159 },
160 );
161
162 dioxus_serve_options.insert(
164 "--args",
165 DioxusOptionInfo {
166 long: "--args",
167 short: None,
168 option_type: DioxusOptionType::String { default: None },
169 description: "Additional arguments to pass to the executable",
170 possible_values: None,
171 },
172 );
173
174 dioxus_serve_options.insert(
176 "--wsl-file-poll-interval",
177 DioxusOptionInfo {
178 long: "--wsl-file-poll-interval",
179 short: None,
180 option_type: DioxusOptionType::Interval { default: None },
181 description:
182 "Sets the interval in seconds that the CLI will poll for file changes on WSL",
183 possible_values: None,
184 },
185 );
186
187 dioxus_serve_options.insert(
189 "--interactive",
190 DioxusOptionInfo {
191 long: "--interactive",
192 short: Some("-i"),
193 option_type: DioxusOptionType::Bool {
194 default: Some(false),
195 },
196 description: "Run the server in interactive mode",
197 possible_values: Some(vec!["true", "false"]),
198 },
199 );
200
201 dioxus_serve_options.insert(
203 "--release",
204 DioxusOptionInfo {
205 long: "--release",
206 short: Some("-r"),
207 option_type: DioxusOptionType::Flag,
208 description: "Build in release mode",
209 possible_values: None,
210 },
211 );
212
213 dioxus_serve_options.insert(
215 "--force-sequential",
216 DioxusOptionInfo {
217 long: "--force-sequential",
218 short: None,
219 option_type: DioxusOptionType::Flag,
220 description: "Force fullstack builds to run server first, then client",
221 possible_values: None,
222 },
223 );
224
225 dioxus_serve_options.insert(
227 "--profile",
228 DioxusOptionInfo {
229 long: "--profile",
230 short: None,
231 option_type: DioxusOptionType::Profile { default: None },
232 description: "Build the app with custom a profile",
233 possible_values: None,
234 },
235 );
236
237 dioxus_serve_options.insert(
239 "--verbose",
240 DioxusOptionInfo {
241 long: "--verbose",
242 short: None,
243 option_type: DioxusOptionType::Flag,
244 description: "Use verbose output",
245 possible_values: None,
246 },
247 );
248
249 dioxus_serve_options.insert(
251 "--server-profile",
252 DioxusOptionInfo {
253 long: "--server-profile",
254 short: None,
255 option_type: DioxusOptionType::Profile {
256 default: Some("server-dev".to_string()),
257 },
258 description: "Build with custom profile for the fullstack server",
259 possible_values: None,
260 },
261 );
262
263 dioxus_serve_options.insert(
265 "--trace",
266 DioxusOptionInfo {
267 long: "--trace",
268 short: None,
269 option_type: DioxusOptionType::Flag,
270 description: "Use trace output",
271 possible_values: None,
272 },
273 );
274
275 dioxus_serve_options.insert(
277 "--json-output",
278 DioxusOptionInfo {
279 long: "--json-output",
280 short: None,
281 option_type: DioxusOptionType::Flag,
282 description: "Output logs in JSON format",
283 possible_values: None,
284 },
285 );
286
287 dioxus_serve_options.insert(
289 "--platform",
290 DioxusOptionInfo {
291 long: "--platform",
292 short: None,
293 option_type: DioxusOptionType::Platform {
294 default: Some("default_platform".to_string()),
295 },
296 description: "Build platform: support Web & Desktop",
297 possible_values: Some(vec![
298 "web", "macos", "windows", "linux", "ios", "android", "server", "liveview",
299 ]),
300 },
301 );
302
303 dioxus_serve_options.insert(
305 "--fullstack",
306 DioxusOptionInfo {
307 long: "--fullstack",
308 short: None,
309 option_type: DioxusOptionType::Flag,
310 description: "Build the fullstack variant of this app",
311 possible_values: None,
312 },
313 );
314
315 dioxus_serve_options.insert(
317 "--ssg",
318 DioxusOptionInfo {
319 long: "--ssg",
320 short: None,
321 option_type: DioxusOptionType::Flag,
322 description: "Run the ssg config of the app and generate the files",
323 possible_values: None,
324 },
325 );
326
327 dioxus_serve_options.insert(
329 "--skip-assets",
330 DioxusOptionInfo {
331 long: "--skip-assets",
332 short: None,
333 option_type: DioxusOptionType::Flag,
334 description: "Skip collecting assets from dependencies",
335 possible_values: None,
336 },
337 );
338
339 dioxus_serve_options.insert(
341 "--inject-loading-scripts",
342 DioxusOptionInfo {
343 long: "--inject-loading-scripts",
344 short: None,
345 option_type: DioxusOptionType::Flag,
346 description: "Inject scripts to load the wasm and js files",
347 possible_values: None,
348 },
349 );
350
351 dioxus_serve_options.insert(
353 "--debug-symbols",
354 DioxusOptionInfo {
355 long: "--debug-symbols",
356 short: None,
357 option_type: DioxusOptionType::Flag,
358 description: "Generate debug symbols for the wasm binary",
359 possible_values: None,
360 },
361 );
362
363 dioxus_serve_options.insert(
365 "--nightly",
366 DioxusOptionInfo {
367 long: "--nightly",
368 short: None,
369 option_type: DioxusOptionType::Flag,
370 description: "Build for nightly",
371 possible_values: None,
372 },
373 );
374
375 dioxus_serve_options.insert(
377 "--example",
378 DioxusOptionInfo {
379 long: "--example",
380 short: None,
381 option_type: DioxusOptionType::String {
382 default: Some("".to_string()),
383 },
384 description: "Build a example",
385 possible_values: None,
386 },
387 );
388
389 dioxus_serve_options.insert(
391 "--bin",
392 DioxusOptionInfo {
393 long: "--bin",
394 short: None,
395 option_type: DioxusOptionType::String {
396 default: Some("".to_string()),
397 },
398 description: "Build a binary",
399 possible_values: None,
400 },
401 );
402
403 dioxus_serve_options.insert(
405 "--package",
406 DioxusOptionInfo {
407 long: "--package",
408 short: Some("-p"),
409 option_type: DioxusOptionType::String { default: None },
410 description: "The package to build",
411 possible_values: None,
412 },
413 );
414
415 dioxus_serve_options.insert(
417 "--features",
418 DioxusOptionInfo {
419 long: "--features",
420 short: None,
421 option_type: DioxusOptionType::Features,
422 description: "Space separated list of features to activate",
423 possible_values: None,
424 },
425 );
426
427 dioxus_serve_options.insert(
429 "--client-features",
430 DioxusOptionInfo {
431 long: "--client-features",
432 short: None,
433 option_type: DioxusOptionType::String {
434 default: Some("web".to_string()),
435 },
436 description: "The feature to use for the client in a fullstack app",
437 possible_values: None,
438 },
439 );
440
441 dioxus_serve_options.insert(
443 "--server-features",
444 DioxusOptionInfo {
445 long: "--server-features",
446 short: None,
447 option_type: DioxusOptionType::String {
448 default: Some("server".to_string()),
449 },
450 description: "The feature to use for the server in a fullstack app",
451 possible_values: None,
452 },
453 );
454
455 dioxus_serve_options.insert(
457 "--no-default-features",
458 DioxusOptionInfo {
459 long: "--no-default-features",
460 short: None,
461 option_type: DioxusOptionType::Flag,
462 description: "Don't include the default features in the build",
463 possible_values: None,
464 },
465 );
466
467 dioxus_serve_options.insert(
469 "--arch",
470 DioxusOptionInfo {
471 long: "--arch",
472 short: None,
473 option_type: DioxusOptionType::Architecture,
474 description: "The architecture to build for",
475 possible_values: Some(vec!["arm", "arm64", "x86", "x64"]),
476 },
477 );
478
479 dioxus_serve_options.insert(
481 "--device",
482 DioxusOptionInfo {
483 long: "--device",
484 short: None,
485 option_type: DioxusOptionType::Bool { default: None },
486 description: "Are we building for a device or just the simulator",
487 possible_values: Some(vec!["true", "false"]),
488 },
489 );
490
491 dioxus_serve_options.insert(
493 "--target",
494 DioxusOptionInfo {
495 long: "--target",
496 short: None,
497 option_type: DioxusOptionType::String { default: None },
498 description: "Rustc platform triple",
499 possible_values: None,
500 },
501 );
502
503 leptos_watch_options.insert(
507 "--release",
508 DioxusOptionInfo {
509 long: "--release",
510 short: Some("-r"),
511 option_type: DioxusOptionType::Flag,
512 description: "Build artifacts in release mode, with optimizations",
513 possible_values: None,
514 },
515 );
516
517 leptos_watch_options.insert("--precompress", DioxusOptionInfo {
519 long: "--precompress",
520 short: Some("-P"),
521 option_type: DioxusOptionType::Flag,
522 description: "Precompress static assets with gzip and brotli. Applies to release builds only",
523 possible_values: None,
524 });
525
526 leptos_watch_options.insert(
528 "--hot-reload",
529 DioxusOptionInfo {
530 long: "--hot-reload",
531 short: None,
532 option_type: DioxusOptionType::Flag,
533 description: "Turn on partial hot-reloading. Requires rust nightly [beta]",
534 possible_values: None,
535 },
536 );
537
538 leptos_watch_options.insert(
540 "--project",
541 DioxusOptionInfo {
542 long: "--project",
543 short: Some("-p"),
544 option_type: DioxusOptionType::String { default: None },
545 description: "Which project to use, from a list of projects defined in a workspace",
546 possible_values: None,
547 },
548 );
549
550 leptos_watch_options.insert(
552 "--features",
553 DioxusOptionInfo {
554 long: "--features",
555 short: None,
556 option_type: DioxusOptionType::Features,
557 description: "The features to use when compiling all targets",
558 possible_values: None,
559 },
560 );
561
562 leptos_watch_options.insert(
564 "--lib-features",
565 DioxusOptionInfo {
566 long: "--lib-features",
567 short: None,
568 option_type: DioxusOptionType::Features,
569 description: "The features to use when compiling the lib target",
570 possible_values: None,
571 },
572 );
573
574 leptos_watch_options.insert(
576 "--lib-cargo-args",
577 DioxusOptionInfo {
578 long: "--lib-cargo-args",
579 short: None,
580 option_type: DioxusOptionType::String { default: None },
581 description: "The cargo flags to pass to cargo when compiling the lib target",
582 possible_values: None,
583 },
584 );
585
586 leptos_watch_options.insert(
588 "--bin-features",
589 DioxusOptionInfo {
590 long: "--bin-features",
591 short: None,
592 option_type: DioxusOptionType::Features,
593 description: "The features to use when compiling the bin target",
594 possible_values: None,
595 },
596 );
597
598 leptos_watch_options.insert(
600 "--bin-cargo-args",
601 DioxusOptionInfo {
602 long: "--bin-cargo-args",
603 short: None,
604 option_type: DioxusOptionType::String { default: None },
605 description: "The cargo flags to pass to cargo when compiling the bin target",
606 possible_values: None,
607 },
608 );
609
610 leptos_watch_options.insert("--wasm-debug", DioxusOptionInfo {
612 long: "--wasm-debug",
613 short: None,
614 option_type: DioxusOptionType::Flag,
615 description: "Include debug information in Wasm output. Includes source maps and DWARF debug info",
616 possible_values: None,
617 });
618
619 leptos_watch_options.insert(
621 "-v",
622 DioxusOptionInfo {
623 long: "-v",
624 short: None,
625 option_type: DioxusOptionType::Multiple,
626 description:
627 "Verbosity (none: info, errors & warnings, -v: verbose, -vv: very verbose)",
628 possible_values: None,
629 },
630 );
631
632 leptos_watch_options.insert(
634 "--js-minify",
635 DioxusOptionInfo {
636 long: "--js-minify",
637 short: None,
638 option_type: DioxusOptionType::Bool {
639 default: Some(true),
640 },
641 description: "Minify javascript assets with swc. Applies to release builds only",
642 possible_values: Some(vec!["true", "false"]),
643 },
644 );
645
646 Self {
647 dioxus_serve_options,
648 leptos_watch_options,
649 platform_values: vec![
650 "web", "macos", "windows", "linux", "ios", "android", "server", "liveview",
651 ],
652 arch_values: vec!["arm", "arm64", "x86", "x64"],
653 bool_values: vec!["true", "false"],
654 }
655 }
656
657 pub fn validate_option(
659 &self,
660 framework: &str,
661 option: &str,
662 value: Option<&str>,
663 ) -> ValidationResult {
664 let option_info = match framework {
666 "dioxus" => self.dioxus_serve_options.get(option),
667 "leptos" => self.leptos_watch_options.get(option),
668 _ => None,
669 };
670
671 let option_info = match option_info {
672 Some(info) => info,
673 None => {
674 let suggestion = self.find_closest_option(framework, option);
676 return ValidationResult::InvalidOption { suggestion };
677 }
678 };
679
680 match &option_info.option_type {
682 DioxusOptionType::Flag => {
683 if let Some(val) = value {
684 ValidationResult::InvalidValue {
685 expected: "no value (this is a flag)".to_string(),
686 got: val.to_string(),
687 suggestions: vec![
688 "Remove the value - this option doesn't take a value".to_string(),
689 ],
690 }
691 } else {
692 ValidationResult::Valid
693 }
694 }
695
696 DioxusOptionType::Bool { default: _ } => {
697 if let Some(val) = value {
698 if self.bool_values.contains(&val) {
699 ValidationResult::Valid
700 } else {
701 ValidationResult::InvalidValue {
702 expected: "true or false".to_string(),
703 got: val.to_string(),
704 suggestions: self.suggest_bool_values(val),
705 }
706 }
707 } else {
708 ValidationResult::Valid
710 }
711 }
712
713 DioxusOptionType::Port { default: _ } => match value {
714 Some(val) => match val.parse::<u16>() {
715 Ok(port) => {
716 if port > 0 {
717 ValidationResult::Valid
718 } else {
719 ValidationResult::InvalidValue {
720 expected: "port number (1-65535)".to_string(),
721 got: val.to_string(),
722 suggestions: vec![
723 "Try a port number like 3000, 8080, or 8000".to_string(),
724 ],
725 }
726 }
727 }
728 Err(_) => ValidationResult::InvalidValue {
729 expected: "port number (1-65535)".to_string(),
730 got: val.to_string(),
731 suggestions: vec![
732 "Port must be a number, e.g., 3000, 8080, 8000".to_string(),
733 ],
734 },
735 },
736 None => ValidationResult::MissingValue {
737 expected: "port number (e.g., 3000, 8080)".to_string(),
738 },
739 },
740
741 DioxusOptionType::Address { default: _ } => match value {
742 Some(val) => {
743 if self.is_valid_address(val) {
744 ValidationResult::Valid
745 } else {
746 ValidationResult::InvalidValue {
747 expected: "valid IP address or hostname".to_string(),
748 got: val.to_string(),
749 suggestions: vec![
750 "localhost".to_string(),
751 "127.0.0.1".to_string(),
752 "0.0.0.0".to_string(),
753 ],
754 }
755 }
756 }
757 None => ValidationResult::MissingValue {
758 expected: "IP address or hostname (e.g., localhost, 127.0.0.1)".to_string(),
759 },
760 },
761
762 DioxusOptionType::Platform { default: _ } => match value {
763 Some(val) => {
764 if self.platform_values.contains(&val) {
765 ValidationResult::Valid
766 } else {
767 ValidationResult::InvalidValue {
768 expected: format!("one of: {}", self.platform_values.join(", ")),
769 got: val.to_string(),
770 suggestions: self.suggest_platform_values(val),
771 }
772 }
773 }
774 None => ValidationResult::MissingValue {
775 expected: format!("platform ({})", self.platform_values.join(", ")),
776 },
777 },
778
779 DioxusOptionType::Architecture => match value {
780 Some(val) => {
781 if self.arch_values.contains(&val) {
782 ValidationResult::Valid
783 } else {
784 ValidationResult::InvalidValue {
785 expected: format!("one of: {}", self.arch_values.join(", ")),
786 got: val.to_string(),
787 suggestions: self.suggest_arch_values(val),
788 }
789 }
790 }
791 None => ValidationResult::MissingValue {
792 expected: format!("architecture ({})", self.arch_values.join(", ")),
793 },
794 },
795
796 DioxusOptionType::Interval { default: _ } => match value {
797 Some(val) => match val.parse::<u64>() {
798 Ok(_) => ValidationResult::Valid,
799 Err(_) => ValidationResult::InvalidValue {
800 expected: "number of seconds".to_string(),
801 got: val.to_string(),
802 suggestions: vec!["Try a number like 1, 5, or 30".to_string()],
803 },
804 },
805 None => ValidationResult::MissingValue {
806 expected: "interval in seconds (e.g., 1, 5, 30)".to_string(),
807 },
808 },
809
810 DioxusOptionType::String { default: _ }
811 | DioxusOptionType::Profile { default: _ }
812 | DioxusOptionType::Features
813 | DioxusOptionType::Multiple => match value {
814 Some(_) => ValidationResult::Valid,
815 None => ValidationResult::MissingValue {
816 expected: "string value".to_string(),
817 },
818 },
819 }
820 }
821
822 fn find_closest_option(&self, framework: &str, input: &str) -> Option<String> {
824 let mut best_match = None;
825 let mut best_distance = usize::MAX;
826
827 let options = match framework {
828 "dioxus" => self.dioxus_serve_options.keys(),
829 "leptos" => self.leptos_watch_options.keys(),
830 _ => return None,
831 };
832
833 for option in options {
834 let distance = levenshtein_distance(input, option);
835 if distance < best_distance && distance <= 3 {
836 best_distance = distance;
837 best_match = Some(option.to_string());
838 }
839 }
840
841 best_match
842 }
843
844 fn suggest_bool_values(&self, input: &str) -> Vec<String> {
846 let lower = input.to_lowercase();
847 if lower.starts_with('t') || lower.starts_with('y') || lower == "1" {
848 vec!["true".to_string()]
849 } else if lower.starts_with('f') || lower.starts_with('n') || lower == "0" {
850 vec!["false".to_string()]
851 } else {
852 vec!["true".to_string(), "false".to_string()]
853 }
854 }
855
856 fn suggest_platform_values(&self, input: &str) -> Vec<String> {
858 let mut suggestions = Vec::new();
859 let lower = input.to_lowercase();
860
861 for platform in &self.platform_values {
862 if platform.starts_with(&lower) || levenshtein_distance(&lower, platform) <= 2 {
863 suggestions.push(platform.to_string());
864 }
865 }
866
867 if suggestions.is_empty() {
868 suggestions.extend_from_slice(&["web".to_string(), "desktop".to_string()]);
870 }
871
872 suggestions
873 }
874
875 fn suggest_arch_values(&self, input: &str) -> Vec<String> {
877 let mut suggestions = Vec::new();
878 let lower = input.to_lowercase();
879
880 for arch in &self.arch_values {
881 if arch.starts_with(&lower) || levenshtein_distance(&lower, arch) <= 1 {
882 suggestions.push(arch.to_string());
883 }
884 }
885
886 if suggestions.is_empty() {
887 suggestions.extend_from_slice(&["x64".to_string(), "arm64".to_string()]);
888 }
889
890 suggestions
891 }
892
893 fn is_valid_address(&self, addr: &str) -> bool {
895 if addr == "localhost" || addr == "0.0.0.0" {
897 return true;
898 }
899
900 let ip_regex = Regex::new(r"^(\d{1,3}\.){3}\d{1,3}$").unwrap();
902 if ip_regex.is_match(addr) {
903 return addr.split('.').all(|octet| octet.parse::<u8>().is_ok());
905 }
906
907 let hostname_regex = Regex::new(r"^[a-zA-Z0-9.-]+$").unwrap();
909 hostname_regex.is_match(addr)
910 }
911
912 pub fn is_valid_option(&self, framework: &str, option: &str) -> bool {
914 match framework {
915 "dioxus" => self.dioxus_serve_options.contains_key(option),
916 "leptos" => self.leptos_watch_options.contains_key(option),
917 _ => false,
918 }
919 }
920
921 pub fn is_valid_serve_option(&self, option: &str) -> bool {
923 self.is_valid_option("dioxus", option)
924 }
925
926 pub fn get_option_info(&self, framework: &str, option: &str) -> Option<&DioxusOptionInfo> {
928 match framework {
929 "dioxus" => self.dioxus_serve_options.get(option),
930 "leptos" => self.leptos_watch_options.get(option),
931 _ => None,
932 }
933 }
934
935 pub fn get_all_options(&self, framework: &str) -> Vec<&str> {
937 match framework {
938 "dioxus" => self.dioxus_serve_options.keys().copied().collect(),
939 "leptos" => self.leptos_watch_options.keys().copied().collect(),
940 _ => vec![],
941 }
942 }
943
944 pub fn validate_dioxus_option(&self, option: &str, value: Option<&str>) -> ValidationResult {
946 self.validate_option("dioxus", option, value)
947 }
948
949 pub fn validate_leptos_option(&self, option: &str, value: Option<&str>) -> ValidationResult {
951 self.validate_option("leptos", option, value)
952 }
953}
954
955impl Default for DioxusValidator {
956 fn default() -> Self {
957 Self::new()
958 }
959}
960
961fn levenshtein_distance(s1: &str, s2: &str) -> usize {
963 let len1 = s1.chars().count();
964 let len2 = s2.chars().count();
965 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
966
967 for (i, row) in matrix.iter_mut().enumerate().take(len1 + 1) {
968 row[0] = i;
969 }
970 for j in 0..=len2 {
971 matrix[0][j] = j;
972 }
973
974 let s1_chars: Vec<char> = s1.chars().collect();
975 let s2_chars: Vec<char> = s2.chars().collect();
976
977 for i in 1..=len1 {
978 for j in 1..=len2 {
979 let cost = if s1_chars[i - 1] == s2_chars[j - 1] {
980 0
981 } else {
982 1
983 };
984 matrix[i][j] = (matrix[i - 1][j] + 1)
985 .min(matrix[i][j - 1] + 1)
986 .min(matrix[i - 1][j - 1] + cost);
987 }
988 }
989
990 matrix[len1][len2]
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996
997 #[test]
998 fn test_valid_dioxus_options() {
999 let validator = FrameworkValidator::new();
1000
1001 assert!(matches!(
1003 validator.validate_dioxus_option("--release", None),
1004 ValidationResult::Valid
1005 ));
1006 assert!(matches!(
1007 validator.validate_dioxus_option("--fullstack", None),
1008 ValidationResult::Valid
1009 ));
1010
1011 assert!(matches!(
1013 validator.validate_dioxus_option("--device", Some("true")),
1014 ValidationResult::Valid
1015 ));
1016 assert!(matches!(
1017 validator.validate_dioxus_option("--device", Some("false")),
1018 ValidationResult::Valid
1019 ));
1020 assert!(matches!(
1021 validator.validate_dioxus_option("--hot-reload", Some("true")),
1022 ValidationResult::Valid
1023 ));
1024
1025 assert!(matches!(
1027 validator.validate_dioxus_option("--port", Some("3000")),
1028 ValidationResult::Valid
1029 ));
1030 assert!(matches!(
1031 validator.validate_dioxus_option("--port", Some("8080")),
1032 ValidationResult::Valid
1033 ));
1034
1035 assert!(matches!(
1037 validator.validate_dioxus_option("--platform", Some("web")),
1038 ValidationResult::Valid
1039 ));
1040
1041 assert!(matches!(
1043 validator.validate_dioxus_option("--arch", Some("x64")),
1044 ValidationResult::Valid
1045 ));
1046 assert!(matches!(
1047 validator.validate_dioxus_option("--arch", Some("arm64")),
1048 ValidationResult::Valid
1049 ));
1050 }
1051
1052 #[test]
1053 fn test_valid_leptos_options() {
1054 let validator = FrameworkValidator::new();
1055
1056 assert!(matches!(
1058 validator.validate_leptos_option("--release", None),
1059 ValidationResult::Valid
1060 ));
1061 assert!(matches!(
1062 validator.validate_leptos_option("--precompress", None),
1063 ValidationResult::Valid
1064 ));
1065 assert!(matches!(
1066 validator.validate_leptos_option("--hot-reload", None),
1067 ValidationResult::Valid
1068 ));
1069
1070 assert!(matches!(
1072 validator.validate_leptos_option("--js-minify", Some("true")),
1073 ValidationResult::Valid
1074 ));
1075 assert!(matches!(
1076 validator.validate_leptos_option("--js-minify", Some("false")),
1077 ValidationResult::Valid
1078 ));
1079
1080 assert!(matches!(
1082 validator.validate_leptos_option("--project", Some("my-project")),
1083 ValidationResult::Valid
1084 ));
1085 assert!(matches!(
1086 validator.validate_leptos_option("--features", Some("ssr,hydrate")),
1087 ValidationResult::Valid
1088 ));
1089 }
1090
1091 #[test]
1092 fn test_invalid_device_value() {
1093 let validator = FrameworkValidator::new();
1094
1095 match validator.validate_dioxus_option("--device", Some("example")) {
1096 ValidationResult::InvalidValue {
1097 expected,
1098 got,
1099 suggestions,
1100 } => {
1101 assert_eq!(expected, "true or false");
1102 assert_eq!(got, "example");
1103 assert!(!suggestions.is_empty());
1104 }
1105 _ => panic!("Expected InvalidValue result"),
1106 }
1107 }
1108
1109 #[test]
1110 fn test_invalid_platform_value() {
1111 let validator = FrameworkValidator::new();
1112
1113 match validator.validate_dioxus_option("--platform", Some("desktop")) {
1114 ValidationResult::InvalidValue {
1115 expected,
1116 got,
1117 suggestions,
1118 } => {
1119 assert!(expected.contains("web"));
1120 assert_eq!(got, "desktop");
1121 assert!(
1122 suggestions
1123 .iter()
1124 .any(|s| s.contains("web") || s.contains("macos"))
1125 );
1126 }
1127 _ => panic!("Expected InvalidValue result"),
1128 }
1129 }
1130
1131 #[test]
1132 fn test_typo_suggestions() {
1133 let validator = FrameworkValidator::new();
1134
1135 match validator.validate_dioxus_option("--hot-reloads", None) {
1136 ValidationResult::InvalidOption { suggestion } => {
1137 assert_eq!(suggestion, Some("--hot-reload".to_string()));
1138 }
1139 _ => panic!("Expected InvalidOption result"),
1140 }
1141 }
1142
1143 #[test]
1144 fn test_leptos_typo_suggestions() {
1145 let validator = FrameworkValidator::new();
1146
1147 match validator.validate_leptos_option("--precompres", None) {
1148 ValidationResult::InvalidOption { suggestion } => {
1149 assert_eq!(suggestion, Some("--precompress".to_string()));
1150 }
1151 _ => panic!("Expected InvalidOption result"),
1152 }
1153 }
1154
1155 #[test]
1156 fn test_port_validation() {
1157 let validator = FrameworkValidator::new();
1158
1159 assert!(matches!(
1161 validator.validate_dioxus_option("--port", Some("3000")),
1162 ValidationResult::Valid
1163 ));
1164 assert!(matches!(
1165 validator.validate_dioxus_option("--port", Some("8080")),
1166 ValidationResult::Valid
1167 ));
1168
1169 match validator.validate_dioxus_option("--port", Some("0")) {
1171 ValidationResult::InvalidValue { .. } => {}
1172 _ => panic!("Expected InvalidValue for port 0"),
1173 }
1174
1175 match validator.validate_dioxus_option("--port", Some("abc")) {
1176 ValidationResult::InvalidValue { .. } => {}
1177 _ => panic!("Expected InvalidValue for non-numeric port"),
1178 }
1179 }
1180
1181 #[test]
1182 fn test_address_validation() {
1183 let validator = FrameworkValidator::new();
1184
1185 assert!(matches!(
1187 validator.validate_dioxus_option("--addr", Some("localhost")),
1188 ValidationResult::Valid
1189 ));
1190 assert!(matches!(
1191 validator.validate_dioxus_option("--addr", Some("127.0.0.1")),
1192 ValidationResult::Valid
1193 ));
1194 assert!(matches!(
1195 validator.validate_dioxus_option("--addr", Some("0.0.0.0")),
1196 ValidationResult::Valid
1197 ));
1198
1199 }
1201
1202 #[test]
1203 fn test_leptos_js_minify() {
1204 let validator = FrameworkValidator::new();
1205
1206 assert!(matches!(
1208 validator.validate_leptos_option("--js-minify", Some("true")),
1209 ValidationResult::Valid
1210 ));
1211 assert!(matches!(
1212 validator.validate_leptos_option("--js-minify", Some("false")),
1213 ValidationResult::Valid
1214 ));
1215
1216 match validator.validate_leptos_option("--js-minify", Some("maybe")) {
1218 ValidationResult::InvalidValue {
1219 expected,
1220 got,
1221 suggestions,
1222 } => {
1223 assert_eq!(expected, "true or false");
1224 assert_eq!(got, "maybe");
1225 assert!(!suggestions.is_empty());
1226 }
1227 _ => panic!("Expected InvalidValue result"),
1228 }
1229 }
1230}