static_router_macros/
lib.rs

1//! See the main `static-router` crate.
2
3#![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				// yes, the value itself contains quotes
111				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}