Skip to main content

rust_serv/error_pages/
templates.rs

1//! Error page templates
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6/// Error page template
7#[derive(Debug, Clone)]
8pub struct ErrorTemplate {
9    /// HTTP status code
10    pub status_code: u16,
11    /// HTML content
12    pub content: String,
13    /// Content-Type header
14    pub content_type: String,
15}
16
17impl ErrorTemplate {
18    /// Create a new error template
19    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    /// Create with custom content type
28    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    /// Create default template for a status code
37    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
44/// Get default error message for status code
45fn 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
65/// Generate default HTML error page
66fn 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/// Error templates manager
131#[derive(Debug, Clone)]
132pub struct ErrorTemplates {
133    /// Custom templates by status code
134    templates: HashMap<u16, ErrorTemplate>,
135    /// Custom template files by status code
136    template_files: HashMap<u16, PathBuf>,
137}
138
139impl ErrorTemplates {
140    /// Create a new error templates manager
141    pub fn new() -> Self {
142        Self {
143            templates: HashMap::new(),
144            template_files: HashMap::new(),
145        }
146    }
147
148    /// Set custom template for a status code
149    pub fn set_template(&mut self, status_code: u16, template: ErrorTemplate) {
150        self.templates.insert(status_code, template);
151    }
152
153    /// Set custom template file for a status code
154    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    /// Get template for a status code
159    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        // Return default template
171        Some(ErrorTemplate::default_for(status_code))
172    }
173
174    /// Check if custom template exists
175    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    /// Remove custom template
180    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    /// Clear all custom templates
185    pub fn clear(&mut self) {
186        self.templates.clear();
187        self.template_files.clear();
188    }
189
190    /// Get count of custom templates
191    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        // Should return default template even if not set
268        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        // Should fall back to default
320        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}