raz_core/
dioxus_validation.rs

1//! Comprehensive Framework CLI validation system
2//!
3//! Provides validation for Dioxus (`dx serve`) and Leptos (`cargo-leptos watch`)
4//! options with proper type checking, enum validation, and intelligent suggestions for typos.
5
6use regex::Regex;
7use std::collections::HashMap;
8
9/// Dioxus option value types
10#[derive(Debug, Clone, PartialEq)]
11pub enum DioxusOptionType {
12    /// Flag option (no value required)
13    Flag,
14    /// Boolean option with explicit true/false values
15    Bool { default: Option<bool> },
16    /// String value
17    String { default: Option<String> },
18    /// Port number (1-65535)
19    Port { default: Option<u16> },
20    /// Network address
21    Address { default: Option<String> },
22    /// Platform enum
23    Platform { default: Option<String> },
24    /// Architecture enum
25    Architecture,
26    /// Profile name
27    Profile { default: Option<String> },
28    /// Features list (space-separated)
29    Features,
30    /// Multiple string values
31    Multiple,
32    /// Interval in seconds
33    Interval { default: Option<u64> },
34}
35
36/// Dioxus option metadata
37#[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/// Validation result
47#[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
63/// Framework CLI validator for Dioxus and Leptos
64pub 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
72/// Legacy alias for backward compatibility
73pub 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 DX SERVE OPTIONS =====
81
82        // Port option
83        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        // Address option
95        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        // Open option - boolean with default true
109        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        // Hot reload option - boolean with default true
123        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        // Always on top option - boolean with default true
137        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        // Cross origin policy - flag
151        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        // Args option
163        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        // WSL file poll interval
175        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        // Interactive option - boolean with optional value
188        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        // Release flag
202        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        // Force sequential flag
214        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        // Profile option
226        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        // Verbose flag
238        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        // Server profile option
250        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        // Trace flag
264        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        // JSON output flag
276        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        // Platform option - enum with specific values
288        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        // Fullstack flag
304        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        // SSG flag
316        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        // Skip assets flag
328        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        // Inject loading scripts flag
340        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        // Debug symbols flag
352        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        // Nightly flag
364        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        // Example option
376        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        // Bin option
390        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        // Package option
404        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        // Features option
416        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        // Client features option
428        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        // Server features option
442        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        // No default features flag
456        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        // Architecture option
468        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        // Device option - boolean
480        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        // Target option
492        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 CARGO-LEPTOS WATCH OPTIONS =====
504
505        // Release flag
506        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        // Precompress flag
518        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        // Hot reload flag
527        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        // Project option
539        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        // Features option
551        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        // Lib features option
563        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        // Lib cargo args option
575        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        // Bin features option
587        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        // Bin cargo args option
599        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        // WASM debug flag
611        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        // Verbosity option (multiple v's allowed)
620        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        // JS minify option
633        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    /// Validate a framework option and its value
658    pub fn validate_option(
659        &self,
660        framework: &str,
661        option: &str,
662        value: Option<&str>,
663    ) -> ValidationResult {
664        // Check if option exists in the specified framework
665        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                // Try to find a close match for typo suggestion
675                let suggestion = self.find_closest_option(framework, option);
676                return ValidationResult::InvalidOption { suggestion };
677            }
678        };
679
680        // Validate based on option type
681        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                    // For boolean options, missing value might be valid (uses default)
709                    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    /// Find the closest matching option for typo suggestions
823    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    /// Suggest boolean values based on input
845    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    /// Suggest platform values based on input
857    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            // Provide common suggestions
869            suggestions.extend_from_slice(&["web".to_string(), "desktop".to_string()]);
870        }
871
872        suggestions
873    }
874
875    /// Suggest architecture values based on input
876    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    /// Basic address validation
894    fn is_valid_address(&self, addr: &str) -> bool {
895        // Allow localhost, IP addresses, and hostnames
896        if addr == "localhost" || addr == "0.0.0.0" {
897            return true;
898        }
899
900        // Simple IP address validation
901        let ip_regex = Regex::new(r"^(\d{1,3}\.){3}\d{1,3}$").unwrap();
902        if ip_regex.is_match(addr) {
903            // Check each octet is valid (0-255)
904            return addr.split('.').all(|octet| octet.parse::<u8>().is_ok());
905        }
906
907        // Allow valid hostnames (basic check)
908        let hostname_regex = Regex::new(r"^[a-zA-Z0-9.-]+$").unwrap();
909        hostname_regex.is_match(addr)
910    }
911
912    /// Check if an option is valid for the specified framework
913    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    /// Check if an option is valid for dx serve (legacy method)
922    pub fn is_valid_serve_option(&self, option: &str) -> bool {
923        self.is_valid_option("dioxus", option)
924    }
925
926    /// Get option information for specified framework
927    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    /// Get all valid options for specified framework
936    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    /// Convenience method for Dioxus validation
945    pub fn validate_dioxus_option(&self, option: &str, value: Option<&str>) -> ValidationResult {
946        self.validate_option("dioxus", option, value)
947    }
948
949    /// Convenience method for Leptos validation
950    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
961/// Calculate Levenshtein distance for typo suggestions
962fn 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        // Test flag options
1002        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        // Test boolean options
1012        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        // Test port option
1026        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        // Test platform option
1036        assert!(matches!(
1037            validator.validate_dioxus_option("--platform", Some("web")),
1038            ValidationResult::Valid
1039        ));
1040
1041        // Test architecture option
1042        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        // Test flag options
1057        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        // Test boolean options
1071        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        // Test string options
1081        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        // Valid ports
1160        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        // Invalid ports
1170        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        // Valid addresses
1186        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        // Invalid addresses would be caught by the validation logic
1200    }
1201
1202    #[test]
1203    fn test_leptos_js_minify() {
1204        let validator = FrameworkValidator::new();
1205
1206        // Valid boolean values
1207        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        // Invalid boolean value
1217        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}