move route serialization to config.rs

This commit is contained in:
Edward Shen 2019-12-31 23:07:01 -05:00
parent 1d7d547475
commit 961cbd721b
Signed by: edward
GPG key ID: F350507060ED6C90
3 changed files with 134 additions and 131 deletions

View file

@ -1,7 +1,11 @@
use crate::{routes::Route, BunBunError}; use crate::BunBunError;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
use serde::{Deserialize, Serialize}; use serde::{
de::{Deserializer, Visitor},
Deserialize, Serialize, Serializer,
};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::fs::{read_to_string, OpenOptions}; use std::fs::{read_to_string, OpenOptions};
use std::io::Write; use std::io::Write;
@ -22,6 +26,73 @@ pub struct RouteGroup {
pub routes: HashMap<String, Route>, pub routes: HashMap<String, Route>,
} }
#[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),
}
}
}
/// Attempts to read the config file. If it doesn't exist, generate one a /// Attempts to read the config file. If it doesn't exist, generate one a
/// default config file before attempting to parse it. /// default config file before attempting to parse it.
pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> { pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
@ -58,6 +129,60 @@ pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
Ok(serde_yaml::from_str(&config_str)?) Ok(serde_yaml::from_str(&config_str)?)
} }
#[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 read_config { mod read_config {
use super::*; use super::*;

View file

@ -1,6 +1,6 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
use crate::config::{read_config, RouteGroup}; use crate::config::{read_config, Route, 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;
@ -24,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, routes::Route>, routes: HashMap<String, Route>,
} }
#[actix_rt::main] #[actix_rt::main]
@ -97,7 +97,7 @@ fn init_logger(
/// 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, routes::Route> { fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, 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 {
@ -218,11 +218,11 @@ mod cache_routes {
fn generate_external_routes( fn generate_external_routes(
routes: &[(&str, &str)], routes: &[(&str, &str)],
) -> HashMap<String, routes::Route> { ) -> HashMap<String, Route> {
HashMap::from_iter( HashMap::from_iter(
routes routes
.iter() .iter()
.map(|(k, v)| ((*k).into(), routes::Route::External((*v).into()))), .map(|(k, v)| ((*k).into(), Route::External((*v).into()))),
) )
} }

View file

@ -1,4 +1,4 @@
use crate::{template_args, BunBunError, State}; use crate::{template_args, BunBunError, Route, State};
use actix_web::web::{Data, Query}; use actix_web::web::{Data, Query};
use actix_web::{get, http::header}; use actix_web::{get, http::header};
use actix_web::{HttpRequest, HttpResponse, Responder}; use actix_web::{HttpRequest, HttpResponse, Responder};
@ -6,9 +6,8 @@ use handlebars::Handlebars;
use itertools::Itertools; use itertools::Itertools;
use log::{debug, error}; use log::{debug, error};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@ -24,73 +23,6 @@ const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
type StateData = Data<Arc<RwLock<State>>>; 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("/")] #[get("/")]
pub async fn index(data: StateData, req: HttpRequest) -> impl Responder { pub async fn index(data: StateData, req: HttpRequest) -> impl Responder {
let data = data.read().unwrap(); let data = data.read().unwrap();
@ -257,60 +189,6 @@ fn resolve_path(path: PathBuf, args: &str) -> Result<Vec<u8>, BunBunError> {
} }
} }
#[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::*;