Compare commits
7 commits
5a122371da
...
e619b2cffc
Author | SHA1 | Date | |
---|---|---|---|
e619b2cffc | |||
6e2deccf24 | |||
71df3394ad | |||
c990aef0e9 | |||
1385045013 | |||
a4543c48ec | |||
cf85a6494a |
8 changed files with 406 additions and 189 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -6,5 +6,6 @@
|
||||||
"bunbunsearch",
|
"bunbunsearch",
|
||||||
"itertools",
|
"itertools",
|
||||||
"opensearchdescription"
|
"opensearchdescription"
|
||||||
]
|
],
|
||||||
|
"python.pythonPath": "/usr/bin/python3"
|
||||||
}
|
}
|
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -113,7 +113,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-server"
|
name = "actix-server"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -145,7 +145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-server 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-server 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-service 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-service 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -206,7 +206,7 @@ dependencies = [
|
||||||
"actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-router 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-router 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-server 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-server 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-service 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-service 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-testing 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-testing 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -1738,7 +1738,7 @@ dependencies = [
|
||||||
"checksum actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "21705adc76bbe4bc98434890e73a89cd00c6015e5704a60bb6eea6c3b72316b6"
|
"checksum actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "21705adc76bbe4bc98434890e73a89cd00c6015e5704a60bb6eea6c3b72316b6"
|
||||||
"checksum actix-router 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad01d9350616bbf91c7a651b40b9205a58076a069c7b8094d15e2fcf17c2edc"
|
"checksum actix-router 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad01d9350616bbf91c7a651b40b9205a58076a069c7b8094d15e2fcf17c2edc"
|
||||||
"checksum actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6a0a55507046441a496b2f0d26a84a65e67c8cafffe279072412f624b5fb6d"
|
"checksum actix-rt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6a0a55507046441a496b2f0d26a84a65e67c8cafffe279072412f624b5fb6d"
|
||||||
"checksum actix-server 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5d8a9cd23ebd54ebfd8dc4f93313c142f9d2d505b3e40865d6b384fedfc4748"
|
"checksum actix-server 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51d3455eaac03ca3e49d7b822eb35c884b861f715627254ccbe4309d08f1841a"
|
||||||
"checksum actix-service 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a5ecef49693fcfe2c13a34c7218eb5b7898ff3fbe334db8445759f871fec2df"
|
"checksum actix-service 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a5ecef49693fcfe2c13a34c7218eb5b7898ff3fbe334db8445759f871fec2df"
|
||||||
"checksum actix-testing 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "48494745b72d0ea8ff0cf874aaf9b622a3ee03d7081ee0c04edea4f26d32c911"
|
"checksum actix-testing 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "48494745b72d0ea8ff0cf874aaf9b622a3ee03d7081ee0c04edea4f26d32c911"
|
||||||
"checksum actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf4082192601de5f303013709ff84d81ca6a1bc4af7fb24f367a500a23c6e84e"
|
"checksum actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf4082192601de5f303013709ff84d81ca6a1bc4af7fb24f367a500a23c6e84e"
|
||||||
|
|
|
@ -7,6 +7,7 @@ description = "Re-implementation of bunny1 in Rust"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/edward-shen/bunbun"
|
repository = "https://github.com/edward-shen/bunbun"
|
||||||
|
exclude = ["/aux/"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "2.0"
|
actix-web = "2.0"
|
||||||
|
@ -20,6 +21,7 @@ itertools = "0.8"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
simple_logger = "1.3"
|
simple_logger = "1.3"
|
||||||
clap = { version = "2.33", features = ["yaml", "wrap_help"] }
|
clap = { version = "2.33", features = ["yaml", "wrap_help"] }
|
||||||
|
# rlua = "0.17"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
|
|
|
@ -15,6 +15,17 @@ default_route: "g"
|
||||||
# contain "{{query}}", which will be populated by the user's search query. This
|
# contain "{{query}}", which will be populated by the user's search query. This
|
||||||
# input is percent-escaped. If multiple routes are defined, then the later
|
# input is percent-escaped. If multiple routes are defined, then the later
|
||||||
# defined route is used.
|
# defined route is used.
|
||||||
|
#
|
||||||
|
# You may provide an (absolute, recommended) path to an executable file to out-
|
||||||
|
# source route resolution to a program. The program will receive one argument
|
||||||
|
# only, which is the entire string provided from the user after matching the
|
||||||
|
# route. It is up to the out-sourced program to parse the arguments and to
|
||||||
|
# interpret those arguments. These programs should print one line to standard
|
||||||
|
# out, which should be a fully resolved URL to lead the user to.
|
||||||
|
#
|
||||||
|
# These programs must be developed defensively, as they accept arbitrary user
|
||||||
|
# input. Improper handling of user input can easily lead to anywhere from simple
|
||||||
|
# flakey responses to remote code execution.
|
||||||
groups:
|
groups:
|
||||||
-
|
-
|
||||||
name: "Meta commands"
|
name: "Meta commands"
|
||||||
|
|
145
src/config.rs
Normal file
145
src/config.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
use crate::{routes::Route, BunBunError};
|
||||||
|
use log::{debug, error, info, trace};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::{read_to_string, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml");
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, PartialEq)]
|
||||||
|
pub struct Config {
|
||||||
|
pub bind_address: String,
|
||||||
|
pub public_address: String,
|
||||||
|
pub default_route: Option<String>,
|
||||||
|
pub groups: Vec<RouteGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
|
||||||
|
pub struct RouteGroup {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub routes: HashMap<String, Route>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO implement rlua:
|
||||||
|
// # use rlua::{Lua, Result};
|
||||||
|
// # fn main() -> Result<()> {
|
||||||
|
// let lua = Lua::new();
|
||||||
|
// lua.context(|lua_context| {
|
||||||
|
// lua_context.load(r#"
|
||||||
|
// print("hello world!")
|
||||||
|
// "#).exec()
|
||||||
|
// })?;
|
||||||
|
// # Ok(())
|
||||||
|
// # }
|
||||||
|
|
||||||
|
/// Attempts to read the config file. If it doesn't exist, generate one a
|
||||||
|
/// default config file before attempting to parse it.
|
||||||
|
pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
|
||||||
|
trace!("Loading config file...");
|
||||||
|
let config_str = match read_to_string(config_file_path) {
|
||||||
|
Ok(conf_str) => {
|
||||||
|
debug!("Successfully loaded config file into memory.");
|
||||||
|
conf_str
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
info!(
|
||||||
|
"Unable to find a {} file. Creating default!",
|
||||||
|
config_file_path
|
||||||
|
);
|
||||||
|
|
||||||
|
let fd = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(config_file_path);
|
||||||
|
|
||||||
|
match fd {
|
||||||
|
Ok(mut fd) => fd.write_all(DEFAULT_CONFIG)?,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to write to {}: {}. Default config will be loaded but not saved.", config_file_path, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
String::from_utf8_lossy(DEFAULT_CONFIG).into_owned()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reading from memory is faster than reading directly from a reader for some
|
||||||
|
// reason; see https://github.com/serde-rs/json/issues/160
|
||||||
|
Ok(serde_yaml::from_str(&config_str)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod read_config {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_default_config_if_path_does_not_exist() {
|
||||||
|
assert_eq!(
|
||||||
|
read_config("/this_is_a_non_existent_file").unwrap(),
|
||||||
|
serde_yaml::from_slice(DEFAULT_CONFIG).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_error_if_given_empty_config() {
|
||||||
|
assert_eq!(
|
||||||
|
read_config("/dev/null").unwrap_err().to_string(),
|
||||||
|
"EOF while parsing a value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_error_if_given_invalid_config() -> Result<(), std::io::Error> {
|
||||||
|
let mut tmp_file = NamedTempFile::new()?;
|
||||||
|
tmp_file.write_all(b"g")?;
|
||||||
|
assert_eq!(
|
||||||
|
read_config(tmp_file.path().to_str().unwrap())
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
r#"invalid type: string "g", expected struct Config at line 1 column 1"#
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_error_if_config_missing_field() -> Result<(), std::io::Error> {
|
||||||
|
let mut tmp_file = NamedTempFile::new()?;
|
||||||
|
tmp_file.write_all(
|
||||||
|
br#"
|
||||||
|
bind_address: "localhost"
|
||||||
|
public_address: "localhost"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
read_config(tmp_file.path().to_str().unwrap())
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
"missing field `groups` at line 2 column 19"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_ok_if_valid_config() -> Result<(), std::io::Error> {
|
||||||
|
let mut tmp_file = NamedTempFile::new()?;
|
||||||
|
tmp_file.write_all(
|
||||||
|
br#"
|
||||||
|
bind_address: "a"
|
||||||
|
public_address: "b"
|
||||||
|
groups: []"#,
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
read_config(tmp_file.path().to_str().unwrap()).unwrap(),
|
||||||
|
Config {
|
||||||
|
bind_address: String::from("a"),
|
||||||
|
public_address: String::from("b"),
|
||||||
|
groups: vec![],
|
||||||
|
default_route: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
12
src/error.rs
12
src/error.rs
|
@ -8,6 +8,7 @@ pub enum BunBunError {
|
||||||
ParseError(serde_yaml::Error),
|
ParseError(serde_yaml::Error),
|
||||||
WatchError(hotwatch::Error),
|
WatchError(hotwatch::Error),
|
||||||
LoggerInitError(log::SetLoggerError),
|
LoggerInitError(log::SetLoggerError),
|
||||||
|
CustomProgramError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for BunBunError {}
|
impl Error for BunBunError {}
|
||||||
|
@ -15,10 +16,11 @@ impl Error for BunBunError {}
|
||||||
impl fmt::Display for BunBunError {
|
impl fmt::Display for BunBunError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
BunBunError::IoError(e) => e.fmt(f),
|
Self::IoError(e) => e.fmt(f),
|
||||||
BunBunError::ParseError(e) => e.fmt(f),
|
Self::ParseError(e) => e.fmt(f),
|
||||||
BunBunError::WatchError(e) => e.fmt(f),
|
Self::WatchError(e) => e.fmt(f),
|
||||||
BunBunError::LoggerInitError(e) => e.fmt(f),
|
Self::LoggerInitError(e) => e.fmt(f),
|
||||||
|
Self::CustomProgramError(msg) => write!(f, "{}", msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +31,7 @@ macro_rules! from_error {
|
||||||
($from:ty, $to:ident) => {
|
($from:ty, $to:ident) => {
|
||||||
impl From<$from> for BunBunError {
|
impl From<$from> for BunBunError {
|
||||||
fn from(e: $from) -> Self {
|
fn from(e: $from) -> Self {
|
||||||
BunBunError::$to(e)
|
Self::$to(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
165
src/main.rs
165
src/main.rs
|
@ -1,25 +1,22 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use crate::config::{read_config, RouteGroup};
|
||||||
use actix_web::{middleware::Logger, App, HttpServer};
|
use actix_web::{middleware::Logger, App, HttpServer};
|
||||||
use clap::{crate_authors, crate_version, load_yaml, App as ClapApp};
|
use clap::{crate_authors, crate_version, load_yaml, App as ClapApp};
|
||||||
use error::BunBunError;
|
use error::BunBunError;
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use hotwatch::{Event, Hotwatch};
|
use hotwatch::{Event, Hotwatch};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, info, trace, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{read_to_string, OpenOptions};
|
|
||||||
use std::io::Write;
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod template_args;
|
mod template_args;
|
||||||
|
|
||||||
const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml");
|
|
||||||
|
|
||||||
/// Dynamic variables that either need to be present at runtime, or can be
|
/// Dynamic variables that either need to be present at runtime, or can be
|
||||||
/// changed during runtime.
|
/// changed during runtime.
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
@ -27,7 +24,7 @@ pub struct State {
|
||||||
default_route: Option<String>,
|
default_route: Option<String>,
|
||||||
groups: Vec<RouteGroup>,
|
groups: Vec<RouteGroup>,
|
||||||
/// Cached, flattened mapping of all routes and their destinations.
|
/// Cached, flattened mapping of all routes and their destinations.
|
||||||
routes: HashMap<String, String>,
|
routes: HashMap<String, routes::Route>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
|
@ -97,61 +94,10 @@ fn init_logger(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, PartialEq)]
|
|
||||||
struct Config {
|
|
||||||
bind_address: String,
|
|
||||||
public_address: String,
|
|
||||||
default_route: Option<String>,
|
|
||||||
groups: Vec<RouteGroup>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
|
|
||||||
struct RouteGroup {
|
|
||||||
name: String,
|
|
||||||
description: Option<String>,
|
|
||||||
routes: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to read the config file. If it doesn't exist, generate one a
|
|
||||||
/// default config file before attempting to parse it.
|
|
||||||
fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
|
|
||||||
trace!("Loading config file...");
|
|
||||||
let config_str = match read_to_string(config_file_path) {
|
|
||||||
Ok(conf_str) => {
|
|
||||||
debug!("Successfully loaded config file into memory.");
|
|
||||||
conf_str
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
info!(
|
|
||||||
"Unable to find a {} file. Creating default!",
|
|
||||||
config_file_path
|
|
||||||
);
|
|
||||||
|
|
||||||
let fd = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.create_new(true)
|
|
||||||
.open(config_file_path);
|
|
||||||
|
|
||||||
match fd {
|
|
||||||
Ok(mut fd) => fd.write_all(DEFAULT_CONFIG)?,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to write to {}: {}. Default config will be loaded but not saved.", config_file_path, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
String::from_utf8_lossy(DEFAULT_CONFIG).into_owned()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reading from memory is faster than reading directly from a reader for some
|
|
||||||
// reason; see https://github.com/serde-rs/json/issues/160
|
|
||||||
Ok(serde_yaml::from_str(&config_str)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a hashmap of routes from the data structure created by the config
|
/// Generates a hashmap of routes from the data structure created by the config
|
||||||
/// file. This should improve runtime performance and is a better solution than
|
/// file. This should improve runtime performance and is a better solution than
|
||||||
/// just iterating over the config object for every hop resolution.
|
/// just iterating over the config object for every hop resolution.
|
||||||
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, String> {
|
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, routes::Route> {
|
||||||
let mut mapping = HashMap::new();
|
let mut mapping = HashMap::new();
|
||||||
for group in groups {
|
for group in groups {
|
||||||
for (kw, dest) in &group.routes {
|
for (kw, dest) in &group.routes {
|
||||||
|
@ -265,90 +211,18 @@ mod init_logger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod read_config {
|
|
||||||
use super::*;
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn returns_default_config_if_path_does_not_exist() {
|
|
||||||
assert_eq!(
|
|
||||||
read_config("/this_is_a_non_existent_file").unwrap(),
|
|
||||||
serde_yaml::from_slice(DEFAULT_CONFIG).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn returns_error_if_given_empty_config() {
|
|
||||||
assert_eq!(
|
|
||||||
read_config("/dev/null").unwrap_err().to_string(),
|
|
||||||
"EOF while parsing a value"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn returns_error_if_given_invalid_config() -> Result<(), std::io::Error> {
|
|
||||||
let mut tmp_file = NamedTempFile::new()?;
|
|
||||||
tmp_file.write_all(b"g")?;
|
|
||||||
assert_eq!(
|
|
||||||
read_config(tmp_file.path().to_str().unwrap())
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string(),
|
|
||||||
r#"invalid type: string "g", expected struct Config at line 1 column 1"#
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn returns_error_if_config_missing_field() -> Result<(), std::io::Error> {
|
|
||||||
let mut tmp_file = NamedTempFile::new()?;
|
|
||||||
tmp_file.write_all(
|
|
||||||
br#"
|
|
||||||
bind_address: "localhost"
|
|
||||||
public_address: "localhost"
|
|
||||||
"#,
|
|
||||||
)?;
|
|
||||||
assert_eq!(
|
|
||||||
read_config(tmp_file.path().to_str().unwrap())
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string(),
|
|
||||||
"missing field `groups` at line 2 column 19"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn returns_ok_if_valid_config() -> Result<(), std::io::Error> {
|
|
||||||
let mut tmp_file = NamedTempFile::new()?;
|
|
||||||
tmp_file.write_all(
|
|
||||||
br#"
|
|
||||||
bind_address: "a"
|
|
||||||
public_address: "b"
|
|
||||||
groups: []"#,
|
|
||||||
)?;
|
|
||||||
assert_eq!(
|
|
||||||
read_config(tmp_file.path().to_str().unwrap()).unwrap(),
|
|
||||||
Config {
|
|
||||||
bind_address: String::from("a"),
|
|
||||||
public_address: String::from("b"),
|
|
||||||
groups: vec![],
|
|
||||||
default_route: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod cache_routes {
|
mod cache_routes {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
fn generate_routes(routes: &[(&str, &str)]) -> HashMap<String, String> {
|
fn generate_external_routes(
|
||||||
|
routes: &[(&str, &str)],
|
||||||
|
) -> HashMap<String, routes::Route> {
|
||||||
HashMap::from_iter(
|
HashMap::from_iter(
|
||||||
routes
|
routes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (String::from(*k), String::from(*v))),
|
.map(|(k, v)| ((*k).into(), routes::Route::External((*v).into()))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,18 +236,23 @@ mod cache_routes {
|
||||||
let group1 = RouteGroup {
|
let group1 = RouteGroup {
|
||||||
name: String::from("x"),
|
name: String::from("x"),
|
||||||
description: Some(String::from("y")),
|
description: Some(String::from("y")),
|
||||||
routes: generate_routes(&[("a", "b"), ("c", "d")]),
|
routes: generate_external_routes(&[("a", "b"), ("c", "d")]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let group2 = RouteGroup {
|
let group2 = RouteGroup {
|
||||||
name: String::from("5"),
|
name: String::from("5"),
|
||||||
description: Some(String::from("6")),
|
description: Some(String::from("6")),
|
||||||
routes: generate_routes(&[("1", "2"), ("3", "4")]),
|
routes: generate_external_routes(&[("1", "2"), ("3", "4")]),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cache_routes(&[group1, group2]),
|
cache_routes(&[group1, group2]),
|
||||||
generate_routes(&[("a", "b"), ("c", "d"), ("1", "2"), ("3", "4")])
|
generate_external_routes(&[
|
||||||
|
("a", "b"),
|
||||||
|
("c", "d"),
|
||||||
|
("1", "2"),
|
||||||
|
("3", "4")
|
||||||
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,29 +261,29 @@ mod cache_routes {
|
||||||
let group1 = RouteGroup {
|
let group1 = RouteGroup {
|
||||||
name: String::from("x"),
|
name: String::from("x"),
|
||||||
description: Some(String::from("y")),
|
description: Some(String::from("y")),
|
||||||
routes: generate_routes(&[("a", "b"), ("c", "d")]),
|
routes: generate_external_routes(&[("a", "b"), ("c", "d")]),
|
||||||
};
|
};
|
||||||
|
|
||||||
let group2 = RouteGroup {
|
let group2 = RouteGroup {
|
||||||
name: String::from("5"),
|
name: String::from("5"),
|
||||||
description: Some(String::from("6")),
|
description: Some(String::from("6")),
|
||||||
routes: generate_routes(&[("a", "1"), ("c", "2")]),
|
routes: generate_external_routes(&[("a", "1"), ("c", "2")]),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cache_routes(&[group1.clone(), group2]),
|
cache_routes(&[group1.clone(), group2]),
|
||||||
generate_routes(&[("a", "1"), ("c", "2")])
|
generate_external_routes(&[("a", "1"), ("c", "2")])
|
||||||
);
|
);
|
||||||
|
|
||||||
let group3 = RouteGroup {
|
let group3 = RouteGroup {
|
||||||
name: String::from("5"),
|
name: String::from("5"),
|
||||||
description: Some(String::from("6")),
|
description: Some(String::from("6")),
|
||||||
routes: generate_routes(&[("a", "1"), ("b", "2")]),
|
routes: generate_external_routes(&[("a", "1"), ("b", "2")]),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cache_routes(&[group1, group3]),
|
cache_routes(&[group1, group3]),
|
||||||
generate_routes(&[("a", "1"), ("b", "2"), ("c", "d")])
|
generate_external_routes(&[("a", "1"), ("b", "2"), ("c", "d")])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
249
src/routes.rs
249
src/routes.rs
|
@ -6,14 +6,15 @@ use actix_web::web::{Data, Query};
|
||||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::debug;
|
use log::{debug, error};
|
||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||||
use serde::Deserialize;
|
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
type StateData = Data<Arc<RwLock<State>>>;
|
|
||||||
|
|
||||||
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||||
const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
.add(b' ')
|
.add(b' ')
|
||||||
|
@ -23,6 +24,75 @@ const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
.add(b'`')
|
.add(b'`')
|
||||||
.add(b'+');
|
.add(b'+');
|
||||||
|
|
||||||
|
type StateData = Data<Arc<RwLock<State>>>;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum Route {
|
||||||
|
External(String),
|
||||||
|
Path(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialization of the Route enum needs to be transparent, but since the
|
||||||
|
/// `#[serde(transparent)]` macro isn't available on enums, so we need to
|
||||||
|
/// implement it manually.
|
||||||
|
impl Serialize for Route {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::External(s) => serializer.serialize_str(s),
|
||||||
|
Self::Path(s) => serializer.serialize_str(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialization of the route string into the enum requires us to figure out
|
||||||
|
/// whether or not the string is valid to run as an executable or not. To
|
||||||
|
/// determine this, we simply check if it exists on disk or assume that it's a
|
||||||
|
/// web path. This incurs a disk check operation, but since users shouldn't be
|
||||||
|
/// updating the config that frequently, it should be fine.
|
||||||
|
impl<'de> Deserialize<'de> for Route {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Route, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct RouteVisitor;
|
||||||
|
impl<'de> Visitor<'de> for RouteVisitor {
|
||||||
|
type Value = Route;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
// Return early if it's a path, don't go through URL parsing
|
||||||
|
if std::path::Path::new(value).exists() {
|
||||||
|
debug!("Parsed {} as a valid local path.", value);
|
||||||
|
Ok(Route::Path(value.into()))
|
||||||
|
} else {
|
||||||
|
debug!("{} does not exist on disk, assuming web path.", value);
|
||||||
|
Ok(Route::External(value.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(RouteVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Route {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::External(s) => write!(f, "raw ({})", s),
|
||||||
|
Self::Path(s) => write!(f, "file ({})", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/ls")]
|
#[get("/ls")]
|
||||||
pub async fn list(
|
pub async fn list(
|
||||||
data: Data<Arc<RwLock<State>>>,
|
data: Data<Arc<RwLock<State>>>,
|
||||||
|
@ -52,21 +122,34 @@ 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) {
|
||||||
(Some(path), args) => HttpResponse::Found()
|
(Some(path), args) => {
|
||||||
.header(
|
let resolved_template = match path {
|
||||||
header::LOCATION,
|
Route::Path(path) => resolve_path(PathBuf::from(path), &args),
|
||||||
req
|
Route::External(path) => Ok(path.to_owned().into_bytes()),
|
||||||
.app_data::<Handlebars>()
|
};
|
||||||
.unwrap()
|
|
||||||
.render_template(
|
match resolved_template {
|
||||||
&path,
|
Ok(path) => HttpResponse::Found()
|
||||||
&template_args::query(
|
.header(
|
||||||
utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(),
|
header::LOCATION,
|
||||||
),
|
req
|
||||||
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
|
.render_template(
|
||||||
|
std::str::from_utf8(&path).unwrap(),
|
||||||
|
&template_args::query(
|
||||||
|
utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.finish(),
|
||||||
)
|
Err(e) => {
|
||||||
.finish(),
|
error!("Failed to redirect user for {}: {}", path, e);
|
||||||
|
HttpResponse::InternalServerError().body("Something went wrong :(\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
(None, _) => HttpResponse::NotFound().body("not found"),
|
(None, _) => HttpResponse::NotFound().body("not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,11 +160,11 @@ pub async fn hop(
|
||||||
///
|
///
|
||||||
/// The first element in the tuple describes the route, while the second element
|
/// The first element in the tuple describes the route, while the second element
|
||||||
/// returns the remaining arguments. If none remain, an empty string is given.
|
/// returns the remaining arguments. If none remain, an empty string is given.
|
||||||
fn resolve_hop(
|
fn resolve_hop<'a>(
|
||||||
query: &str,
|
query: &str,
|
||||||
routes: &HashMap<String, String>,
|
routes: &'a HashMap<String, Route>,
|
||||||
default_route: &Option<String>,
|
default_route: &Option<String>,
|
||||||
) -> (Option<String>, String) {
|
) -> (Option<&'a Route>, String) {
|
||||||
let mut split_args = query.split_ascii_whitespace().peekable();
|
let mut split_args = query.split_ascii_whitespace().peekable();
|
||||||
let command = match split_args.peek() {
|
let command = match split_args.peek() {
|
||||||
Some(command) => command,
|
Some(command) => command,
|
||||||
|
@ -94,7 +177,7 @@ fn resolve_hop(
|
||||||
match (routes.get(*command), default_route) {
|
match (routes.get(*command), default_route) {
|
||||||
// Found a route
|
// Found a route
|
||||||
(Some(resolved), _) => (
|
(Some(resolved), _) => (
|
||||||
Some(resolved.clone()),
|
Some(resolved),
|
||||||
match split_args.next() {
|
match split_args.next() {
|
||||||
// Discard the first result, we found the route using the first arg
|
// Discard the first result, we found the route using the first arg
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
|
@ -113,7 +196,7 @@ fn resolve_hop(
|
||||||
let args = split_args.join(" ");
|
let args = split_args.join(" ");
|
||||||
debug!("Using default route {} with args {}", route, args);
|
debug!("Using default route {} with args {}", route, args);
|
||||||
match routes.get(route) {
|
match routes.get(route) {
|
||||||
Some(v) => (Some(v.to_owned()), args),
|
Some(v) => (Some(v), args),
|
||||||
None => (None, String::new()),
|
None => (None, String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,6 +223,28 @@ pub async fn index(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// file.
|
||||||
|
fn resolve_path(
|
||||||
|
path: PathBuf,
|
||||||
|
args: &str,
|
||||||
|
) -> Result<Vec<u8>, crate::BunBunError> {
|
||||||
|
let output = Command::new(path.canonicalize()?).arg(args).output()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(output.stdout)
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Program exit code for {} was not 0! Dumping standard error!",
|
||||||
|
path.display(),
|
||||||
|
);
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(crate::BunBunError::CustomProgramError(error.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/bunbunsearch.xml")]
|
#[get("/bunbunsearch.xml")]
|
||||||
pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
|
pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
let data = data.read().unwrap();
|
let data = data.read().unwrap();
|
||||||
|
@ -160,15 +265,69 @@ pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod route {
|
||||||
|
use super::*;
|
||||||
|
use serde_yaml::{from_str, to_string};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_relative_path() {
|
||||||
|
let tmpfile = NamedTempFile::new_in(".").unwrap();
|
||||||
|
let path = format!("{}", tmpfile.path().display());
|
||||||
|
let path = path.get(path.rfind(".").unwrap()..).unwrap();
|
||||||
|
let path = std::path::Path::new(path);
|
||||||
|
assert!(path.is_relative());
|
||||||
|
let path = path.to_str().unwrap();
|
||||||
|
assert_eq!(from_str::<Route>(path).unwrap(), Route::Path(path.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_absolute_path() {
|
||||||
|
let tmpfile = NamedTempFile::new().unwrap();
|
||||||
|
let path = format!("{}", tmpfile.path().display());
|
||||||
|
assert!(tmpfile.path().is_absolute());
|
||||||
|
assert_eq!(from_str::<Route>(&path).unwrap(), Route::Path(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_http_path() {
|
||||||
|
assert_eq!(
|
||||||
|
from_str::<Route>("http://google.com").unwrap(),
|
||||||
|
Route::External("http://google.com".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_https_path() {
|
||||||
|
assert_eq!(
|
||||||
|
from_str::<Route>("https://google.com").unwrap(),
|
||||||
|
Route::External("https://google.com".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize() {
|
||||||
|
assert_eq!(
|
||||||
|
&to_string(&Route::External("hello world".into())).unwrap(),
|
||||||
|
"---\nhello world"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&to_string(&Route::Path("hello world".into())).unwrap(),
|
||||||
|
"---\nhello world"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod resolve_hop {
|
mod resolve_hop {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn generate_route_result(
|
fn generate_route_result<'a>(
|
||||||
keyword: &str,
|
keyword: &'a Route,
|
||||||
args: &str,
|
args: &str,
|
||||||
) -> (Option<String>, String) {
|
) -> (Option<&'a Route>, String) {
|
||||||
(Some(String::from(keyword)), String::from(args))
|
(Some(keyword), String::from(args))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -193,31 +352,49 @@ mod resolve_hop {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_default_routes_some_default_yields_default_hop() {
|
fn only_default_routes_some_default_yields_default_hop() {
|
||||||
let mut map = HashMap::new();
|
let mut map: HashMap<String, Route> = HashMap::new();
|
||||||
map.insert(String::from("google"), String::from("https://example.com"));
|
map.insert(
|
||||||
|
"google".into(),
|
||||||
|
Route::External("https://example.com".into()),
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_hop("hello world", &map, &Some(String::from("google"))),
|
resolve_hop("hello world", &map, &Some(String::from("google"))),
|
||||||
generate_route_result("https://example.com", "hello world"),
|
generate_route_result(
|
||||||
|
&Route::External("https://example.com".into()),
|
||||||
|
"hello world"
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_default_routes_some_default_yields_non_default_hop() {
|
fn non_default_routes_some_default_yields_non_default_hop() {
|
||||||
let mut map = HashMap::new();
|
let mut map: HashMap<String, Route> = HashMap::new();
|
||||||
map.insert(String::from("google"), String::from("https://example.com"));
|
map.insert(
|
||||||
|
"google".into(),
|
||||||
|
Route::External("https://example.com".into()),
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_hop("google hello world", &map, &Some(String::from("a"))),
|
resolve_hop("google hello world", &map, &Some(String::from("a"))),
|
||||||
generate_route_result("https://example.com", "hello world"),
|
generate_route_result(
|
||||||
|
&Route::External("https://example.com".into()),
|
||||||
|
"hello world"
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_default_routes_no_default_yields_non_default_hop() {
|
fn non_default_routes_no_default_yields_non_default_hop() {
|
||||||
let mut map = HashMap::new();
|
let mut map: HashMap<String, Route> = HashMap::new();
|
||||||
map.insert(String::from("google"), String::from("https://example.com"));
|
map.insert(
|
||||||
|
"google".into(),
|
||||||
|
Route::External("https://example.com".into()),
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_hop("google hello world", &map, &None),
|
resolve_hop("google hello world", &map, &None),
|
||||||
generate_route_result("https://example.com", "hello world"),
|
generate_route_result(
|
||||||
|
&Route::External("https://example.com".into()),
|
||||||
|
"hello world"
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue