actix_sitemaps_rs/
lib.rs

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}