diff --git a/src/main.rs b/src/main.rs index 2d96cb5..ab8c80a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,6 @@ -use actix_web::{ - get, - http::header, - web::{Data, Query}, - App, HttpResponse, HttpServer, Responder, -}; +use actix_web::{App, HttpServer}; use handlebars::Handlebars; use hotwatch::{Event, Hotwatch}; -use itertools::Itertools; -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use serde::Deserialize; use std::collections::HashMap; use std::fmt; @@ -16,11 +9,9 @@ use std::io::Write; use std::sync::{Arc, RwLock}; use std::time::Duration; +mod routes; mod template_args; -/// https://url.spec.whatwg.org/#fragment-percent-encode-set -static FRAGMENT_ENCODE_SET: &AsciiSet = - &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); static DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.toml"); static CONFIG_FILE: &str = "bunbun.toml"; @@ -58,113 +49,9 @@ from_error!(std::io::Error, IoError); from_error!(serde_yaml::Error, ParseError); from_error!(hotwatch::Error, WatchError); -#[get("/ls")] -fn list(data: Data>>) -> impl Responder { - let data = data.read().unwrap(); - HttpResponse::Ok().body(data.renderer.render("list", &data.routes).unwrap()) -} - -#[derive(Deserialize)] -struct SearchQuery { - to: String, -} - -#[get("/hop")] -fn hop( - data: Data>>, - query: Query, -) -> impl Responder { - let data = data.read().unwrap(); - - match resolve_hop(&query.to, &data.routes, &data.default_route) { - (Some(path), args) => HttpResponse::Found() - .header( - header::LOCATION, - data - .renderer - .render_template( - &path, - &template_args::query( - utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(), - ), - ) - .unwrap(), - ) - .finish(), - (None, _) => HttpResponse::NotFound().body("not found"), - } -} - -/// 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 -/// failing to resolve a route. -/// -/// 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. -fn resolve_hop( - query: &str, - routes: &HashMap, - default_route: &Option, -) -> (Option, String) { - let mut split_args = query.split_ascii_whitespace().peekable(); - let command = match split_args.peek() { - Some(command) => command, - None => return (None, String::new()), - }; - - match (routes.get(*command), default_route) { - // Found a route - (Some(resolved), _) => ( - Some(resolved.clone()), - match split_args.next() { - // Discard the first result, we found the route using the first arg - Some(_) => split_args.join(" "), - None => String::new(), - }, - ), - // Unable to find route, but had a default route - (None, Some(route)) => (routes.get(route).cloned(), split_args.join(" ")), - // No default route and no match - (None, None) => (None, String::new()), - } -} - -#[get("/")] -fn index(data: Data>>) -> impl Responder { - let data = data.read().unwrap(); - HttpResponse::Ok().body( - data - .renderer - .render( - "index", - &template_args::hostname(data.public_address.clone()), - ) - .unwrap(), - ) -} - -#[get("/bunbunsearch.xml")] -fn opensearch(data: Data>>) -> impl Responder { - let data = data.read().unwrap(); - HttpResponse::Ok() - .header( - header::CONTENT_TYPE, - "application/opensearchdescription+xml", - ) - .body( - data - .renderer - .render( - "opensearch", - &template_args::hostname(data.public_address.clone()), - ) - .unwrap(), - ) -} - /// Dynamic variables that either need to be present at runtime, or can be /// changed during runtime. -struct State { +pub struct State { public_address: String, default_route: Option, routes: HashMap, @@ -201,10 +88,10 @@ fn main() -> Result<(), BunBunError> { HttpServer::new(move || { App::new() .data(state_ref.clone()) - .service(hop) - .service(list) - .service(index) - .service(opensearch) + .service(routes::hop) + .service(routes::list) + .service(routes::index) + .service(routes::opensearch) }) .bind(&conf.bind_address)? .run()?; diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..311ca85 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,119 @@ +use crate::template_args; +use crate::State; +use actix_web::get; +use actix_web::http::header; +use actix_web::web::{Data, Query}; +use actix_web::{HttpResponse, Responder}; +use itertools::Itertools; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use serde::Deserialize; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +/// https://url.spec.whatwg.org/#fragment-percent-encode-set +static FRAGMENT_ENCODE_SET: &AsciiSet = + &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); + +#[get("/ls")] +pub fn list(data: Data>>) -> impl Responder { + let data = data.read().unwrap(); + HttpResponse::Ok().body(data.renderer.render("list", &data.routes).unwrap()) +} + +#[derive(Deserialize)] +pub struct SearchQuery { + to: String, +} + +#[get("/hop")] +pub fn hop( + data: Data>>, + query: Query, +) -> impl Responder { + let data = data.read().unwrap(); + + match resolve_hop(&query.to, &data.routes, &data.default_route) { + (Some(path), args) => HttpResponse::Found() + .header( + header::LOCATION, + data + .renderer + .render_template( + &path, + &template_args::query( + utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(), + ), + ) + .unwrap(), + ) + .finish(), + (None, _) => HttpResponse::NotFound().body("not found"), + } +} + +/// 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 +/// failing to resolve a route. +/// +/// 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. +fn resolve_hop( + query: &str, + routes: &HashMap, + default_route: &Option, +) -> (Option, String) { + let mut split_args = query.split_ascii_whitespace().peekable(); + let command = match split_args.peek() { + Some(command) => command, + None => return (None, String::new()), + }; + + match (routes.get(*command), default_route) { + // Found a route + (Some(resolved), _) => ( + Some(resolved.clone()), + match split_args.next() { + // Discard the first result, we found the route using the first arg + Some(_) => split_args.join(" "), + None => String::new(), + }, + ), + // Unable to find route, but had a default route + (None, Some(route)) => (routes.get(route).cloned(), split_args.join(" ")), + // No default route and no match + (None, None) => (None, String::new()), + } +} + +#[get("/")] +pub fn index(data: Data>>) -> impl Responder { + let data = data.read().unwrap(); + HttpResponse::Ok().body( + data + .renderer + .render( + "index", + &template_args::hostname(data.public_address.clone()), + ) + .unwrap(), + ) +} + +#[get("/bunbunsearch.xml")] +pub fn opensearch(data: Data>>) -> impl Responder { + let data = data.read().unwrap(); + HttpResponse::Ok() + .header( + header::CONTENT_TYPE, + "application/opensearchdescription+xml", + ) + .body( + data + .renderer + .render( + "opensearch", + &template_args::hostname(data.public_address.clone()), + ) + .unwrap(), + ) +}