Compare commits

..

No commits in common. "83a2ba0f8a4351b209387d168cb53061f8116d5d" and "5d7629487a9280a1ff8a603d5080d4d645f1d895" have entirely different histories.

5 changed files with 50 additions and 167 deletions

View file

@ -34,20 +34,13 @@ groups:
routes: routes:
# /ls is the only page that comes with bunbun besides the homepage. This # /ls is the only page that comes with bunbun besides the homepage. This
# page provides a full list of routes and their groups they're in. # page provides a full list of routes and their groups they're in.
ls: &ls ls: &ls "/ls"
path: "/ls"
# You can specify a maximum number of arguments, which are string
# delimited strings.
max_args: 0
# You can also specify a minimum amount of arguments.
# min_args: 1
help: help:
path: &ls "/ls" # Bunbun supports all standard YAML features, so things like YAML pointers
max_args: 0 # and references are supported.
path: *ls
# Paths can be hidden from the listings page if desired. # Paths can be hidden from the listings page if desired.
hidden: true hidden: true
# Bunbun supports all standard YAML features, so things like YAML pointers
# and references are supported.
list: *ls list: *ls
- -
# This is another group without a description # This is another group without a description

View file

@ -2,7 +2,7 @@ use crate::BunBunError;
use dirs::{config_dir, home_dir}; use dirs::{config_dir, home_dir};
use log::{debug, info, trace}; use log::{debug, info, trace};
use serde::{ use serde::{
de::{self, Deserializer, MapAccess, Unexpected, Visitor}, de::{self, Deserializer, MapAccess, Visitor},
Deserialize, Serialize, Deserialize, Serialize,
}; };
use std::collections::HashMap; use std::collections::HashMap;
@ -38,8 +38,6 @@ pub struct Route {
pub path: String, pub path: String,
pub hidden: bool, pub hidden: bool,
pub description: Option<String>, pub description: Option<String>,
pub min_args: Option<usize>,
pub max_args: Option<usize>,
} }
impl FromStr for Route { impl FromStr for Route {
@ -50,8 +48,6 @@ impl FromStr for Route {
path: s.to_string(), path: s.to_string(),
hidden: false, hidden: false,
description: None, description: None,
min_args: None,
max_args: None,
}) })
} }
} }
@ -67,13 +63,11 @@ impl<'de> Deserialize<'de> for Route {
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")] #[serde(field_identifier, rename_all = "lowercase")]
enum Field { enum Field {
Path, Path,
Hidden, Hidden,
Description, Description,
MinArgs,
MaxArgs,
} }
struct RouteVisitor; struct RouteVisitor;
@ -89,7 +83,7 @@ impl<'de> Deserialize<'de> for Route {
where where
E: serde::de::Error, E: serde::de::Error,
{ {
// This is infallable // This is infalliable
Ok(Self::Value::from_str(path).unwrap()) Ok(Self::Value::from_str(path).unwrap())
} }
@ -100,8 +94,6 @@ impl<'de> Deserialize<'de> for Route {
let mut path = None; let mut path = None;
let mut hidden = None; let mut hidden = None;
let mut description = None; let mut description = None;
let mut min_args = None;
let mut max_args = None;
while let Some(key) = map.next_key()? { while let Some(key) = map.next_key()? {
match key { match key {
@ -123,32 +115,6 @@ impl<'de> Deserialize<'de> for Route {
} }
description = Some(map.next_value()?); description = Some(map.next_value()?);
} }
Field::MinArgs => {
if min_args.is_some() {
return Err(de::Error::duplicate_field("min_args"));
}
min_args = Some(map.next_value()?);
}
Field::MaxArgs => {
if max_args.is_some() {
return Err(de::Error::duplicate_field("max_args"));
}
max_args = Some(map.next_value()?);
}
}
}
if let (Some(min_args), Some(max_args)) = (min_args, max_args) {
if min_args > max_args {
{
return Err(de::Error::invalid_value(
Unexpected::Other(&format!(
"argument count range {} to {}",
min_args, max_args
)),
&"a valid argument count range",
));
}
} }
} }
@ -158,8 +124,6 @@ impl<'de> Deserialize<'de> for Route {
path, path,
hidden: hidden.unwrap_or_default(), hidden: hidden.unwrap_or_default(),
description, description,
min_args,
max_args,
}) })
} }
} }
@ -373,7 +337,7 @@ mod route {
fn serialize() { fn serialize() {
assert_eq!( assert_eq!(
&to_string(&Route::from_str("hello world").unwrap()).unwrap(), &to_string(&Route::from_str("hello world").unwrap()).unwrap(),
"---\nroute_type: External\npath: hello world\nhidden: false\ndescription: ~\nmin_args: ~\nmax_args: ~" "---\nroute_type: External\npath: hello world\nhidden: false\ndescription: ~"
); );
} }
} }

View file

@ -66,7 +66,6 @@ async fn run() -> Result<(), BunBunError> {
groups: conf.groups, groups: conf.groups,
})); }));
// Cannot be named _ or Rust will immediately drop it.
let _watch = start_watch(Arc::clone(&state), conf_data, opts.large_config)?; let _watch = start_watch(Arc::clone(&state), conf_data, opts.large_config)?;
HttpServer::new(move || { HttpServer::new(move || {

View file

@ -90,7 +90,7 @@ pub async fn hop(
let data = data.read().unwrap(); let data = data.read().unwrap();
match resolve_hop(&query.to, &data.routes, &data.default_route) { match resolve_hop(&query.to, &data.routes, &data.default_route) {
RouteResolution::Resolved { route: path, args } => { (Some(path), args) => {
let resolved_template = match path { let resolved_template = match path {
ConfigRoute { ConfigRoute {
route_type: RouteType::Internal, route_type: RouteType::Internal,
@ -126,16 +126,10 @@ pub async fn hop(
} }
} }
} }
RouteResolution::Unresolved => HttpResponse::NotFound().body("not found"), (None, _) => HttpResponse::NotFound().body("not found"),
} }
} }
#[derive(Debug, PartialEq)]
enum RouteResolution<'a> {
Resolved { route: &'a Route, args: String },
Unresolved,
}
/// Attempts to resolve the provided string into its route and its arguments. /// Attempts to resolve the provided string into its route and its arguments.
/// If a default route was provided, then this will consider that route before /// If a default route was provided, then this will consider that route before
/// failing to resolve a route. /// failing to resolve a route.
@ -146,61 +140,48 @@ fn resolve_hop<'a>(
query: &str, query: &str,
routes: &'a HashMap<String, Route>, routes: &'a HashMap<String, Route>,
default_route: &Option<String>, default_route: &Option<String>,
) -> RouteResolution<'a> { ) -> (Option<&'a Route>, String) {
let mut split_args = query.split_ascii_whitespace().peekable(); let mut split_args = query.split_ascii_whitespace().peekable();
let maybe_route = { let command = match split_args.peek() {
match split_args.peek() { Some(command) => command,
Some(command) => routes.get(*command), None => {
None => { debug!("Found empty query, returning no route.");
debug!("Found empty query, returning no route."); return (None, String::new());
return RouteResolution::Unresolved;
}
} }
}; };
let args = split_args.collect::<Vec<_>>(); match (routes.get(*command), default_route) {
let arg_count = args.len(); // Found a route
(Some(resolved), _) => (
// Try resolving with a matched command Some(resolved),
if let Some(route) = maybe_route { match split_args.next() {
let args = if args.is_empty() { &[] } else { &args[1..] }.join(" "); // Discard the first result, we found the route using the first arg
let arg_count = arg_count - 1; Some(_) => {
if check_route(route, arg_count) { let args = split_args.collect::<Vec<&str>>().join(" ");
debug!("Resolved {} with args {}", route, args); debug!("Resolved {} with args {}", resolved, args);
return RouteResolution::Resolved { route, args }; args
} }
} None => {
debug!("Resolved {} with no args", resolved);
// Try resolving with the default route, if it exists String::new()
if let Some(route) = default_route { }
if let Some(route) = routes.get(route) { },
if check_route(route, arg_count) { ),
let args = args.join(" "); // Unable to find route, but had a default route
debug!("Using default route {} with args {}", route, args); (None, Some(route)) => {
return RouteResolution::Resolved { route, args }; let args = split_args.collect::<Vec<&str>>().join(" ");
debug!("Using default route {} with args {}", route, args);
match routes.get(route) {
Some(v) => (Some(v), args),
None => (None, String::new()),
} }
} }
} // No default route and no match
(None, None) => {
RouteResolution::Unresolved debug!("Failed to resolve route!");
} (None, String::new())
/// 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 /// Runs the executable with the user's input as a single argument. Returns Ok
@ -230,18 +211,15 @@ mod resolve_hop {
fn generate_route_result<'a>( fn generate_route_result<'a>(
keyword: &'a Route, keyword: &'a Route,
args: &str, args: &str,
) -> RouteResolution<'a> { ) -> (Option<&'a Route>, String) {
RouteResolution::Resolved { (Some(keyword), String::from(args))
route: keyword,
args: String::from(args),
}
} }
#[test] #[test]
fn empty_routes_no_default_yields_failed_hop() { fn empty_routes_no_default_yields_failed_hop() {
assert_eq!( assert_eq!(
resolve_hop("hello world", &HashMap::new(), &None), resolve_hop("hello world", &HashMap::new(), &None),
RouteResolution::Unresolved (None, String::new())
); );
} }
@ -253,7 +231,7 @@ mod resolve_hop {
&HashMap::new(), &HashMap::new(),
&Some(String::from("google")) &Some(String::from("google"))
), ),
RouteResolution::Unresolved (None, String::new())
); );
} }
@ -306,57 +284,6 @@ mod resolve_hop {
} }
} }
#[cfg(test)]
mod check_route {
use super::*;
fn create_route(
min_args: impl Into<Option<usize>>,
max_args: impl Into<Option<usize>>,
) -> 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)] #[cfg(test)]
mod resolve_path { mod resolve_path {
use super::resolve_path; use super::resolve_path;

View file

@ -4,6 +4,6 @@
<Description>Hop to where you need to go</Description> <Description>Hop to where you need to go</Description>
<InputEncoding>UTF-8</InputEncoding> <InputEncoding>UTF-8</InputEncoding>
<!--<Image width="16" height="16">data:image/x-icon;base64,</Image>--> <!--<Image width="16" height="16">data:image/x-icon;base64,</Image>-->
<Url type="text/html" template="http://{{hostname}}/hop?to={searchTerms}" /> <Url type="text/html" template="http://{{hostname}}/hop?to={searchTerms}"></Url>
<Url type="application/x-moz-keywordsearch" template="http://{{hostname}}/hop?to={searchTerms}" /> <Url type="application/x-moz-keywordsearch" template="http://{{hostname}}/hop?to={searchTerms}"></Url>
</OpenSearchDescription> </OpenSearchDescription>