added route descriptions, hidden routes and groups

master
Edward Shen 2020-07-04 23:18:47 -04:00
parent a8fba09955
commit 323fa6ba71
Signed by: edward
GPG Key ID: 19182661E818369F
3 changed files with 133 additions and 42 deletions

View File

@ -2,14 +2,15 @@ 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::{Deserializer, Visitor}, de::{self, Deserializer, MapAccess, Visitor},
Deserialize, Serialize, Serializer, Deserialize, Serialize,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
const CONFIG_FILENAME: &str = "bunbun.yaml"; const CONFIG_FILENAME: &str = "bunbun.yaml";
const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml"); const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml");
@ -26,27 +27,34 @@ pub struct Config {
pub struct RouteGroup { pub struct RouteGroup {
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
#[serde(default)]
pub hidden: bool,
pub routes: HashMap<String, Route>, pub routes: HashMap<String, Route>,
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub enum Route { pub struct Route {
External(String), pub route_type: RouteType,
Path(String), pub path: String,
pub hidden: bool,
pub description: Option<String>,
} }
/// Serialization of the Route enum needs to be transparent, but since the #[derive(Debug, PartialEq, Clone, Serialize)]
/// `#[serde(transparent)]` macro isn't available on enums, so we need to pub enum RouteType {
/// implement it manually. External,
impl Serialize for Route { Internal,
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> }
where
S: Serializer, impl FromStr for Route {
{ type Err = std::convert::Infallible;
match self { fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::External(s) => serializer.serialize_str(s), Ok(Self {
Self::Path(s) => serializer.serialize_str(s), route_type: get_route_type(s),
} path: s.to_string(),
hidden: false,
description: None,
})
} }
} }
@ -60,7 +68,16 @@ impl<'de> Deserialize<'de> for Route {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Path,
Hidden,
Description,
}
struct RouteVisitor; struct RouteVisitor;
impl<'de> Visitor<'de> for RouteVisitor { impl<'de> Visitor<'de> for RouteVisitor {
type Value = Route; type Value = Route;
@ -68,30 +85,82 @@ impl<'de> Deserialize<'de> for Route {
formatter.write_str("string") formatter.write_str("string")
} }
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> fn visit_str<E>(self, path: &str) -> Result<Self::Value, E>
where where
E: serde::de::Error, E: serde::de::Error,
{ {
// Return early if it's a path, don't go through URL parsing // This is infalliable
if std::path::Path::new(value).exists() { Ok(Self::Value::from_str(path).unwrap())
debug!("Parsed {} as a valid local path.", value); }
Ok(Route::Path(value.into()))
} else { fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
debug!("{} does not exist on disk, assuming web path.", value); where
Ok(Route::External(value.into())) M: MapAccess<'de>,
{
let mut path = None;
let mut hidden = None;
let mut description = None;
while let Some(key) = map.next_key()? {
match key {
Field::Path => {
if path.is_some() {
return Err(de::Error::duplicate_field("path"));
}
path = Some(map.next_value::<String>()?);
}
Field::Hidden => {
if hidden.is_some() {
return Err(de::Error::duplicate_field("hidden"));
}
hidden = map.next_value()?;
}
Field::Description => {
if description.is_some() {
return Err(de::Error::duplicate_field("description"));
}
description = Some(map.next_value()?);
}
}
} }
let path = path.ok_or_else(|| de::Error::missing_field("path"))?;
Ok(Route {
route_type: get_route_type(&path),
path,
hidden: hidden.unwrap_or_default(),
description,
})
} }
} }
deserializer.deserialize_str(RouteVisitor) deserializer.deserialize_any(RouteVisitor)
}
}
fn get_route_type(path: &str) -> RouteType {
if std::path::Path::new(path).exists() {
debug!("Parsed {} as a valid local path.", path);
RouteType::Internal
} else {
debug!("{} does not exist on disk, assuming web path.", path);
RouteType::External
} }
} }
impl std::fmt::Display for Route { impl std::fmt::Display for Route {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::External(s) => write!(f, "raw ({})", s), Self {
Self::Path(s) => write!(f, "file ({})", s), route_type: RouteType::External,
path,
..
} => write!(f, "raw ({})", path),
Self {
route_type: RouteType::Internal,
path,
..
} => write!(f, "file ({})", path),
} }
} }
} }

View File

@ -1,3 +1,4 @@
use crate::config::{Route as ConfigRoute, RouteType};
use crate::{template_args, BunBunError, Route, 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};
@ -90,8 +91,16 @@ pub async fn hop(
match resolve_hop(&query.to, &data.routes, &data.default_route) { match resolve_hop(&query.to, &data.routes, &data.default_route) {
(Some(path), args) => { (Some(path), args) => {
let resolved_template = match path { let resolved_template = match path {
Route::Path(path) => resolve_path(PathBuf::from(path), &args), ConfigRoute {
Route::External(path) => Ok(path.to_owned().into_bytes()), route_type: RouteType::Internal,
path,
..
} => resolve_path(PathBuf::from(path), &args),
ConfigRoute {
route_type: RouteType::External,
path,
..
} => Ok(path.to_owned().into_bytes()),
}; };
match resolved_template { match resolved_template {

View File

@ -20,7 +20,7 @@
i { color: rgba(255, 255, 255, 0.5); } i { color: rgba(255, 255, 255, 0.5); }
td, th { padding: 0 0.5rem; } td, th { padding: 0 0.5rem; }
.shortcut { text-align: right; } .shortcut { text-align: right; }
.target { text-align: left; width: 100%; } .description { text-align: left; width: 100%; }
footer { footer {
margin-top: 1rem; margin-top: 1rem;
color: #444; color: #444;
@ -31,16 +31,29 @@
<h1>Bunbun Command List</h1> <h1>Bunbun Command List</h1>
<p><i>To edit this list, edit your <code>bunbun.yaml</code> file.</i></p> <p><i>To edit this list, edit your <code>bunbun.yaml</code> file.</i></p>
<main> <main>
{{#each this}} {{!-- Iterate over RouteGroup --}} {{~#each this}} {{!-- Iterate over RouteGroup --}}
<header><h2>{{this.name}}</h2><i>{{this.description}}</i></header> {{~#unless this.hidden}}
<table> <header><h2>{{this.name}}</h2><i>{{this.description}}</i></header>
<tr> <table>
<th>Shortcut</th> <tr>
<th class="target">Target</th> <th>Shortcut</th>
</tr> <th class="description">Description</th>
{{#each this.routes}}<tr><td class="shortcut">{{@key}}</td><td class="target">{{this}}</td></tr>{{/each}} </tr>
</table> {{~#each this.routes}} {{!-- Iterate over Route --}}
{{/each}} {{~#unless this.hidden}}
<tr>
<td class="shortcut">{{@key}}</td>
{{~#if this.description~}}
<td class="description">{{this.description}}</td>
{{~else~}}
<td class="description">{{this.path}}</td>
{{~/if}}
</tr>
{{~/unless}}
{{~/each}}
</table>
{{~/unless}}
{{~/each}}
</main> </main>
</body> </body>
<footer> <footer>