reinhardt_utils/staticfiles/
template_integration.rs1use super::{ManifestStaticFilesStorage, StaticFilesConfig};
6use std::collections::HashMap;
7use std::io;
8
9#[derive(Debug, Clone)]
14pub struct TemplateStaticConfig {
15 pub static_url: String,
17 pub use_manifest: bool,
19 pub manifest: HashMap<String, String>,
21}
22
23impl From<&StaticFilesConfig> for TemplateStaticConfig {
24 fn from(config: &StaticFilesConfig) -> Self {
25 Self {
26 static_url: config.static_url.clone(),
27 use_manifest: false,
28 manifest: HashMap::new(),
29 }
30 }
31}
32
33impl TemplateStaticConfig {
34 pub fn new(static_url: String) -> Self {
46 Self {
47 static_url,
48 use_manifest: false,
49 manifest: HashMap::new(),
50 }
51 }
52
53 pub fn with_manifest(mut self, manifest: HashMap<String, String>) -> Self {
71 self.use_manifest = true;
72 self.manifest = manifest;
73 self
74 }
75
76 pub async fn from_storage(storage: &ManifestStaticFilesStorage) -> io::Result<Self> {
97 let manifest_path = storage.location.join(&storage.manifest_name);
98
99 if !manifest_path.exists() {
100 return Ok(Self {
101 static_url: storage.base_url.clone(),
102 use_manifest: false,
103 manifest: HashMap::new(),
104 });
105 }
106
107 let manifest_content = tokio::fs::read_to_string(&manifest_path).await?;
108
109 let manifest =
111 if let Ok(structured) = serde_json::from_str::<serde_json::Value>(&manifest_content) {
112 if let Some(paths) = structured.get("paths").and_then(|v| v.as_object()) {
113 paths
114 .iter()
115 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
116 .collect()
117 } else if let Some(files) = structured.get("files").and_then(|v| v.as_object()) {
118 files
120 .iter()
121 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
122 .collect()
123 } else {
124 serde_json::from_str::<HashMap<String, String>>(&manifest_content)
126 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
127 }
128 } else {
129 return Err(io::Error::new(
130 io::ErrorKind::InvalidData,
131 "Invalid manifest JSON",
132 ));
133 };
134
135 Ok(Self {
136 static_url: storage.base_url.clone(),
137 use_manifest: true,
138 manifest,
139 })
140 }
141
142 pub fn resolve_url(&self, name: &str) -> String {
189 let (path, query_fragment) = match name.split_once('?') {
191 Some((p, qf)) => (p, Some(qf)),
192 None => (name, None),
193 };
194
195 let resolved_path = if self.use_manifest {
197 self.manifest.get(path).map(|s| s.as_str()).unwrap_or(path)
198 } else {
199 path
200 };
201
202 let base = self.static_url.trim_end_matches('/');
204 let path = resolved_path.trim_start_matches('/');
205 let mut url = format!("{}/{}", base, path);
206
207 if let Some(qf) = query_fragment {
209 url.push('?');
210 url.push_str(qf);
211 }
212
213 url
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_template_static_config_new() {
223 let config = TemplateStaticConfig::new("/static/".to_string());
224 assert_eq!(config.static_url, "/static/");
225 assert!(!config.use_manifest);
226 assert!(config.manifest.is_empty());
227 }
228
229 #[test]
230 fn test_template_static_config_with_manifest() {
231 let mut manifest = HashMap::new();
232 manifest.insert(
233 "css/style.css".to_string(),
234 "css/style.abc123.css".to_string(),
235 );
236
237 let config =
238 TemplateStaticConfig::new("/static/".to_string()).with_manifest(manifest.clone());
239
240 assert_eq!(config.static_url, "/static/");
241 assert!(config.use_manifest);
242 assert_eq!(config.manifest.len(), 1);
243 assert_eq!(
244 config.manifest.get("css/style.css"),
245 Some(&"css/style.abc123.css".to_string())
246 );
247 }
248
249 #[test]
250 fn test_template_static_config_from_static_files_config() {
251 let static_config = StaticFilesConfig {
252 static_root: std::path::PathBuf::from("/var/www/static"),
253 static_url: "/assets/".to_string(),
254 staticfiles_dirs: vec![],
255 media_url: None,
256 };
257
258 let template_config = TemplateStaticConfig::from(&static_config);
259 assert_eq!(template_config.static_url, "/assets/");
260 assert!(!template_config.use_manifest);
261 assert!(template_config.manifest.is_empty());
262 }
263
264 #[tokio::test]
265 async fn test_from_storage() {
266 use tempfile::tempdir;
267
268 let temp_dir = tempdir().unwrap();
269 let static_root = temp_dir.path().to_path_buf();
270
271 let manifest_content = r#"{
273 "version": "1.0",
274 "paths": {
275 "css/style.css": "css/style.abc123.css",
276 "js/app.js": "js/app.def456.js"
277 }
278}"#;
279
280 std::fs::write(static_root.join("staticfiles.json"), manifest_content).unwrap();
281
282 let storage = ManifestStaticFilesStorage::new(static_root, "/static/");
283 let config = TemplateStaticConfig::from_storage(&storage).await.unwrap();
284
285 assert_eq!(config.static_url, "/static/");
286 assert!(config.use_manifest);
287 assert_eq!(config.manifest.len(), 2);
288 assert_eq!(
289 config.manifest.get("css/style.css"),
290 Some(&"css/style.abc123.css".to_string())
291 );
292 assert_eq!(
293 config.manifest.get("js/app.js"),
294 Some(&"js/app.def456.js".to_string())
295 );
296 }
297
298 #[tokio::test]
299 async fn test_from_storage_with_version_and_paths() {
300 use tempfile::tempdir;
301
302 let temp_dir = tempdir().unwrap();
303 let static_root = temp_dir.path().to_path_buf();
304
305 let manifest_content = r#"{
307 "version": "1.0",
308 "paths": {
309 "css/style.css": "css/style.abc123.css"
310 }
311}"#;
312
313 std::fs::write(static_root.join("staticfiles.json"), manifest_content).unwrap();
314
315 let storage = ManifestStaticFilesStorage::new(static_root, "/static/");
316 let config = TemplateStaticConfig::from_storage(&storage).await.unwrap();
317
318 assert_eq!(config.static_url, "/static/");
319 assert!(config.use_manifest);
320 assert_eq!(config.manifest.len(), 1);
321 assert_eq!(
322 config.manifest.get("css/style.css"),
323 Some(&"css/style.abc123.css".to_string())
324 );
325 }
326
327 #[tokio::test]
328 async fn test_from_storage_with_legacy_files_key() {
329 use tempfile::tempdir;
330
331 let temp_dir = tempdir().unwrap();
332 let static_root = temp_dir.path().to_path_buf();
333
334 let manifest_content = r#"{
336 "version": "1.0",
337 "files": {
338 "js/app.js": "js/app.def456.js"
339 }
340}"#;
341
342 std::fs::write(static_root.join("staticfiles.json"), manifest_content).unwrap();
343
344 let storage = ManifestStaticFilesStorage::new(static_root, "/static/");
345 let config = TemplateStaticConfig::from_storage(&storage).await.unwrap();
346
347 assert_eq!(config.static_url, "/static/");
348 assert!(config.use_manifest);
349 assert_eq!(config.manifest.len(), 1);
350 assert_eq!(
351 config.manifest.get("js/app.js"),
352 Some(&"js/app.def456.js".to_string())
353 );
354 }
355
356 #[test]
357 fn test_resolve_url_basic() {
358 let config = TemplateStaticConfig::new("/static/".to_string());
359 assert_eq!(config.resolve_url("css/style.css"), "/static/css/style.css");
360 }
361
362 #[test]
363 fn test_resolve_url_with_leading_slash() {
364 let config = TemplateStaticConfig::new("/static/".to_string());
365 assert_eq!(
366 config.resolve_url("/css/style.css"),
367 "/static/css/style.css"
368 );
369 }
370
371 #[test]
372 fn test_resolve_url_with_manifest() {
373 let mut manifest = HashMap::new();
374 manifest.insert(
375 "css/style.css".to_string(),
376 "css/style.abc123.css".to_string(),
377 );
378
379 let config = TemplateStaticConfig::new("/static/".to_string()).with_manifest(manifest);
380
381 assert_eq!(
382 config.resolve_url("css/style.css"),
383 "/static/css/style.abc123.css"
384 );
385 }
386
387 #[test]
388 fn test_resolve_url_manifest_fallback() {
389 let mut manifest = HashMap::new();
390 manifest.insert(
391 "css/style.css".to_string(),
392 "css/style.abc123.css".to_string(),
393 );
394
395 let config = TemplateStaticConfig::new("/static/".to_string()).with_manifest(manifest);
396
397 assert_eq!(config.resolve_url("js/app.js"), "/static/js/app.js");
399 }
400
401 #[test]
402 fn test_resolve_url_with_query_string() {
403 let config = TemplateStaticConfig::new("/static/".to_string());
404 assert_eq!(config.resolve_url("test.css?v=1"), "/static/test.css?v=1");
405 }
406
407 #[test]
408 fn test_resolve_url_with_fragment() {
409 let config = TemplateStaticConfig::new("/static/".to_string());
410 assert_eq!(
411 config.resolve_url("test.css#section"),
412 "/static/test.css#section"
413 );
414 }
415
416 #[test]
417 fn test_resolve_url_with_query_and_fragment() {
418 let config = TemplateStaticConfig::new("/static/".to_string());
419 assert_eq!(
420 config.resolve_url("test.css?v=1#section"),
421 "/static/test.css?v=1#section"
422 );
423 }
424
425 #[test]
426 fn test_resolve_url_manifest_with_query_string() {
427 let mut manifest = HashMap::new();
428 manifest.insert(
429 "css/style.css".to_string(),
430 "css/style.abc123.css".to_string(),
431 );
432
433 let config = TemplateStaticConfig::new("/static/".to_string()).with_manifest(manifest);
434
435 assert_eq!(
437 config.resolve_url("css/style.css?v=1"),
438 "/static/css/style.abc123.css?v=1"
439 );
440 }
441
442 #[test]
443 fn test_resolve_url_different_base_urls() {
444 let config1 = TemplateStaticConfig::new("/static/".to_string());
445 assert_eq!(config1.resolve_url("test.txt"), "/static/test.txt");
446
447 let config2 = TemplateStaticConfig::new("/static".to_string());
448 assert_eq!(config2.resolve_url("test.txt"), "/static/test.txt");
449
450 let config3 = TemplateStaticConfig::new("static/".to_string());
451 assert_eq!(config3.resolve_url("test.txt"), "static/test.txt");
452 }
453
454 #[test]
455 fn test_resolve_url_empty_path() {
456 let config = TemplateStaticConfig::new("/static/".to_string());
457 assert_eq!(config.resolve_url(""), "/static/");
458 }
459
460 #[test]
461 fn test_resolve_url_cdn_url() {
462 let config = TemplateStaticConfig::new("https://cdn.example.com/static/".to_string());
463 assert_eq!(
464 config.resolve_url("css/style.css"),
465 "https://cdn.example.com/static/css/style.css"
466 );
467 }
468}