autumn_web/static_gen/
types.rs1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::future::Future;
10use std::path::Path;
11use std::pin::Pin;
12
13pub type StaticParams = HashMap<String, String>;
26
27#[macro_export]
38macro_rules! static_params {
39 ($($key:expr => $value:expr),* $(,)?) => {{
40 #[allow(unused_mut)]
41 let mut map = ::std::collections::HashMap::new();
42 $(map.insert($key.to_owned(), $value.to_owned());)*
43 map
44 }};
45}
46
47pub type ParamsFn = fn(axum::Router) -> Pin<Box<dyn Future<Output = Vec<StaticParams>> + Send>>;
54
55#[derive(Clone)]
62pub struct StaticRouteMeta {
63 pub path: &'static str,
65 pub name: &'static str,
67 pub revalidate: Option<u64>,
70 pub params_fn: Option<ParamsFn>,
73}
74
75impl std::fmt::Debug for StaticRouteMeta {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("StaticRouteMeta")
78 .field("path", &self.path)
79 .field("name", &self.name)
80 .field("revalidate", &self.revalidate)
81 .field("params_fn", &self.params_fn.as_ref().map(|_| "..."))
82 .finish()
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct StaticManifest {
92 pub generated_at: String,
94 pub autumn_version: String,
96 pub routes: HashMap<String, ManifestEntry>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ManifestEntry {
103 pub file: String,
106 pub revalidate: Option<u64>,
109}
110
111impl StaticManifest {
112 pub fn load(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
118 let contents = std::fs::read_to_string(path)?;
119 let manifest: Self = serde_json::from_str(&contents)?;
120 Ok(manifest)
121 }
122}
123
124#[must_use]
136pub fn url_to_file_path(url_path: &str) -> String {
137 let trimmed = url_path.trim_matches('/');
138 if trimmed.is_empty() {
139 "index.html".to_owned()
140 } else {
141 format!("{trimmed}/index.html")
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use std::io::Write;
149
150 #[test]
151 fn url_to_file_path_root() {
152 assert_eq!(url_to_file_path("/"), "index.html");
153 }
154
155 #[test]
156 fn url_to_file_path_simple() {
157 assert_eq!(url_to_file_path("/about"), "about/index.html");
158 }
159
160 #[test]
161 fn url_to_file_path_nested() {
162 assert_eq!(url_to_file_path("/posts/hello"), "posts/hello/index.html");
163 }
164
165 #[test]
166 fn url_to_file_path_trailing_slash() {
167 assert_eq!(url_to_file_path("/about/"), "about/index.html");
168 }
169
170 #[test]
171 fn manifest_roundtrip() {
172 let mut routes = HashMap::new();
173 routes.insert(
174 "/".to_owned(),
175 ManifestEntry {
176 file: "index.html".to_owned(),
177 revalidate: None,
178 },
179 );
180 routes.insert(
181 "/about".to_owned(),
182 ManifestEntry {
183 file: "about/index.html".to_owned(),
184 revalidate: Some(3600),
185 },
186 );
187
188 let manifest = StaticManifest {
189 generated_at: "2026-03-27T12:00:00Z".to_owned(),
190 autumn_version: "0.3.0".to_owned(),
191 routes,
192 };
193
194 let json = serde_json::to_string(&manifest).expect("serialize");
196
197 let dir = tempfile::tempdir().expect("tempdir");
199 let file_path = dir.path().join("manifest.json");
200 {
201 let mut f = std::fs::File::create(&file_path).expect("create file");
202 f.write_all(json.as_bytes()).expect("write");
203 }
204
205 let loaded = StaticManifest::load(&file_path).expect("load");
206
207 assert_eq!(loaded.generated_at, "2026-03-27T12:00:00Z");
208 assert_eq!(loaded.autumn_version, "0.3.0");
209 assert_eq!(loaded.routes.len(), 2);
210
211 let root_entry = loaded.routes.get("/").expect("root route");
212 assert_eq!(root_entry.file, "index.html");
213 assert!(root_entry.revalidate.is_none());
214
215 let about_entry = loaded.routes.get("/about").expect("about route");
216 assert_eq!(about_entry.file, "about/index.html");
217 assert_eq!(about_entry.revalidate, Some(3600));
218 }
219
220 #[test]
221 fn static_route_meta_clone() {
222 let meta = StaticRouteMeta {
223 path: "/test",
224 name: "test_handler",
225 revalidate: Some(60),
226 params_fn: None,
227 };
228 let copy = meta.clone();
229 assert_eq!(meta.path, copy.path);
231 assert_eq!(copy.name, "test_handler");
232 assert_eq!(copy.revalidate, Some(60));
233 }
234
235 #[test]
236 fn static_params_macro() {
237 let params = static_params! { "slug" => "hello-world" };
238 assert_eq!(params.get("slug").unwrap(), "hello-world");
239 }
240
241 #[test]
242 fn static_params_macro_multiple() {
243 let params = static_params! {
244 "year" => "2026",
245 "month" => "03",
246 "slug" => "hello",
247 };
248 assert_eq!(params.len(), 3);
249 assert_eq!(params.get("year").unwrap(), "2026");
250 assert_eq!(params.get("month").unwrap(), "03");
251 assert_eq!(params.get("slug").unwrap(), "hello");
252 }
253
254 #[test]
255 fn static_params_macro_empty() {
256 let params: StaticParams = static_params! {};
257 assert!(params.is_empty());
258 }
259
260 #[test]
261 fn static_route_meta_with_params_fn() {
262 fn dummy_params(
263 _router: axum::Router,
264 ) -> Pin<Box<dyn Future<Output = Vec<StaticParams>> + Send>> {
265 Box::pin(async { vec![static_params! { "slug" => "test" }] })
266 }
267
268 let meta = StaticRouteMeta {
269 path: "/posts/{slug}",
270 name: "show_post",
271 revalidate: None,
272 params_fn: Some(dummy_params),
273 };
274 assert!(meta.params_fn.is_some());
275 assert_eq!(meta.path, "/posts/{slug}");
276 }
277
278 #[test]
279 fn static_route_meta_debug() {
280 let meta = StaticRouteMeta {
281 path: "/test",
282 name: "test",
283 revalidate: None,
284 params_fn: None,
285 };
286 let debug = format!("{meta:?}");
287 assert!(debug.contains("test"));
288 }
289}