diff --git a/src/config.rs b/src/config.rs index 47efac2..a66b4b5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,14 +2,15 @@ use crate::BunBunError; use dirs::{config_dir, home_dir}; use log::{debug, info, trace}; use serde::{ - de::{Deserializer, Visitor}, - Deserialize, Serialize, Serializer, + de::{self, Deserializer, MapAccess, Visitor}, + Deserialize, Serialize, }; use std::collections::HashMap; use std::fmt; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::PathBuf; +use std::str::FromStr; const CONFIG_FILENAME: &str = "bunbun.yaml"; const DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.yaml"); @@ -26,27 +27,34 @@ pub struct Config { pub struct RouteGroup { pub name: String, pub description: Option, + #[serde(default)] + pub hidden: bool, pub routes: HashMap, } -#[derive(Debug, PartialEq, Clone)] -pub enum Route { - External(String), - Path(String), +#[derive(Debug, PartialEq, Clone, Serialize)] +pub struct Route { + pub route_type: RouteType, + pub path: String, + pub hidden: bool, + pub description: Option, } -/// 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(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - Self::External(s) => serializer.serialize_str(s), - Self::Path(s) => serializer.serialize_str(s), - } +#[derive(Debug, PartialEq, Clone, Serialize)] +pub enum RouteType { + External, + Internal, +} + +impl FromStr for Route { + type Err = std::convert::Infallible; + fn from_str(s: &str) -> Result { + Ok(Self { + 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 D: Deserializer<'de>, { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { + Path, + Hidden, + Description, + } + struct RouteVisitor; + impl<'de> Visitor<'de> for RouteVisitor { type Value = Route; @@ -68,30 +85,82 @@ impl<'de> Deserialize<'de> for Route { formatter.write_str("string") } - fn visit_str(self, value: &str) -> Result + fn visit_str(self, path: &str) -> Result 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())) + // This is infalliable + Ok(Self::Value::from_str(path).unwrap()) + } + + fn visit_map(self, mut map: M) -> Result + where + 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::()?); + } + 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 { 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), + Self { + route_type: RouteType::External, + path, + .. + } => write!(f, "raw ({})", path), + Self { + route_type: RouteType::Internal, + path, + .. + } => write!(f, "file ({})", path), } } } diff --git a/src/routes.rs b/src/routes.rs index d6b053a..f053627 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,3 +1,4 @@ +use crate::config::{Route as ConfigRoute, RouteType}; use crate::{template_args, BunBunError, Route, State}; use actix_web::web::{Data, Query}; use actix_web::{get, http::header}; @@ -90,8 +91,16 @@ pub async fn hop( match resolve_hop(&query.to, &data.routes, &data.default_route) { (Some(path), args) => { let resolved_template = match path { - Route::Path(path) => resolve_path(PathBuf::from(path), &args), - Route::External(path) => Ok(path.to_owned().into_bytes()), + ConfigRoute { + 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 { diff --git a/src/templates/list.hbs b/src/templates/list.hbs index 100bcc9..c80fdf6 100644 --- a/src/templates/list.hbs +++ b/src/templates/list.hbs @@ -20,7 +20,7 @@ i { color: rgba(255, 255, 255, 0.5); } td, th { padding: 0 0.5rem; } .shortcut { text-align: right; } - .target { text-align: left; width: 100%; } + .description { text-align: left; width: 100%; } footer { margin-top: 1rem; color: #444; @@ -31,16 +31,29 @@

Bunbun Command List

To edit this list, edit your bunbun.yaml file.

- {{#each this}} {{!-- Iterate over RouteGroup --}} -

{{this.name}}

{{this.description}}
- - - - - - {{#each this.routes}}{{/each}} -
ShortcutTarget
{{@key}}{{this}}
- {{/each}} + {{~#each this}} {{!-- Iterate over RouteGroup --}} + {{~#unless this.hidden}} +

{{this.name}}

{{this.description}}
+ + + + + + {{~#each this.routes}} {{!-- Iterate over Route --}} + {{~#unless this.hidden}} + + + {{~#if this.description~}} + + {{~else~}} + + {{~/if}} + + {{~/unless}} + {{~/each}} +
ShortcutDescription
{{@key}}{{this.description}}{{this.path}}
+ {{~/unless}} + {{~/each}}