1use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone)]
8pub struct ErrorTemplate {
9 pub status_code: u16,
11 pub content: String,
13 pub content_type: String,
15}
16
17impl ErrorTemplate {
18 pub fn new(status_code: u16, content: impl Into<String>) -> Self {
20 Self {
21 status_code,
22 content: content.into(),
23 content_type: "text/html; charset=utf-8".to_string(),
24 }
25 }
26
27 pub fn with_content_type(status_code: u16, content: impl Into<String>, content_type: impl Into<String>) -> Self {
29 Self {
30 status_code,
31 content: content.into(),
32 content_type: content_type.into(),
33 }
34 }
35
36 pub fn default_for(status_code: u16) -> Self {
38 let (title, message) = get_default_error_message(status_code);
39 let content = generate_default_html(status_code, &title, &message);
40 Self::new(status_code, content)
41 }
42}
43
44fn get_default_error_message(status_code: u16) -> (String, String) {
46 match status_code {
47 400 => ("Bad Request".to_string(), "The request could not be understood by the server.".to_string()),
48 401 => ("Unauthorized".to_string(), "Authentication is required to access this resource.".to_string()),
49 403 => ("Forbidden".to_string(), "You don't have permission to access this resource.".to_string()),
50 404 => ("Not Found".to_string(), "The requested resource could not be found on this server.".to_string()),
51 405 => ("Method Not Allowed".to_string(), "The request method is not supported for this resource.".to_string()),
52 408 => ("Request Timeout".to_string(), "The server timed out waiting for the request.".to_string()),
53 413 => ("Payload Too Large".to_string(), "The request entity is larger than the server is willing to process.".to_string()),
54 414 => ("URI Too Long".to_string(), "The URL requested is too long for the server to process.".to_string()),
55 429 => ("Too Many Requests".to_string(), "You have sent too many requests in a given amount of time.".to_string()),
56 500 => ("Internal Server Error".to_string(), "The server encountered an unexpected condition that prevented it from fulfilling the request.".to_string()),
57 501 => ("Not Implemented".to_string(), "The server does not support the functionality required to fulfill the request.".to_string()),
58 502 => ("Bad Gateway".to_string(), "The server received an invalid response from an upstream server.".to_string()),
59 503 => ("Service Unavailable".to_string(), "The server is currently unable to handle the request due to temporary overload or maintenance.".to_string()),
60 504 => ("Gateway Timeout".to_string(), "The server did not receive a timely response from an upstream server.".to_string()),
61 _ => ("Error".to_string(), format!("An error occurred (HTTP {}).", status_code)),
62 }
63}
64
65fn generate_default_html(status_code: u16, title: &str, message: &str) -> String {
67 format!(
68 r#"<!DOCTYPE html>
69<html lang="en">
70<head>
71 <meta charset="UTF-8">
72 <meta name="viewport" content="width=device-width, initial-scale=1.0">
73 <title>{} - Rust Serv</title>
74 <style>
75 body {{
76 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
77 display: flex;
78 align-items: center;
79 justify-content: center;
80 min-height: 100vh;
81 margin: 0;
82 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
83 color: #333;
84 }}
85 .container {{
86 text-align: center;
87 background: white;
88 padding: 60px;
89 border-radius: 20px;
90 box-shadow: 0 20px 60px rgba(0,0,0,0.3);
91 max-width: 500px;
92 }}
93 .status-code {{
94 font-size: 120px;
95 font-weight: bold;
96 color: #764ba2;
97 margin: 0;
98 line-height: 1;
99 }}
100 .title {{
101 font-size: 24px;
102 color: #333;
103 margin: 20px 0 10px;
104 }}
105 .message {{
106 font-size: 16px;
107 color: #666;
108 margin: 0;
109 }}
110 .footer {{
111 margin-top: 30px;
112 font-size: 14px;
113 color: #999;
114 }}
115 </style>
116</head>
117<body>
118 <div class="container">
119 <p class="status-code">{}</p>
120 <h1 class="title">{}</h1>
121 <p class="message">{}</p>
122 <p class="footer">Powered by Rust Serv</p>
123 </div>
124</body>
125</html>"#,
126 title, status_code, title, message
127 )
128}
129
130#[derive(Debug, Clone)]
132pub struct ErrorTemplates {
133 templates: HashMap<u16, ErrorTemplate>,
135 template_files: HashMap<u16, PathBuf>,
137}
138
139impl ErrorTemplates {
140 pub fn new() -> Self {
142 Self {
143 templates: HashMap::new(),
144 template_files: HashMap::new(),
145 }
146 }
147
148 pub fn set_template(&mut self, status_code: u16, template: ErrorTemplate) {
150 self.templates.insert(status_code, template);
151 }
152
153 pub fn set_template_file(&mut self, status_code: u16, path: impl Into<PathBuf>) {
155 self.template_files.insert(status_code, path.into());
156 }
157
158 pub fn get(&self, status_code: u16) -> Option<ErrorTemplate> {
160 if let Some(template) = self.templates.get(&status_code) {
161 return Some(template.clone());
162 }
163
164 if let Some(path) = self.template_files.get(&status_code) {
165 if let Ok(content) = std::fs::read_to_string(path) {
166 return Some(ErrorTemplate::new(status_code, content));
167 }
168 }
169
170 Some(ErrorTemplate::default_for(status_code))
172 }
173
174 pub fn has_custom(&self, status_code: u16) -> bool {
176 self.templates.contains_key(&status_code) || self.template_files.contains_key(&status_code)
177 }
178
179 pub fn remove(&mut self, status_code: u16) -> bool {
181 self.templates.remove(&status_code).is_some() || self.template_files.remove(&status_code).is_some()
182 }
183
184 pub fn clear(&mut self) {
186 self.templates.clear();
187 self.template_files.clear();
188 }
189
190 pub fn custom_count(&self) -> usize {
192 self.templates.len() + self.template_files.len()
193 }
194}
195
196impl Default for ErrorTemplates {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use tempfile::TempDir;
206
207 #[test]
208 fn test_error_template_creation() {
209 let template = ErrorTemplate::new(404, "<html>Not Found</html>");
210 assert_eq!(template.status_code, 404);
211 assert!(template.content.contains("Not Found"));
212 assert_eq!(template.content_type, "text/html; charset=utf-8");
213 }
214
215 #[test]
216 fn test_error_template_custom_content_type() {
217 let template = ErrorTemplate::with_content_type(404, "{}", "application/json");
218 assert_eq!(template.content_type, "application/json");
219 }
220
221 #[test]
222 fn test_default_template_404() {
223 let template = ErrorTemplate::default_for(404);
224 assert_eq!(template.status_code, 404);
225 assert!(template.content.contains("404"));
226 assert!(template.content.contains("Not Found"));
227 }
228
229 #[test]
230 fn test_default_template_500() {
231 let template = ErrorTemplate::default_for(500);
232 assert_eq!(template.status_code, 500);
233 assert!(template.content.contains("500"));
234 assert!(template.content.contains("Internal Server Error"));
235 }
236
237 #[test]
238 fn test_default_template_unknown() {
239 let template = ErrorTemplate::default_for(999);
240 assert_eq!(template.status_code, 999);
241 assert!(template.content.contains("999"));
242 }
243
244 #[test]
245 fn test_templates_creation() {
246 let templates = ErrorTemplates::new();
247 assert_eq!(templates.custom_count(), 0);
248 }
249
250 #[test]
251 fn test_templates_set_and_get() {
252 let mut templates = ErrorTemplates::new();
253 let template = ErrorTemplate::new(404, "<html>Custom 404</html>");
254 templates.set_template(404, template);
255
256 assert!(templates.has_custom(404));
257 assert_eq!(templates.custom_count(), 1);
258
259 let retrieved = templates.get(404).unwrap();
260 assert!(retrieved.content.contains("Custom 404"));
261 }
262
263 #[test]
264 fn test_templates_get_default() {
265 let templates = ErrorTemplates::new();
266
267 let template = templates.get(404).unwrap();
269 assert_eq!(template.status_code, 404);
270 assert!(!templates.has_custom(404));
271 }
272
273 #[test]
274 fn test_templates_remove() {
275 let mut templates = ErrorTemplates::new();
276 templates.set_template(404, ErrorTemplate::new(404, "test"));
277
278 assert!(templates.remove(404));
279 assert!(!templates.has_custom(404));
280 assert_eq!(templates.custom_count(), 0);
281 }
282
283 #[test]
284 fn test_templates_remove_nonexistent() {
285 let mut templates = ErrorTemplates::new();
286 assert!(!templates.remove(999));
287 }
288
289 #[test]
290 fn test_templates_clear() {
291 let mut templates = ErrorTemplates::new();
292 templates.set_template(404, ErrorTemplate::new(404, "test"));
293 templates.set_template(500, ErrorTemplate::new(500, "test"));
294
295 templates.clear();
296 assert_eq!(templates.custom_count(), 0);
297 }
298
299 #[test]
300 fn test_templates_file() {
301 let dir = TempDir::new().unwrap();
302 let file_path = dir.path().join("404.html");
303 std::fs::write(&file_path, "<html>Custom File 404</html>").unwrap();
304
305 let mut templates = ErrorTemplates::new();
306 templates.set_template_file(404, &file_path);
307
308 assert!(templates.has_custom(404));
309
310 let template = templates.get(404).unwrap();
311 assert!(template.content.contains("Custom File 404"));
312 }
313
314 #[test]
315 fn test_templates_file_not_found() {
316 let mut templates = ErrorTemplates::new();
317 templates.set_template_file(404, "/nonexistent/404.html");
318
319 let template = templates.get(404).unwrap();
321 assert!(template.content.contains("404"));
322 }
323
324 #[test]
325 fn test_default_error_messages() {
326 let (title, msg) = get_default_error_message(400);
327 assert_eq!(title, "Bad Request");
328
329 let (title, msg) = get_default_error_message(401);
330 assert_eq!(title, "Unauthorized");
331
332 let (title, msg) = get_default_error_message(403);
333 assert_eq!(title, "Forbidden");
334
335 let (title, msg) = get_default_error_message(405);
336 assert_eq!(title, "Method Not Allowed");
337
338 let (title, msg) = get_default_error_message(429);
339 assert_eq!(title, "Too Many Requests");
340
341 let (title, msg) = get_default_error_message(503);
342 assert_eq!(title, "Service Unavailable");
343 }
344
345 #[test]
346 fn test_generate_default_html() {
347 let html = generate_default_html(404, "Not Found", "Test message");
348 assert!(html.contains("<!DOCTYPE html>"));
349 assert!(html.contains("404"));
350 assert!(html.contains("Not Found"));
351 assert!(html.contains("Test message"));
352 assert!(html.contains("Rust Serv"));
353 }
354
355 #[test]
356 fn test_templates_default() {
357 let templates = ErrorTemplates::default();
358 assert_eq!(templates.custom_count(), 0);
359 }
360
361 #[test]
362 fn test_multiple_templates() {
363 let mut templates = ErrorTemplates::new();
364 templates.set_template(400, ErrorTemplate::new(400, "Bad Request"));
365 templates.set_template(404, ErrorTemplate::new(404, "Not Found"));
366 templates.set_template(500, ErrorTemplate::new(500, "Server Error"));
367
368 assert_eq!(templates.custom_count(), 3);
369 assert!(templates.has_custom(400));
370 assert!(templates.has_custom(404));
371 assert!(templates.has_custom(500));
372 }
373
374 #[test]
375 fn test_overwrite_template() {
376 let mut templates = ErrorTemplates::new();
377 templates.set_template(404, ErrorTemplate::new(404, "First"));
378 templates.set_template(404, ErrorTemplate::new(404, "Second"));
379
380 let template = templates.get(404).unwrap();
381 assert!(template.content.contains("Second"));
382 assert_eq!(templates.custom_count(), 1);
383 }
384}