diff --git a/src/config.rs b/src/config.rs index 791ca74..3fc90c5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,7 +67,7 @@ impl<'de> Deserialize<'de> for Route { D: Deserializer<'de>, { #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] + #[serde(field_identifier, rename_all = "snake_case")] enum Field { Path, Hidden, diff --git a/src/main.rs b/src/main.rs index ec92f9f..4284dd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,7 @@ async fn run() -> Result<(), BunBunError> { groups: conf.groups, })); + // Cannot be named _ or Rust will immediately drop it. let _watch = start_watch(Arc::clone(&state), conf_data, opts.large_config)?; HttpServer::new(move || { diff --git a/src/routes.rs b/src/routes.rs index 5e848f5..3ecb893 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -158,29 +158,51 @@ fn resolve_hop<'a>( } }; - if maybe_route.is_some() { - split_args.next(); - } - - let args = split_args.collect::>().join(" "); + let args = split_args.collect::>(); + let arg_count = args.len(); // Try resolving with a matched command if let Some(route) = maybe_route { - debug!("Resolved {} with args {}", route, args); - return RouteResolution::Resolved { route, args }; + let args = if args.is_empty() { &[] } else { &args[1..] }.join(" "); + let arg_count = arg_count - 1; + if check_route(route, arg_count) { + debug!("Resolved {} with args {}", route, args); + return RouteResolution::Resolved { route, args }; + } } // Try resolving with the default route, if it exists if let Some(route) = default_route { if let Some(route) = routes.get(route) { - debug!("Using default route {} with args {}", route, args); - return RouteResolution::Resolved { route, args }; + if check_route(route, arg_count) { + let args = args.join(" "); + debug!("Using default route {} with args {}", route, args); + return RouteResolution::Resolved { route, args }; + } } } RouteResolution::Unresolved } +/// Checks if the user provided string has the correct properties required by +/// the route to be successfully matched. +fn check_route(route: &Route, arg_count: usize) -> bool { + if let Some(min_args) = route.min_args { + if arg_count < min_args { + return false; + } + } + + if let Some(max_args) = route.max_args { + if arg_count > max_args { + return false; + } + } + + true +} + /// Runs the executable with the user's input as a single argument. Returns Ok /// so long as the executable was successfully executed. Returns an Error if the /// file doesn't exist or bunbun did not have permission to read and execute the @@ -284,6 +306,57 @@ mod resolve_hop { } } +#[cfg(test)] +mod check_route { + use super::*; + + fn create_route( + min_args: impl Into>, + max_args: impl Into>, + ) -> Route { + Route { + description: None, + hidden: false, + max_args: max_args.into(), + min_args: min_args.into(), + path: String::new(), + route_type: RouteType::External, + } + } + + #[test] + fn no_min_arg_no_max_arg_counts() { + assert!(check_route(&create_route(None, None), 0)); + assert!(check_route(&create_route(None, None), usize::MAX)); + } + + #[test] + fn min_arg_no_max_arg_counts() { + assert!(!check_route(&create_route(3, None), 0)); + assert!(!check_route(&create_route(3, None), 2)); + assert!(check_route(&create_route(3, None), 3)); + assert!(check_route(&create_route(3, None), 4)); + assert!(check_route(&create_route(3, None), usize::MAX)); + } + + #[test] + fn no_min_arg_max_arg_counts() { + assert!(check_route(&create_route(None, 3), 0)); + assert!(check_route(&create_route(None, 3), 2)); + assert!(check_route(&create_route(None, 3), 3)); + assert!(!check_route(&create_route(None, 3), 4)); + assert!(!check_route(&create_route(None, 3), usize::MAX)); + } + + #[test] + fn min_arg_max_arg_counts() { + assert!(!check_route(&create_route(2, 3), 1)); + assert!(check_route(&create_route(2, 3), 2)); + assert!(check_route(&create_route(2, 3), 3)); + assert!(!check_route(&create_route(2, 3), 4)); + } +} + #[cfg(test)] mod resolve_path { use super::resolve_path;