1use actix_web::{
2 dev::ResourcePath,
3 get,
4 http::{self, header::ContentType},
5 web, HttpResponse,
6};
7
8use std::{
9 fs,
10 path::{Path, PathBuf},
11 sync::Arc,
12};
13
14pub trait NotFoundStrategy: Send + Sync {
15 fn handle_not_found(&self) -> HttpResponse;
16}
17
18pub struct RedirectToRootStrategy;
19impl NotFoundStrategy for RedirectToRootStrategy {
20 fn handle_not_found(&self) -> HttpResponse {
21 let redirect_response = HttpResponse::build(http::StatusCode::PERMANENT_REDIRECT)
22 .append_header(("Location", "/"))
23 .finish();
24
25 redirect_response
26 }
27}
28
29pub struct ShowErrorMessageStrategy;
30impl NotFoundStrategy for ShowErrorMessageStrategy {
31 fn handle_not_found(&self) -> HttpResponse {
32 HttpResponse::build(http::StatusCode::NOT_FOUND).body("404 Not Found")
33 }
34}
35
36#[derive(Clone)]
37pub struct Sitemap {
38 pub static_file_path: PathBuf,
39 pub web_directory: PathBuf,
40 pub web_filename: PathBuf,
41 pub not_found_strategy: Arc<Box<dyn NotFoundStrategy>>,
42}
43
44pub struct SitemapBuilder {
45 pub static_file_path: String,
46 pub web_directory: String,
47 pub web_filename: String,
48 pub not_found_strategy: Box<dyn NotFoundStrategy>,
49}
50
51impl Default for SitemapBuilder {
52 fn default() -> Self {
53 Self {
54 static_file_path: String::from("sitemaps.xml"),
55 web_directory: String::from(""),
56 web_filename: String::from("sitemaps.xml"),
57 not_found_strategy: Box::new(ShowErrorMessageStrategy) as Box<dyn NotFoundStrategy>,
58 }
59 }
60}
61
62impl SitemapBuilder {
63 pub fn static_file(mut self, static_file_path: String) -> Self {
64 self.static_file_path = static_file_path;
65 self
66 }
67
68 pub fn web_directory(mut self, web_directory: String) -> Self {
69 self.web_directory = web_directory;
70 self
71 }
72
73 pub fn web_filename(mut self, web_filename: String) -> Self {
74 self.web_filename = web_filename;
75 self
76 }
77
78 pub fn not_found_strategy(mut self, strategy: impl NotFoundStrategy + 'static) -> Self {
79 self.not_found_strategy = Box::new(strategy);
80 self
81 }
82
83 pub fn build(self) -> Sitemap {
84 Sitemap {
85 static_file_path: Path::new(&self.static_file_path).to_path_buf(),
86 web_directory: Path::new(&self.web_directory).to_path_buf(),
87 web_filename: Path::new(&self.web_filename).to_path_buf(),
88 not_found_strategy: Arc::new(self.not_found_strategy),
89 }
90 }
91}
92
93#[get("/{requested_path:.*}")]
94pub async fn serve_sitemap(
95 requested_path: web::Path<String>,
96 data: web::Data<Sitemap>,
97) -> HttpResponse {
98 let expected_path = data.web_directory.join(data.web_filename.as_path());
99 let requested_path = Path::new(requested_path.path()).to_path_buf();
100
101 if requested_path != expected_path {
102 let strategy = data.not_found_strategy.clone();
103 return strategy.handle_not_found();
104 }
105
106 let sitemap_fs =
107 fs::read_to_string(&data.static_file_path).expect("Can't open sitemaps file !");
108 HttpResponse::Ok()
109 .content_type(ContentType::xml())
110 .body(sitemap_fs)
111}
112
113#[cfg(test)]
114mod tests {
115 use crate::{serve_sitemap, RedirectToRootStrategy, ShowErrorMessageStrategy, SitemapBuilder};
116 use actix_web::web::Data;
117 use actix_web::{http::header::ContentType, test, App};
118
119 #[actix_web::test]
120 async fn given_sitemap_at_root_then_get_success_status_code_when_show_error_message_strategy() {
121 let sitemap = SitemapBuilder::default()
122 .static_file("./tests/sitemaps.xml".to_string())
123 .web_filename("sitemaps.xml".to_string())
124 .not_found_strategy(ShowErrorMessageStrategy)
125 .build();
126
127 let app = test::init_service(
128 App::new()
129 .app_data(Data::new(sitemap.clone()))
130 .service(serve_sitemap),
131 )
132 .await;
133
134 let req = test::TestRequest::default()
135 .insert_header(ContentType::xml())
136 .uri("/sitemaps.xml")
137 .to_request();
138
139 let resp = test::call_service(&app, req).await;
140 assert!(resp.status().is_success());
141 }
142
143 #[actix_web::test]
144 async fn given_sitemap_with_webdirectory_then_get_success_status_code_when_show_error_message_strategy(
145 ) {
146 let sitemap = SitemapBuilder::default()
147 .static_file("./tests/sitemaps.xml".to_string())
148 .web_directory(".well-known/".to_string())
149 .web_filename("sitemaps.xml".to_string())
150 .not_found_strategy(ShowErrorMessageStrategy)
151 .build();
152
153 let app = test::init_service(
154 App::new()
155 .app_data(Data::new(sitemap.clone()))
156 .service(serve_sitemap),
157 )
158 .await;
159
160 let req = test::TestRequest::default()
161 .insert_header(ContentType::xml())
162 .uri("/.well-known/sitemaps.xml")
163 .to_request();
164
165 let resp = test::call_service(&app, req).await;
166 assert!(resp.status().is_success());
167 }
168
169 #[actix_web::test]
170 async fn given_sitemap_then_get_not_found_when_show_error_message_strategy() {
171 let sitemap = SitemapBuilder::default()
172 .static_file("./tests/sitemaps.xml".to_string())
173 .web_directory(".well-known/".to_string())
174 .web_filename("sitemaps.xml".to_string())
175 .not_found_strategy(ShowErrorMessageStrategy)
176 .build();
177
178 let app = test::init_service(
179 App::new()
180 .app_data(Data::new(sitemap.clone()))
181 .service(serve_sitemap),
182 )
183 .await;
184
185 let req = test::TestRequest::default()
186 .insert_header(ContentType::xml())
187 .uri("/notfound/sitemaps.xml")
188 .to_request();
189
190 let resp = test::call_service(&app, req).await;
191 assert_eq!(resp.status().as_u16(), 404);
192 }
193
194 #[actix_web::test]
195 async fn given_sitemap_then_get_not_found_when_bad_path_and_redirect_to_root_strategy() {
196 let sitemap = SitemapBuilder::default()
197 .static_file("./tests/sitemaps.xml".to_string())
198 .web_directory("./.well-known/".to_string())
199 .web_filename("sitemaps.xml".to_string())
200 .not_found_strategy(RedirectToRootStrategy)
201 .build();
202
203 let app = test::init_service(
204 App::new()
205 .app_data(Data::new(sitemap.clone()))
206 .service(serve_sitemap),
207 )
208 .await;
209
210 let req = test::TestRequest::default()
211 .insert_header(ContentType::xml())
212 .uri("/notfound/sitemaps.xml")
213 .to_request();
214
215 let resp = test::call_service(&app, req).await;
216 assert_eq!(resp.headers().get("location").unwrap(), "/");
217 }
218}