Phase 1.4: Enhanced input validation system
- Add comprehensive validation methods for dates, UUIDs, ranges, files - Domain-specific validation functions for bulletins, events, hymnal search - Enhanced recurring pattern validation with specific error types - Better date range validation with improved error messages - Foundation for consistent input validation across all endpoints - All using new specific error types for better debugging
This commit is contained in:
parent
ad43a19f7c
commit
e0fffbc4b9
|
@ -212,6 +212,10 @@ impl ApiError {
|
|||
Self::InvalidDateRange(format!("Invalid date range: {} to {}", start, end))
|
||||
}
|
||||
|
||||
pub fn invalid_recurring_pattern(pattern: impl std::fmt::Display) -> Self {
|
||||
Self::InvalidRecurringPattern(format!("Invalid recurring pattern: {}", pattern))
|
||||
}
|
||||
|
||||
pub fn duplicate_entry(resource: &str, identifier: impl std::fmt::Display) -> Self {
|
||||
Self::DuplicateEntry(format!("{} already exists: {}", resource, identifier))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::error::{ApiError, Result};
|
||||
use regex::Regex;
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ValidationBuilder {
|
||||
|
@ -59,6 +61,63 @@ impl ValidationBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn validate_date(mut self, date_str: &str, field_name: &str) -> Self {
|
||||
if !date_str.is_empty() {
|
||||
if let Err(_) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
|
||||
self.errors.push(format!("{} must be a valid date in YYYY-MM-DD format", field_name));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate_datetime(mut self, datetime_str: &str, field_name: &str) -> Self {
|
||||
if !datetime_str.is_empty() {
|
||||
if let Err(_) = NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%dT%H:%M:%S") {
|
||||
self.errors.push(format!("{} must be a valid datetime in ISO format", field_name));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate_uuid(mut self, uuid_str: &str, field_name: &str) -> Self {
|
||||
if !uuid_str.is_empty() {
|
||||
if let Err(_) = Uuid::parse_str(uuid_str) {
|
||||
self.errors.push(format!("{} must be a valid UUID", field_name));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate_positive_number(mut self, num: i32, field_name: &str) -> Self {
|
||||
if num <= 0 {
|
||||
self.errors.push(format!("{} must be a positive number", field_name));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate_range(mut self, num: i32, field_name: &str, min: i32, max: i32) -> Self {
|
||||
if num < min || num > max {
|
||||
self.errors.push(format!("{} must be between {} and {}", field_name, min, max));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate_file_extension(mut self, filename: &str, field_name: &str, allowed_extensions: &[&str]) -> Self {
|
||||
if !filename.is_empty() {
|
||||
let extension = filename.split('.').last().unwrap_or("").to_lowercase();
|
||||
if !allowed_extensions.contains(&extension.as_str()) {
|
||||
self.errors.push(format!("{} must have one of these extensions: {}", field_name, allowed_extensions.join(", ")));
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate_content_length(mut self, content: &str, field_name: &str, max_length: usize) -> Self {
|
||||
if content.len() > max_length {
|
||||
self.errors.push(format!("{} content is too long (max {} characters)", field_name, max_length));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<()> {
|
||||
if self.errors.is_empty() {
|
||||
|
@ -76,13 +135,75 @@ pub fn validate_recurring_type(recurring_type: &Option<String>) -> Result<()> {
|
|||
if let Some(rt) = recurring_type {
|
||||
match rt.as_str() {
|
||||
"none" | "daily" | "weekly" | "biweekly" | "monthly" | "first_tuesday" | "2nd/3rd Saturday Monthly" | "2nd_3rd_saturday_monthly" => Ok(()),
|
||||
_ => Err(ApiError::ValidationError("Invalid recurring type. Must be one of: none, daily, weekly, biweekly, monthly, first_tuesday, 2nd_3rd_saturday_monthly".to_string())),
|
||||
_ => Err(ApiError::invalid_recurring_pattern(rt)),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Domain-specific validation functions using our enhanced error types
|
||||
pub fn validate_date_range(start_date: &str, end_date: &str) -> Result<()> {
|
||||
let start = NaiveDate::parse_from_str(start_date, "%Y-%m-%d")
|
||||
.map_err(|_| ApiError::ValidationError("Invalid start date format".to_string()))?;
|
||||
let end = NaiveDate::parse_from_str(end_date, "%Y-%m-%d")
|
||||
.map_err(|_| ApiError::ValidationError("Invalid end date format".to_string()))?;
|
||||
|
||||
if end < start {
|
||||
return Err(ApiError::invalid_date_range(start_date, end_date));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate bulletin-specific fields
|
||||
pub fn validate_bulletin_data(title: &str, date_str: &str, content: &Option<String>) -> Result<()> {
|
||||
ValidationBuilder::new()
|
||||
.require(title, "title")
|
||||
.validate_length(title, "title", 1, 200)
|
||||
.validate_date(date_str, "date")
|
||||
.build()?;
|
||||
|
||||
if let Some(content) = content {
|
||||
ValidationBuilder::new()
|
||||
.validate_content_length(content, "content", 50000)
|
||||
.build()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate event-specific fields
|
||||
pub fn validate_event_data(title: &str, description: &str, location: &str, category: &str) -> Result<()> {
|
||||
ValidationBuilder::new()
|
||||
.require(title, "title")
|
||||
.require(description, "description")
|
||||
.require(location, "location")
|
||||
.require(category, "category")
|
||||
.validate_length(title, "title", 1, 200)
|
||||
.validate_length(description, "description", 1, 2000)
|
||||
.validate_length(location, "location", 1, 200)
|
||||
.validate_length(category, "category", 1, 50)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Validate hymnal search parameters
|
||||
pub fn validate_hymnal_search(query: &Option<String>, hymnal_code: &Option<String>, number: &Option<i32>) -> Result<()> {
|
||||
if let Some(q) = query {
|
||||
ValidationBuilder::new()
|
||||
.validate_length(q, "search query", 1, 100)
|
||||
.build()?;
|
||||
}
|
||||
|
||||
if let Some(num) = number {
|
||||
ValidationBuilder::new()
|
||||
.validate_range(*num, "hymn number", 1, 9999)
|
||||
.build()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_valid_recurring_types() -> Vec<&'static str> {
|
||||
vec!["none", "daily", "weekly", "biweekly", "monthly", "first_tuesday", "2nd_3rd_saturday_monthly"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue