1use crate::result::MultipartResult;
2use crate::{FileInput, MultipartError};
3use std::collections::HashMap;
4use std::fmt::{Display, Formatter};
5
6#[derive(Debug, Clone)]
7pub struct InputError {
8 pub name: String,
9 pub error: ErrorMessage,
10}
11
12#[derive(Debug, Clone, Eq, PartialEq)]
13pub enum ErrorMessage {
14 NoFiles(String),
15 FileTooSmall(String, String, usize),
16 FileTooLarge(String, String, usize),
17 TooFewFiles(String, usize),
18 TooManyFiles(String, usize),
19 InvalidFileExtension(String, String, Option<String>),
20 InvalidContentType(String, String, String),
21 MissingFileExtension(String, String, String),
22}
23
24#[derive(Debug, Clone, Default)]
25pub struct Validator {
26 rules: HashMap<String, FileRules>,
27}
28
29#[derive(Debug, Default, Clone)]
31pub struct FileRules {
32 pub required: bool,
34
35 pub extension_required: bool,
37
38 pub min_size: Option<usize>,
40
41 pub max_size: Option<usize>,
43
44 pub allowed_extensions: Option<Vec<String>>,
46
47 pub allowed_content_types: Option<Vec<String>>,
49
50 pub min_files: Option<usize>,
52
53 pub max_files: Option<usize>,
55}
56
57impl Validator {
58 pub fn new() -> Self {
59 Default::default()
60 }
61
62 pub fn add_rule(&mut self, field: &str, rules: FileRules) -> Self {
63 let mut validator = self.clone();
64 validator.rules.insert(field.to_string(), rules);
65 validator
66 }
67
68 pub fn validate(&self, files: &HashMap<String, Vec<FileInput>>) -> MultipartResult<()> {
69 for (field_name, rules) in &self.rules {
70 let files = files.get(field_name);
71 Self::validate_files(field_name.clone(), files, rules)
72 .map_err(MultipartError::ValidationError)?;
73 }
74
75 Ok(())
76 }
77
78 fn validate_files(
79 field_name: String,
80 files: Option<&Vec<FileInput>>,
81 rules: &FileRules,
82 ) -> Result<(), InputError> {
83 if files.is_none() {
84 if rules.required {
85 return Err(InputError {
86 name: field_name.clone(),
87 error: ErrorMessage::NoFiles(field_name),
88 });
89 }
90
91 return Ok(());
92 }
93
94 let files = files.unwrap();
95 let file_count = files.len();
96
97 if rules.required && file_count == 0 {
99 return Err(InputError {
100 name: field_name.clone(),
101 error: ErrorMessage::NoFiles(field_name),
102 });
103 }
104
105 if file_count < rules.min_files.unwrap_or(0) {
106 return Err(InputError {
107 name: field_name.clone(),
108 error: ErrorMessage::TooFewFiles(field_name, file_count),
109 });
110 }
111
112 if file_count > rules.max_files.unwrap_or(usize::MAX) {
113 return Err(InputError {
114 name: field_name.clone(),
115 error: ErrorMessage::TooManyFiles(field_name, file_count),
116 });
117 }
118
119 for file in files {
120 Self::validate_file(&field_name, rules.clone(), file)?;
121 }
122
123 Ok(())
125 }
126
127 fn validate_file(
128 field_name: &str,
129 rule: FileRules,
130 file: &FileInput,
131 ) -> Result<(), InputError> {
132 if rule.extension_required && file.extension.is_none() {
134 return Err(InputError {
135 name: field_name.to_string(),
136 error: ErrorMessage::MissingFileExtension(
137 field_name.to_string(),
138 file.file_name.clone(),
139 "Extension is required".to_string(),
140 ),
141 });
142 }
143
144 if let Some(min_size) = rule.min_size
146 && file.size < min_size
147 {
148 return Err(InputError {
149 name: field_name.to_string(),
150 error: ErrorMessage::FileTooSmall(
151 field_name.to_string(),
152 file.file_name.clone(),
153 min_size,
154 ),
155 });
156 }
157
158 if let Some(max_size) = rule.max_size
159 && file.size > max_size
160 {
161 return Err(InputError {
162 name: field_name.to_string(),
163 error: ErrorMessage::FileTooLarge(
164 field_name.to_string(),
165 file.file_name.clone(),
166 max_size,
167 ),
168 });
169 }
170
171 if let Some(allowed_extensions) = &rule.allowed_extensions {
173 if let Some(extension) = &file.extension {
174 if !allowed_extensions.contains(&extension.to_lowercase()) {
175 return Err(InputError {
176 name: field_name.to_string(),
177 error: ErrorMessage::InvalidFileExtension(
178 field_name.to_string(),
179 file.file_name.clone(),
180 file.extension.clone(),
181 ),
182 });
183 }
184 } else {
185 return Err(InputError {
186 name: field_name.to_string(),
187 error: ErrorMessage::MissingFileExtension(
188 field_name.to_string(),
189 file.file_name.clone(),
190 "File extension is missing but required".to_string(),
191 ),
192 });
193 }
194 }
195
196 if let Some(allowed_content_types) = &rule.allowed_content_types
198 && !allowed_content_types.contains(&file.content_type.to_lowercase())
199 {
200 return Err(InputError {
201 name: field_name.to_string(),
202 error: ErrorMessage::InvalidContentType(
203 field_name.to_string(),
204 file.file_name.clone(),
205 format!(
206 "Invalid content type. Allowed content types are: {allowed_content_types:?}"
207 ),
208 ),
209 });
210 }
211
212 Ok(())
213 }
214}
215
216impl Display for ErrorMessage {
217 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218 match self {
219 ErrorMessage::NoFiles(field_name) => {
220 let display_name = field_name.replace("_", " ");
221 write!(f, "No files were uploaded for field: '{display_name}'")
222 }
223 ErrorMessage::FileTooSmall(field_name, file_name, size) => {
224 let display_name = field_name.replace("_", " ");
225 write!(
226 f,
227 "File '{}' is too small for field '{}'. Minimum size is {}",
228 file_name,
229 display_name,
230 FileInput::format_size(*size)
231 )
232 }
233 ErrorMessage::FileTooLarge(field_name, file_name, size) => {
234 let display_name = field_name.replace("_", " ");
235 write!(
236 f,
237 "File '{}' is too large for field '{}'. Maximum size is {}",
238 file_name,
239 display_name,
240 FileInput::format_size(*size)
241 )
242 }
243 ErrorMessage::TooFewFiles(field_name, count) => {
244 let display_name = field_name.replace("_", " ");
245 write!(
246 f,
247 "Too few files uploaded for field '{}'. Current count: {}, minimum required",
248 display_name, count
249 )
250 }
251 ErrorMessage::TooManyFiles(field_name, count) => {
252 let display_name = field_name.replace("_", " ");
253 write!(
254 f,
255 "Too many files uploaded for field '{}'. Current count: {}, maximum allowed",
256 display_name, count
257 )
258 }
259 ErrorMessage::InvalidFileExtension(field_name, file_name, ext) => {
260 let display_name = field_name.replace("_", " ");
261 match ext {
262 Some(extension) => write!(
263 f,
264 "Invalid file extension '.{}' for file '{}' in field '{}'",
265 extension, file_name, display_name
266 ),
267 None => write!(
268 f,
269 "Missing file extension for file '{}' in field '{}'",
270 file_name, display_name
271 ),
272 }
273 }
274 ErrorMessage::InvalidContentType(field_name, file_name, message) => {
275 let display_name = field_name.replace("_", " ");
276 write!(
277 f,
278 "Invalid content type for file '{}' in field '{}': {}",
279 file_name, display_name, message
280 )
281 }
282 ErrorMessage::MissingFileExtension(field_name, file_name, message) => {
283 let display_name = field_name.replace("_", " ");
284 write!(
285 f,
286 "Missing file extension for file '{}' in field '{}': {}",
287 file_name, display_name, message
288 )
289 }
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::MultipartError;
298
299 fn create_file_input(
301 field_name: &str,
302 file_name: &str,
303 size: usize,
304 extension: Option<&str>,
305 content_type: &str,
306 ) -> FileInput {
307 FileInput {
308 field_name: field_name.to_string(),
309 file_name: file_name.to_string(),
310 size,
311 extension: extension.map(|e| e.to_string()),
312 content_type: content_type.to_string(),
313 ..Default::default()
314 }
315 }
316
317 #[test]
318 fn test_validate_required_files_missing() {
319 let validator = Validator::new().add_rule(
320 "file_field",
321 FileRules {
322 required: true,
323 ..Default::default()
324 },
325 );
326
327 let mut files = HashMap::new();
328 files.insert("file_field".to_string(), vec![]);
329
330 let result = validator.validate(&files);
331
332 assert!(result.is_err());
333 if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
334 assert_eq!(error, ErrorMessage::NoFiles("file_field".to_string()));
335 }
336 }
337
338 #[test]
339 fn test_validate_required_files_present() {
340 let validator = Validator::new().add_rule(
341 "file_field",
342 FileRules {
343 required: true,
344 ..Default::default()
345 },
346 );
347
348 let mut files = HashMap::new();
349 let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
350 files.insert("file_field".to_string(), vec![file]);
351
352 let result = validator.validate(&files);
353
354 assert!(result.is_ok());
355 }
356
357 #[test]
358 fn test_validate_file_size_too_small() {
359 let validator = Validator::new().add_rule(
360 "file_field",
361 FileRules {
362 min_size: Some(1024),
363 ..Default::default()
364 },
365 );
366
367 let mut files = HashMap::new();
368 let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
369 files.insert("file_field".to_string(), vec![file]);
370
371 let result = validator.validate(&files);
372
373 assert!(result.is_err());
374 if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
375 assert_eq!(
376 error,
377 ErrorMessage::FileTooSmall("file_field".to_string(), "test.jpg".to_string(), 1024)
378 );
379 }
380 }
381
382 #[test]
383 fn test_validate_file_size_ok() {
384 let validator = Validator::new().add_rule(
385 "file_field",
386 FileRules {
387 min_size: Some(100),
388 max_size: Some(1024),
389 ..Default::default()
390 },
391 );
392
393 let mut files = HashMap::new();
394 let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
395 files.insert("file_field".to_string(), vec![file]);
396
397 let result = validator.validate(&files);
398
399 assert!(result.is_ok());
400 }
401
402 #[test]
403 fn test_validate_file_extension_invalid() {
404 let validator = Validator::new().add_rule(
405 "file_field",
406 FileRules {
407 allowed_extensions: Some(vec!["jpg".to_string(), "png".to_string()]),
408 ..Default::default()
409 },
410 );
411
412 let mut files = HashMap::new();
413 let file = create_file_input("file_field", "test.txt", 500, Some("txt"), "image/jpeg");
414 files.insert("file_field".to_string(), vec![file]);
415
416 let result = validator.validate(&files);
417
418 assert!(result.is_err());
419 if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
420 assert_eq!(
421 error,
422 ErrorMessage::InvalidFileExtension(
423 "file_field".to_string(),
424 "test.txt".to_string(),
425 Some("txt".to_string())
426 )
427 );
428 }
429 }
430
431 #[test]
432 fn test_validate_file_extension_valid() {
433 let validator = Validator::new().add_rule(
434 "file_field",
435 FileRules {
436 allowed_extensions: Some(vec!["jpg".to_string(), "png".to_string()]),
437 ..Default::default()
438 },
439 );
440
441 let mut files = HashMap::new();
442 let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
443 files.insert("file_field".to_string(), vec![file]);
444
445 let result = validator.validate(&files);
446
447 assert!(result.is_ok());
448 }
449
450 #[test]
451 fn test_validate_content_type_invalid() {
452 let validator = Validator::new().add_rule(
453 "file_field",
454 FileRules {
455 allowed_content_types: Some(vec![
456 "image/jpeg".to_string(),
457 "image/png".to_string(),
458 ]),
459 ..Default::default()
460 },
461 );
462
463 let mut files = HashMap::new();
464 let file = create_file_input(
465 "file_field",
466 "test.jpg",
467 500,
468 Some("jpg"),
469 "application/pdf",
470 );
471 files.insert("file_field".to_string(), vec![file]);
472
473 let result = validator.validate(&files);
474
475 assert!(result.is_err());
476 if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
477 assert_eq!(
478 error,
479 ErrorMessage::InvalidContentType(
480 "file_field".to_string(),
481 "test.jpg".to_string(),
482 "Invalid content type. Allowed content types are: [\"image/jpeg\", \"image/png\"]".to_string()
483 )
484 );
485 }
486 }
487
488 #[test]
489 fn test_validate_file_count_too_few() {
490 let validator = Validator::new().add_rule(
491 "file_field",
492 FileRules {
493 min_files: Some(2),
494 ..Default::default()
495 },
496 );
497
498 let mut files = HashMap::new();
499 let file = create_file_input("file_field", "test.jpg", 500, Some("jpg"), "image/jpeg");
500 files.insert("file_field".to_string(), vec![file]);
501
502 let result = validator.validate(&files);
503
504 assert!(result.is_err());
505 if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
506 assert_eq!(
507 error,
508 ErrorMessage::TooFewFiles("file_field".to_string(), 1)
509 );
510 }
511 }
512
513 #[test]
514 fn test_validate_file_count_too_many() {
515 let validator = Validator::new().add_rule(
516 "file_field",
517 FileRules {
518 max_files: Some(1),
519 ..Default::default()
520 },
521 );
522
523 let mut files = HashMap::new();
524 let file1 = create_file_input("file_field", "test1.jpg", 500, Some("jpg"), "image/jpeg");
525 let file2 = create_file_input("file_field", "test2.jpg", 500, Some("jpg"), "image/jpeg");
526 files.insert("file_field".to_string(), vec![file1, file2]);
527
528 let result = validator.validate(&files);
529
530 assert!(result.is_err());
531 if let Err(MultipartError::ValidationError(InputError { error, .. })) = result {
532 assert_eq!(
533 error,
534 ErrorMessage::TooManyFiles("file_field".to_string(), 2)
535 );
536 }
537 }
538
539 #[test]
540 fn test_validate_file_count_ok() {
541 let validator = Validator::new().add_rule(
542 "file_field",
543 FileRules {
544 max_files: Some(2),
545 min_files: Some(1),
546 ..Default::default()
547 },
548 );
549
550 let mut files = HashMap::new();
551 let file1 = create_file_input("file_field", "test1.jpg", 500, Some("jpg"), "image/jpeg");
552 let file2 = create_file_input("file_field", "test2.jpg", 500, Some("jpg"), "image/jpeg");
553 files.insert("file_field".to_string(), vec![file1, file2]);
554
555 let result = validator.validate(&files);
556
557 assert!(result.is_ok());
558 }
559}