static_router_macros/
lib.rs1#![warn(clippy::pedantic)]
4#![warn(
5 missing_copy_implementations,
6 elided_lifetimes_in_paths,
7 explicit_outlives_requirements,
8 macro_use_extern_crate,
9 meta_variable_misuse,
10 missing_abi,
11 missing_copy_implementations,
12 missing_debug_implementations,
13 non_ascii_idents,
14 noop_method_call,
15 pointer_structural_match,
16 single_use_lifetimes,
17 trivial_casts,
18 trivial_numeric_casts,
19 unreachable_pub,
20 unused_crate_dependencies,
21 unused_extern_crates,
22 unused_import_braces,
23 unused_lifetimes,
24 unused_macro_rules,
25 unused_qualifications,
26 variant_size_differences
27)]
28#![forbid(unsafe_code)]
29
30use std::path::PathBuf;
31
32use proc_macro2::{Literal, TokenStream, TokenTree};
33use proc_macro_error::{abort, abort_call_site, proc_macro_error};
34use quote::quote;
35
36mod mime;
37
38#[proc_macro_error]
39#[proc_macro]
40pub fn static_router(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
41 let tokens = TokenStream::from(tokens);
42 let mut tokens = tokens.into_iter();
43
44 let router_name = match tokens
45 .next()
46 .unwrap_or_else(|| abort_call_site!("expected router name"))
47 {
48 TokenTree::Ident(ident) => ident,
49 other => abort!(other, "expected router name"),
50 };
51
52 match tokens
53 .next()
54 .unwrap_or_else(|| abort_call_site!("expected comma"))
55 {
56 TokenTree::Punct(punct) if punct.as_char() == ',' => (),
57 other => abort!(other, "expected comma"),
58 }
59
60 let static_path = match tokens
61 .next()
62 .unwrap_or_else(|| abort_call_site!("expected static directory path"))
63 {
64 ref token @ TokenTree::Literal(ref literal) => match litrs::Literal::from(literal) {
65 litrs::Literal::String(s_lit) => s_lit.into_value().into_owned(),
66 _ => abort!(token, "expected static directory path"),
67 },
68 other => abort!(other, "expected static directory path"),
69 };
70
71 let static_router = make_static_router(&static_path);
72 let dynamic_router = make_dynamic_router(&static_path);
73
74 quote! {
75 pub fn #router_name() -> ::static_router::__private::axum::Router {
76 #[cfg(debug_assertions)]
77 { #dynamic_router }
78 #[cfg(not(debug_assertions))]
79 { #static_router }
80 }
81 }
82 .into()
83}
84
85fn make_static_router(root_path: &str) -> TokenStream {
86 let root_path = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join(root_path);
87
88 let routes = walkdir::WalkDir::new(&root_path)
89 .follow_links(true)
90 .into_iter()
91 .filter_map(|entry| match entry {
92 Ok(entry) => {
93 let actual_path = entry.path();
94
95 if entry.file_type().is_dir() {
96 return None;
97 }
98
99 let user_path = actual_path.strip_prefix(&root_path).unwrap().to_str().unwrap();
100 let user_path = format!("/{user_path}");
101 let user_path_lit = Literal::string(&user_path);
102
103 let mime = actual_path.extension().unwrap_or_else(|| abort_call_site!("missing extension on {:?}: needed to determine MIME type", actual_path)).to_str().and_then(mime::ext_to_mime).unwrap_or_else(|| abort_call_site!("invalid or unrecognized extension on {:?}", actual_path));
104 let mime_lit = Literal::string(mime);
105
106 let data = std::fs::read(actual_path).unwrap_or_else(|err| abort_call_site!(err));
107 let data_lit = Literal::byte_string(&data);
108
109 let hash = xxhash_rust::xxh3::xxh3_128(&data);
110 let e_tag = format!("\"{hash:032x}\"");
112 let e_tag_lit = Literal::byte_string(e_tag.as_bytes());
113
114 Some(quote! {
115 router = router.route(#user_path_lit, ::static_router::__private::axum::routing::get(|req: ::static_router::__private::http::Request<::static_router::__private::axum::body::Body>| async move { ::static_router::__private::handler(&req, #mime_lit, #data_lit, #e_tag_lit) }));
116 })
117 }
118 Err(error) => abort_call_site!("error walking directories: {}", error),
119 });
120
121 quote! {
122 let mut router = ::static_router::__private::axum::Router::new();
123 #(#routes)*
124 router
125 }
126}
127
128fn make_dynamic_router(path: &str) -> TokenStream {
129 quote! {
130 ::static_router::__private::axum::Router::new().fallback(::static_router::__private::axum::routing::get_service(::static_router::__private::tower_http::services::ServeDir::new(#path)).handle_error(|err: ::static_router::__private::std::io::Error| async move {
131 let status = match err.kind() {
132 ::static_router::__private::std::io::ErrorKind::NotFound => ::static_router::__private::http::status::StatusCode::NOT_FOUND,
133 _ => ::static_router::__private::http::status::StatusCode::INTERNAL_SERVER_ERROR,
134 };
135 (status, err.to_string())
136 }))
137 }
138}