Compare commits
No commits in common. "83a2ba0f8a4351b209387d168cb53061f8116d5d" and "5d7629487a9280a1ff8a603d5080d4d645f1d895" have entirely different histories.
83a2ba0f8a
...
5d7629487a
5 changed files with 50 additions and 167 deletions
|
@ -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"
|
|
||||||
max_args: 0
|
|
||||||
# Paths can be hidden from the listings page if desired.
|
|
||||||
hidden: true
|
|
||||||
# Bunbun supports all standard YAML features, so things like YAML pointers
|
# Bunbun supports all standard YAML features, so things like YAML pointers
|
||||||
# and references are supported.
|
# and references are supported.
|
||||||
|
path: *ls
|
||||||
|
# Paths can be hidden from the listings page if desired.
|
||||||
|
hidden: true
|
||||||
list: *ls
|
list: *ls
|
||||||
-
|
-
|
||||||
# This is another group without a description
|
# This is another group without a description
|
||||||
|
|
|
@ -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: ~"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 || {
|
||||||
|
|
143
src/routes.rs
143
src/routes.rs
|
@ -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 RouteResolution::Unresolved;
|
return (None, String::new());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
String::new()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Try resolving with the default route, if it exists
|
),
|
||||||
if let Some(route) = default_route {
|
// Unable to find route, but had a default route
|
||||||
if let Some(route) = routes.get(route) {
|
(None, Some(route)) => {
|
||||||
if check_route(route, arg_count) {
|
let args = split_args.collect::<Vec<&str>>().join(" ");
|
||||||
let args = args.join(" ");
|
|
||||||
debug!("Using default route {} with args {}", route, args);
|
debug!("Using default route {} with args {}", route, args);
|
||||||
return RouteResolution::Resolved { 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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue