Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: more ergonomic params

+184 -45
+7
Cargo.lock
··· 294 294 "maud", 295 295 "maudit-macros", 296 296 "rayon", 297 + "rustc-hash", 297 298 ] 298 299 299 300 [[package]] ··· 422 423 version = "0.8.5" 423 424 source = "registry+https://github.com/rust-lang/crates.io-index" 424 425 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 426 + 427 + [[package]] 428 + name = "rustc-hash" 429 + version = "2.1.0" 430 + source = "registry+https://github.com/rust-lang/crates.io-index" 431 + checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" 425 432 426 433 [[package]] 427 434 name = "shlex"
+1
crates/framework/Cargo.toml
··· 13 13 chrono = "0.4.39" 14 14 colored = "2.2.0" 15 15 rayon = "1.10.0" 16 + rustc-hash = "2.1"
+6 -5
crates/framework/src/lib.rs
··· 1 1 pub mod assets; 2 2 pub mod page; 3 + pub mod params; 3 4 pub mod routes; 4 5 5 6 mod logging; ··· 22 23 pub use maudit_macros; 23 24 24 25 use log::{info, trace}; 25 - use page::RouteContext; 26 + use page::{RouteContext, RouteParams}; 26 27 27 28 pub fn coronate(router: routes::Router) -> Result<(), Box<dyn std::error::Error>> { 28 29 let build_start = SystemTime::now(); ··· 56 57 let pages_start = SystemTime::now(); 57 58 58 59 let route_format_options = FormatElapsedTimeOptions { 59 - additional_fn: Some(Box::new(|msg: ColoredString| { 60 + additional_fn: Some(&|msg: ColoredString| { 60 61 let formatted_msg = format!("(+{})", msg); 61 62 if msg.fgcolor.is_none() { 62 63 formatted_msg.dimmed() 63 64 } else { 64 65 formatted_msg.into() 65 66 } 66 - })), 67 + }), 67 68 ..Default::default() 68 69 }; 69 70 ··· 81 82 true => { 82 83 let route_start = SystemTime::now(); 83 84 let ctx = RouteContext { 84 - params: HashMap::new(), 85 + params: page::RouteParams(HashMap::new()), 85 86 }; 86 87 87 88 let (file_path, file) = create_route_file(&**route, ctx.params.clone())?; ··· 148 149 149 150 fn create_route_file( 150 151 route: &dyn page::FullPage, 151 - params: HashMap<String, String>, 152 + params: RouteParams, 152 153 ) -> Result<(PathBuf, File), Box<dyn std::error::Error>> { 153 154 let file_path = PathBuf::from_str("./dist/") 154 155 .unwrap()
+3 -3
crates/framework/src/logging.rs
··· 1 1 use colored::{ColoredString, Colorize}; 2 2 use std::time::{Duration, SystemTimeError}; 3 3 4 - pub struct FormatElapsedTimeOptions { 4 + pub struct FormatElapsedTimeOptions<'a> { 5 5 pub(crate) sec_yellow_threshold: u64, 6 6 pub(crate) sec_red_threshold: u64, 7 7 pub(crate) millis_yellow_threshold: Option<u128>, 8 8 pub(crate) millis_red_threshold: Option<u128>, 9 - pub(crate) additional_fn: Option<Box<dyn Fn(ColoredString) -> ColoredString>>, 9 + pub(crate) additional_fn: Option<&'a dyn Fn(ColoredString) -> ColoredString>, 10 10 } 11 11 12 - impl Default for FormatElapsedTimeOptions { 12 + impl Default for FormatElapsedTimeOptions<'_> { 13 13 fn default() -> Self { 14 14 Self { 15 15 sec_yellow_threshold: 1,
+23 -4
crates/framework/src/page.rs
··· 6 6 } 7 7 8 8 pub struct RouteContext { 9 - pub params: HashMap<String, String>, 9 + pub params: RouteParams, 10 10 } 11 11 12 12 pub trait Page { 13 13 fn render(&self, ctx: &RouteContext) -> RenderResult; 14 14 } 15 15 16 + #[derive(Clone, Default, Debug)] 17 + pub struct RouteParams(pub HashMap<String, String>); 18 + 19 + impl RouteParams { 20 + pub fn from_vec<T>(params: Vec<T>) -> Vec<RouteParams> 21 + where 22 + T: Into<RouteParams>, 23 + { 24 + params.into_iter().map(|p| p.into()).collect() 25 + } 26 + 27 + pub fn parse_into<T>(&self) -> T 28 + where 29 + T: From<RouteParams>, 30 + { 31 + T::from(self.clone()) 32 + } 33 + } 34 + 16 35 pub trait DynamicPage { 17 - fn routes(&self) -> Vec<HashMap<String, String>>; 36 + fn routes(&self) -> Vec<RouteParams>; 18 37 } 19 38 20 39 pub trait InternalPage { 21 40 fn route_raw(&self) -> String; 22 - fn route(&self, params: HashMap<String, String>) -> String; 23 - fn file_path(&self, params: HashMap<String, String>) -> PathBuf; 41 + fn route(&self, params: RouteParams) -> String; 42 + fn file_path(&self, params: RouteParams) -> PathBuf; 24 43 } 25 44 26 45 pub trait FullPage: Page + InternalPage + DynamicPage + Sync {}
+50
crates/framework/src/params.rs
··· 1 + // Adapted from https://github.com/rwf2/Rocket/blob/28891e8072136f4641a33fb8c3f2aafce9d88d5b/core/lib/src/request/from_param.rs 2 + // See https://github.com/rwf2/Rocket/blob/28891e8072136f4641a33fb8c3f2aafce9d88d5b/LICENSE-MIT for license information 3 + 4 + use std::str::FromStr; 5 + 6 + pub trait FromParam: Sized { 7 + /// The associated error to be returned if parsing/validation fails. 8 + type Error: std::fmt::Debug; 9 + 10 + /// Parses and validates an instance of `Self` from a path parameter string 11 + /// or returns an `Error` if parsing or validation fails. 12 + fn from_param(param: &str) -> Result<Self, Self::Error>; 13 + } 14 + 15 + impl FromParam for String { 16 + type Error = Empty; 17 + 18 + #[track_caller] 19 + #[inline(always)] 20 + fn from_param(param: &str) -> Result<String, Self::Error> { 21 + Ok(param.to_string()) 22 + } 23 + } 24 + 25 + macro_rules! impl_with_fromstr { 26 + ($($T:ty),+) => ($( 27 + impl FromParam for $T { 28 + type Error = <$T as FromStr>::Err; 29 + 30 + #[inline(always)] 31 + fn from_param(param: &str) -> Result<Self, Self::Error> { 32 + Ok(<$T as FromStr>::from_str(&param).unwrap()) 33 + } 34 + } 35 + )+) 36 + } 37 + 38 + use std::io::Empty; 39 + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; 40 + use std::num::{ 41 + NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, 42 + NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, 43 + }; 44 + 45 + impl_with_fromstr! { 46 + i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, 47 + NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, 48 + NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, 49 + bool, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr 50 + }
+4 -4
crates/framework/src/routes.rs
··· 1 1 use crate::page::FullPage; 2 2 3 - pub struct Router { 4 - pub(crate) routes: Vec<Box<dyn FullPage>>, 3 + pub struct Router<'a> { 4 + pub(crate) routes: Vec<&'a dyn FullPage>, 5 5 } 6 6 7 - impl Router { 8 - pub fn new(routes: Vec<Box<dyn FullPage>>) -> Self { 7 + impl<'a> Router<'a> { 8 + pub fn new(routes: Vec<&'a dyn FullPage>) -> Router<'a> { 9 9 Router { routes } 10 10 } 11 11 }
+66 -10
crates/macros/src/lib.rs
··· 39 39 let dynamic_page_impl = match params.is_empty() { 40 40 false => quote! {}, 41 41 true => quote! { 42 - impl DynamicPage for #struct_name { 43 - fn routes(&self) -> Vec<std::collections::HashMap<String, String>> { 42 + impl maudit::page::DynamicPage for #struct_name { 43 + fn routes(&self) -> Vec<maudit::page::RouteParams> { 44 44 Vec::new() 45 45 } 46 46 } ··· 53 53 .iter() 54 54 .map(|v| { 55 55 let key = format_ident!("{}", v.key); 56 - quote! { let #key = params.get(stringify!(#key)).unwrap().to_string() } 56 + quote! { let #key = params.0.get(stringify!(#key)).unwrap().to_string() } 57 + }) 58 + .collect::<Vec<_>>(); 59 + 60 + let struct_def_params = params 61 + .iter() 62 + .map(|v| { 63 + let key = format_ident!("{}", v.key); 64 + quote! { #key: String } 57 65 }) 58 66 .collect::<Vec<_>>(); 59 67 ··· 61 69 let file_path_for_route = url_to_file_path(&path, attrs.is_endpoint_file, &params); 62 70 63 71 let expanded = quote! { 64 - use std::path::{Path, PathBuf}; 65 - use maudit::page::{FullPage, InternalPage, Page, RenderResult, DynamicPage}; 72 + struct RawParams { 73 + #(#struct_def_params,)* 74 + } 75 + 76 + impl RawParams { 77 + fn get_field_names() -> Vec<&'static str> { 78 + vec![#(stringify!(#struct_def_params)),*] 79 + } 80 + } 66 81 67 - impl InternalPage for #struct_name { 82 + impl maudit::page::InternalPage for #struct_name { 68 83 fn route_raw(&self) -> String { 69 84 #path.to_string() 70 85 } 71 86 72 - fn route(&self, params: std::collections::HashMap<String, String>) -> String { 87 + fn route(&self, params: maudit::page::RouteParams) -> String { 73 88 #(#list_params;)* 74 89 return format!(#path_for_route); 75 90 } 76 91 77 - fn file_path(&self, params: std::collections::HashMap<String, String>) -> PathBuf { 92 + fn file_path(&self, params: maudit::page::RouteParams) -> std::path::PathBuf { 78 93 // List params in the shape of let id = ctx.params.get("id").unwrap().to_string(); 79 94 #(#list_params;)* 80 - PathBuf::from(format!(#file_path_for_route)) 95 + std::path::PathBuf::from(format!(#file_path_for_route)) 81 96 } 82 97 } 83 98 84 99 85 100 #dynamic_page_impl 86 101 87 - impl FullPage for #struct_name {} 102 + impl maudit::page::FullPage for #struct_name {} 88 103 89 104 #item_struct 90 105 }; ··· 174 189 175 190 file_path 176 191 } 192 + 193 + #[proc_macro_derive(Params)] 194 + pub fn derive_params(item: TokenStream) -> TokenStream { 195 + let item_struct = syn::parse_macro_input!(item as ItemStruct); 196 + let struct_name = &item_struct.ident; 197 + 198 + let fields = match &item_struct.fields { 199 + syn::Fields::Named(fields) => fields 200 + .named 201 + .iter() 202 + .map(|f| f.ident.as_ref().unwrap()) 203 + .collect::<Vec<_>>(), 204 + _ => panic!("Only named fields are supported"), 205 + }; 206 + 207 + // Add a from Hashmap conversion 208 + let expanded = quote! { 209 + use maudit::params::FromParam; 210 + 211 + impl From<RouteParams> for #struct_name { 212 + fn from(params: RouteParams) -> Self { 213 + #struct_name { 214 + #(#fields: FromParam::from_param(params.0.get(stringify!(#fields)).unwrap()).unwrap(),)* 215 + 216 + } 217 + } 218 + } 219 + 220 + impl Into<RouteParams> for #struct_name { 221 + fn into(self) -> RouteParams { 222 + let mut map = std::collections::HashMap::new(); 223 + #( 224 + map.insert(stringify!(#fields).to_string(), self.#fields.to_string()); 225 + )* 226 + RouteParams(map) 227 + } 228 + } 229 + }; 230 + 231 + TokenStream::from(expanded) 232 + }
+1 -1
crates/user-example/src/main.rs
··· 2 2 use maudit::routes::Router; 3 3 4 4 fn main() -> Result<(), Box<dyn std::error::Error>> { 5 - let router = Router::new(vec![Box::new(pages::Index), Box::new(pages::Endpoint)]); 5 + let router = Router::new(vec![&pages::Index, &pages::Endpoint]); 6 6 7 7 maudit::coronate(router) 8 8 }
+2 -1
crates/user-example/src/pages/endpoint.rs
··· 1 - use maudit::{maudit_macros::route, page::RouteContext}; 1 + use maudit::maudit_macros::route; 2 + use maudit::page::{Page, RenderResult, RouteContext}; 2 3 3 4 #[route("/catalogue/data.json")] 4 5 pub struct Endpoint;
+21 -17
crates/user-example/src/pages/index.rs
··· 1 1 use maudit::maud::html; 2 - use maudit::maudit_macros::route; 3 - use maudit::page::RouteContext; 2 + use maudit::maudit_macros::{route, Params}; 3 + use maudit::page::{DynamicPage, Page, RenderResult, RouteContext, RouteParams}; 4 4 5 5 #[route("/[page]")] 6 6 pub struct Index; 7 7 8 + #[derive(Params)] 9 + struct Params { 10 + page: u128, 11 + } 12 + 13 + impl DynamicPage for Index { 14 + fn routes(&self) -> Vec<RouteParams> { 15 + let mut static_routes: Vec<Params> = vec![]; 16 + 17 + for i in 0..1000 { 18 + static_routes.push(Params { page: i }); 19 + } 20 + 21 + RouteParams::from_vec(static_routes) 22 + } 23 + } 24 + 8 25 impl Page for Index { 9 26 fn render(&self, ctx: &RouteContext) -> RenderResult { 10 - let params = ctx.params.get("page").unwrap(); 27 + let params = ctx.params.parse_into::<Params>(); 11 28 12 29 RenderResult::Html(html! { 13 30 h1 { "Hello, world!" } 14 - p { (params) } 31 + p { (params.page) } 15 32 }) 16 33 } 17 34 } 18 - 19 - impl DynamicPage for Index { 20 - fn routes(&self) -> Vec<std::collections::HashMap<String, String>> { 21 - // Return 100 routes 22 - (0..1000) 23 - .map(|i| { 24 - let mut map = std::collections::HashMap::new(); 25 - map.insert("page".into(), i.to_string()); 26 - map 27 - }) 28 - .collect() 29 - } 30 - }