#![forbid(unsafe_code)]
#![deny(missing_docs)]
use crate::config::{
get_config_data, load_custom_path_config, read_config, ConfigData, Route,
RouteGroup,
};
use actix_web::{middleware::Logger, App, HttpServer};
use clap::Clap;
use error::BunBunError;
use handlebars::{Handlebars, TemplateError};
use hotwatch::{Event, Hotwatch};
use log::{debug, error, info, trace, warn};
use simple_logger::SimpleLogger;
use std::cmp::min;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Duration;
mod cli;
mod config;
mod error;
mod routes;
mod template_args;
pub struct State {
public_address: String,
default_route: Option<String>,
groups: Vec<RouteGroup>,
routes: HashMap<String, Route>,
}
#[actix_rt::main]
async fn main() {
std::process::exit(match run().await {
Ok(_) => 0,
Err(e) => {
error!("{}", e);
1
}
})
}
async fn run() -> Result<(), BunBunError> {
let opts = cli::Opts::parse();
init_logger(opts.verbose, opts.quiet)?;
let conf_data = match opts.config {
Some(file_name) => load_custom_path_config(file_name),
None => get_config_data(),
}?;
let conf = read_config(conf_data.file.try_clone()?, opts.large_config)?;
let state = Arc::from(RwLock::new(State {
public_address: conf.public_address,
default_route: conf.default_route,
routes: cache_routes(&conf.groups),
groups: conf.groups,
}));
let _watch = start_watch(Arc::clone(&state), conf_data, opts.large_config)?;
HttpServer::new(move || {
let templates = match compile_templates() {
Ok(templates) => templates,
Err(e) => unreachable!("Failed to compile templates: {}", e),
};
App::new()
.data(Arc::clone(&state))
.app_data(templates)
.wrap(Logger::default())
.service(routes::hop)
.service(routes::list)
.service(routes::index)
.service(routes::opensearch)
})
.bind(&conf.bind_address)?
.run()
.await?;
Ok(())
}
fn init_logger(
num_verbose_flags: u8,
num_quiet_flags: u8,
) -> Result<(), BunBunError> {
let log_level =
match min(num_verbose_flags, 3) as i8 - min(num_quiet_flags, 2) as i8 {
-2 => None,
-1 => Some(log::LevelFilter::Error),
0 => Some(log::LevelFilter::Warn),
1 => Some(log::LevelFilter::Info),
2 => Some(log::LevelFilter::Debug),
3 => Some(log::LevelFilter::Trace),
_ => unreachable!(), };
if let Some(level) = log_level {
SimpleLogger::new().with_level(level).init()?;
}
Ok(())
}
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, Route> {
let mut mapping = HashMap::new();
for group in groups {
for (kw, dest) in &group.routes {
match mapping.insert(kw.clone(), dest.clone()) {
None => trace!("Inserting {} into mapping.", kw),
Some(old_value) => {
trace!("Overriding {} route from {} to {}.", kw, old_value, dest)
}
}
}
}
mapping
}
fn compile_templates() -> Result<Handlebars<'static>, TemplateError> {
let mut handlebars = Handlebars::new();
handlebars.set_strict_mode(true);
handlebars.register_partial("bunbun_version", env!("CARGO_PKG_VERSION"))?;
handlebars.register_partial("bunbun_src", env!("CARGO_PKG_REPOSITORY"))?;
macro_rules! register_template {
[ $( $template:expr ),* ] => {
$(
handlebars
.register_template_string(
$template,
String::from_utf8_lossy(
include_bytes!(concat!("templates/", $template, ".hbs")))
)?;
debug!("Loaded {} template.", $template);
)*
};
}
register_template!["index", "list", "opensearch"];
Ok(handlebars)
}
fn start_watch(
state: Arc<RwLock<State>>,
config_data: ConfigData,
large_config: bool,
) -> Result<Hotwatch, BunBunError> {
let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?;
let ConfigData { path, file } = config_data;
let watch_result = watch.watch(&path, move |e: Event| {
if let Event::Write(_) = e {
trace!("Grabbing writer lock on state...");
let mut state = state.write().expect("Failed to get write lock on state");
trace!("Obtained writer lock on state!");
match read_config(
file.try_clone().expect("Failed to clone file handle"),
large_config,
) {
Ok(conf) => {
state.public_address = conf.public_address;
state.default_route = conf.default_route;
state.routes = cache_routes(&conf.groups);
state.groups = conf.groups;
info!("Successfully updated active state");
}
Err(e) => warn!("Failed to update config file: {}", e),
}
} else {
debug!("Saw event {:#?} but ignored it", e);
}
});
match watch_result {
Ok(_) => info!("Watcher is now watching {:?}", &path),
Err(e) => warn!(
"Couldn't watch {:?}: {}. Changes to this file won't be seen!",
&path, e
),
}
Ok(watch)
}
#[cfg(test)]
mod init_logger {
use super::*;
#[test]
fn defaults_to_warn() -> Result<(), BunBunError> {
init_logger(0, 0)?;
assert_eq!(log::max_level(), log::Level::Warn);
Ok(())
}
#[test]
#[ignore]
fn caps_to_2_when_log_level_is_lt_2() -> Result<(), BunBunError> {
init_logger(0, 3)?;
assert_eq!(log::max_level(), log::LevelFilter::Off);
Ok(())
}
#[test]
#[ignore]
fn caps_to_3_when_log_level_is_gt_3() -> Result<(), BunBunError> {
init_logger(4, 0)?;
assert_eq!(log::max_level(), log::Level::Trace);
Ok(())
}
}
#[cfg(test)]
mod cache_routes {
use super::*;
use std::iter::FromIterator;
use std::str::FromStr;
fn generate_external_routes(
routes: &[(&str, &str)],
) -> HashMap<String, Route> {
HashMap::from_iter(
routes
.into_iter()
.map(|kv| (kv.0.into(), Route::from_str(kv.1).unwrap())),
)
}
#[test]
fn empty_groups_yield_empty_routes() {
assert_eq!(cache_routes(&[]), HashMap::new());
}
#[test]
fn disjoint_groups_yield_summed_routes() {
let group1 = RouteGroup {
name: String::from("x"),
description: Some(String::from("y")),
routes: generate_external_routes(&[("a", "b"), ("c", "d")]),
hidden: false,
};
let group2 = RouteGroup {
name: String::from("5"),
description: Some(String::from("6")),
routes: generate_external_routes(&[("1", "2"), ("3", "4")]),
hidden: false,
};
assert_eq!(
cache_routes(&[group1, group2]),
generate_external_routes(&[
("a", "b"),
("c", "d"),
("1", "2"),
("3", "4")
])
);
}
#[test]
fn overlapping_groups_use_latter_routes() {
let group1 = RouteGroup {
name: String::from("x"),
description: Some(String::from("y")),
routes: generate_external_routes(&[("a", "b"), ("c", "d")]),
hidden: false,
};
let group2 = RouteGroup {
name: String::from("5"),
description: Some(String::from("6")),
routes: generate_external_routes(&[("a", "1"), ("c", "2")]),
hidden: false,
};
assert_eq!(
cache_routes(&[group1.clone(), group2]),
generate_external_routes(&[("a", "1"), ("c", "2")])
);
let group3 = RouteGroup {
name: String::from("5"),
description: Some(String::from("6")),
routes: generate_external_routes(&[("a", "1"), ("b", "2")]),
hidden: false,
};
assert_eq!(
cache_routes(&[group1, group3]),
generate_external_routes(&[("a", "1"), ("b", "2"), ("c", "d")])
);
}
}
#[cfg(test)]
mod compile_templates {
use super::compile_templates;
#[test]
fn templates_compile() {
let _ = compile_templates();
}
}