added route descriptions, hidden routes and groups
This commit is contained in:
parent
a8fba09955
commit
323fa6ba71
3 changed files with 133 additions and 42 deletions
127
src/config.rs
127
src/config.rs
|
@ -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()?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializer.deserialize_str(RouteVisitor)
|
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_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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 --}}
|
||||||
|
{{~#unless this.hidden}}
|
||||||
<header><h2>{{this.name}}</h2><i>{{this.description}}</i></header>
|
<header><h2>{{this.name}}</h2><i>{{this.description}}</i></header>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Shortcut</th>
|
<th>Shortcut</th>
|
||||||
<th class="target">Target</th>
|
<th class="description">Description</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each this.routes}}<tr><td class="shortcut">{{@key}}</td><td class="target">{{this}}</td></tr>{{/each}}
|
{{~#each this.routes}} {{!-- Iterate over Route --}}
|
||||||
|
{{~#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>
|
</table>
|
||||||
{{/each}}
|
{{~/unless}}
|
||||||
|
{{~/each}}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
Loading…
Reference in a new issue