reinhardt_utils/staticfiles/
cdn.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum CdnProvider {
12 CloudFront,
14 Fastly,
16 Cloudflare,
18 Custom(String),
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CdnConfig {
25 pub enabled: bool,
27 pub provider: CdnProvider,
29 pub base_url: String,
31 pub path_prefix: Option<String>,
33 pub custom_headers: HashMap<String, String>,
35 pub use_https: bool,
37}
38
39impl CdnConfig {
40 pub fn new(provider: CdnProvider, base_url: String) -> Self {
53 Self {
54 enabled: true,
55 provider,
56 base_url: base_url.trim_end_matches('/').to_string(),
57 path_prefix: None,
58 custom_headers: HashMap::new(),
59 use_https: true,
60 }
61 }
62
63 pub fn disabled() -> Self {
65 Self {
66 enabled: false,
67 provider: CdnProvider::Custom("none".to_string()),
68 base_url: String::new(),
69 path_prefix: None,
70 custom_headers: HashMap::new(),
71 use_https: true,
72 }
73 }
74
75 pub fn with_path_prefix(mut self, prefix: String) -> Self {
77 self.path_prefix = Some(prefix.trim_start_matches('/').to_string());
78 self
79 }
80
81 pub fn with_custom_header(mut self, key: String, value: String) -> Self {
83 self.custom_headers.insert(key, value);
84 self
85 }
86
87 pub fn without_https(mut self) -> Self {
89 self.use_https = false;
90 self
91 }
92}
93
94pub struct CdnUrlGenerator {
96 config: CdnConfig,
97}
98
99impl CdnUrlGenerator {
100 pub fn new(config: CdnConfig) -> Self {
114 Self { config }
115 }
116
117 pub fn generate_url(&self, path: &str) -> String {
135 if !self.config.enabled {
136 return path.to_string();
137 }
138
139 let scheme = if self.config.use_https {
140 "https"
141 } else {
142 "http"
143 };
144 let path = path.trim_start_matches('/');
145
146 let full_path = if let Some(prefix) = &self.config.path_prefix {
147 format!("{}/{}", prefix.trim_end_matches('/'), path)
148 } else {
149 path.to_string()
150 };
151
152 format!("{}://{}/{}", scheme, self.config.base_url, full_path)
153 }
154
155 pub fn generate_urls(&self, paths: &[&str]) -> Vec<String> {
157 paths.iter().map(|p| self.generate_url(p)).collect()
158 }
159
160 pub fn generate_versioned_url(&self, path: &str, version: &str) -> String {
178 let base_url = self.generate_url(path);
179 format!("{}?v={}", base_url, version)
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct CdnInvalidationRequest {
186 pub paths: Vec<String>,
188 pub caller_reference: Option<String>,
190}
191
192impl CdnInvalidationRequest {
193 pub fn new(paths: Vec<String>) -> Self {
206 Self {
207 paths,
208 caller_reference: None,
209 }
210 }
211
212 pub fn with_caller_reference(mut self, reference: String) -> Self {
214 self.caller_reference = Some(reference);
215 self
216 }
217
218 pub fn add_path(&mut self, path: String) {
220 self.paths.push(path);
221 }
222
223 pub fn add_paths(&mut self, paths: Vec<String>) {
225 self.paths.extend(paths);
226 }
227}
228
229pub struct CdnPurgeHelper {
231 config: CdnConfig,
232}
233
234impl CdnPurgeHelper {
235 pub fn new(config: CdnConfig) -> Self {
249 Self { config }
250 }
251
252 pub fn create_invalidation_request(&self, paths: Vec<String>) -> CdnInvalidationRequest {
271 CdnInvalidationRequest::new(paths)
272 }
273
274 pub fn create_wildcard_invalidation(&self, pattern: &str) -> CdnInvalidationRequest {
276 CdnInvalidationRequest::new(vec![pattern.to_string()])
277 }
278
279 pub fn get_purge_endpoint(&self) -> String {
294 match &self.config.provider {
295 CdnProvider::CloudFront => {
296 "https://cloudfront.amazonaws.com/2020-05-31/distribution/{distribution-id}/invalidation".to_string()
299 }
300 CdnProvider::Fastly => {
301 "https://api.fastly.com/service/{service-id}/purge".to_string()
304 }
305 CdnProvider::Cloudflare => {
306 "https://api.cloudflare.com/client/v4/zones/{zone-id}/purge_cache".to_string()
309 }
310 CdnProvider::Custom(name) => {
311 format!("custom://{}/purge", name)
313 }
314 }
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_cdn_config_creation() {
324 let config = CdnConfig::new(
325 CdnProvider::CloudFront,
326 "d1234567890.cloudfront.net".to_string(),
327 );
328
329 assert!(config.enabled);
330 assert_eq!(config.provider, CdnProvider::CloudFront);
331 assert_eq!(config.base_url, "d1234567890.cloudfront.net");
332 assert!(config.use_https);
333 }
334
335 #[test]
336 fn test_cdn_url_generation() {
337 let config = CdnConfig::new(
338 CdnProvider::CloudFront,
339 "d1234567890.cloudfront.net".to_string(),
340 );
341 let generator = CdnUrlGenerator::new(config);
342
343 let url = generator.generate_url("/css/style.css");
344 assert_eq!(url, "https://d1234567890.cloudfront.net/css/style.css");
345 }
346
347 #[test]
348 fn test_cdn_url_generation_with_prefix() {
349 let config = CdnConfig::new(
350 CdnProvider::CloudFront,
351 "d1234567890.cloudfront.net".to_string(),
352 )
353 .with_path_prefix("static".to_string());
354
355 let generator = CdnUrlGenerator::new(config);
356 let url = generator.generate_url("/css/style.css");
357
358 assert_eq!(
359 url,
360 "https://d1234567890.cloudfront.net/static/css/style.css"
361 );
362 }
363
364 #[test]
365 fn test_cdn_url_generation_without_https() {
366 let config =
367 CdnConfig::new(CdnProvider::Fastly, "example.fastly.net".to_string()).without_https();
368
369 let generator = CdnUrlGenerator::new(config);
370 let url = generator.generate_url("/image.png");
371
372 assert_eq!(url, "http://example.fastly.net/image.png");
373 }
374
375 #[test]
376 fn test_versioned_url_generation() {
377 let config = CdnConfig::new(
378 CdnProvider::CloudFront,
379 "d1234567890.cloudfront.net".to_string(),
380 );
381 let generator = CdnUrlGenerator::new(config);
382
383 let url = generator.generate_versioned_url("/css/style.css", "v1.2.3");
384 assert_eq!(
385 url,
386 "https://d1234567890.cloudfront.net/css/style.css?v=v1.2.3"
387 );
388 }
389
390 #[test]
391 fn test_multiple_urls_generation() {
392 let config = CdnConfig::new(CdnProvider::Cloudflare, "cdn.example.com".to_string());
393 let generator = CdnUrlGenerator::new(config);
394
395 let paths = vec!["/css/style.css", "/js/app.js", "/img/logo.png"];
396 let urls = generator.generate_urls(&paths);
397
398 assert_eq!(urls.len(), 3);
399 assert_eq!(urls[0], "https://cdn.example.com/css/style.css");
400 assert_eq!(urls[1], "https://cdn.example.com/js/app.js");
401 assert_eq!(urls[2], "https://cdn.example.com/img/logo.png");
402 }
403
404 #[test]
405 fn test_disabled_cdn_returns_original_path() {
406 let config = CdnConfig::disabled();
407 let generator = CdnUrlGenerator::new(config);
408
409 let url = generator.generate_url("/css/style.css");
410 assert_eq!(url, "/css/style.css");
411 }
412
413 #[test]
414 fn test_invalidation_request_creation() {
415 let request = CdnInvalidationRequest::new(vec![
416 "/css/style.css".to_string(),
417 "/js/app.js".to_string(),
418 ]);
419
420 assert_eq!(request.paths.len(), 2);
421 assert!(request.caller_reference.is_none());
422 }
423
424 #[test]
425 fn test_invalidation_request_with_caller_reference() {
426 let request = CdnInvalidationRequest::new(vec!["/css/style.css".to_string()])
427 .with_caller_reference("unique-id-123".to_string());
428
429 assert_eq!(request.caller_reference, Some("unique-id-123".to_string()));
430 }
431
432 #[test]
433 fn test_add_paths_to_invalidation_request() {
434 let mut request = CdnInvalidationRequest::new(vec!["/css/style.css".to_string()]);
435 request.add_path("/js/app.js".to_string());
436 request.add_paths(vec![
437 "/img/logo.png".to_string(),
438 "/fonts/font.woff2".to_string(),
439 ]);
440
441 assert_eq!(request.paths.len(), 4);
442 }
443
444 #[test]
445 fn test_purge_helper_creates_request() {
446 let config = CdnConfig::new(
447 CdnProvider::CloudFront,
448 "d1234567890.cloudfront.net".to_string(),
449 );
450 let helper = CdnPurgeHelper::new(config);
451
452 let request = helper.create_invalidation_request(vec![
453 "/css/style.css".to_string(),
454 "/js/app.js".to_string(),
455 ]);
456
457 assert_eq!(request.paths.len(), 2);
458 }
459
460 #[test]
461 fn test_wildcard_invalidation() {
462 let config = CdnConfig::new(
463 CdnProvider::CloudFront,
464 "d1234567890.cloudfront.net".to_string(),
465 );
466 let helper = CdnPurgeHelper::new(config);
467
468 let request = helper.create_wildcard_invalidation("/css/*");
469 assert_eq!(request.paths, vec!["/css/*"]);
470 }
471
472 #[test]
473 fn test_purge_endpoints() {
474 let config = CdnConfig::new(CdnProvider::CloudFront, "example.com".to_string());
476 let helper = CdnPurgeHelper::new(config);
477 let endpoint = helper.get_purge_endpoint();
478 assert!(endpoint.contains("cloudfront.amazonaws.com"));
479 assert!(endpoint.contains("invalidation"));
480
481 let config = CdnConfig::new(CdnProvider::Fastly, "example.com".to_string());
483 let helper = CdnPurgeHelper::new(config);
484 let endpoint = helper.get_purge_endpoint();
485 assert!(endpoint.contains("api.fastly.com"));
486 assert!(endpoint.contains("purge"));
487
488 let config = CdnConfig::new(CdnProvider::Cloudflare, "example.com".to_string());
490 let helper = CdnPurgeHelper::new(config);
491 let endpoint = helper.get_purge_endpoint();
492 assert!(endpoint.contains("api.cloudflare.com"));
493 assert!(endpoint.contains("purge_cache"));
494
495 let config = CdnConfig::new(
497 CdnProvider::Custom("mycdn".to_string()),
498 "example.com".to_string(),
499 );
500 let helper = CdnPurgeHelper::new(config);
501 let endpoint = helper.get_purge_endpoint();
502 assert!(endpoint.contains("custom://mycdn/purge"));
503 }
504
505 #[test]
506 fn test_custom_provider() {
507 let config = CdnConfig::new(
508 CdnProvider::Custom("my-cdn".to_string()),
509 "cdn.mycompany.com".to_string(),
510 );
511
512 assert_eq!(config.provider, CdnProvider::Custom("my-cdn".to_string()));
513 }
514
515 #[test]
516 fn test_custom_headers() {
517 let config = CdnConfig::new(
518 CdnProvider::CloudFront,
519 "d1234567890.cloudfront.net".to_string(),
520 )
521 .with_custom_header("X-Custom-Header".to_string(), "value".to_string())
522 .with_custom_header("Authorization".to_string(), "Bearer token".to_string());
523
524 assert_eq!(config.custom_headers.len(), 2);
525 assert_eq!(
526 config.custom_headers.get("X-Custom-Header"),
527 Some(&"value".to_string())
528 );
529 }
530}