use regex::Regex;
use crate::core::constants::{VALID_RATINGS, GENRES, SEARCH_FILTERS};
pub fn is_valid_username(username: &str) -> bool {
if username.is_empty() || username.len() > 50 {
return false;
}
let username_regex = Regex::new(r"^[A-Za-z0-9_]*$").unwrap();
username_regex.is_match(username)
}
pub fn is_valid_film_slug(slug: &str) -> bool {
if slug.is_empty() || slug.len() > 200 {
return false;
}
let slug_regex = Regex::new(r"^[a-z0-9\-]*$").unwrap();
slug_regex.is_match(slug)
}
pub fn is_valid_rating(rating: f32) -> bool {
VALID_RATINGS.contains(&rating)
}
pub fn is_valid_genre(genre: &str) -> bool {
GENRES.contains(&genre.to_lowercase().as_str())
}
pub fn is_valid_search_filter(filter: &str) -> bool {
SEARCH_FILTERS.contains(&filter)
}
pub fn is_valid_year(year: i32) -> bool {
year >= 1888 && year <= 2030 }
pub fn is_valid_month(month: u32) -> bool {
month >= 1 && month <= 12
}
pub fn is_valid_day(day: u32) -> bool {
day >= 1 && day <= 31
}
pub fn is_valid_letterboxd_url(url: &str) -> bool {
let url_regex = Regex::new(r"^https?://(www\.)?letterboxd\.com/.+$").unwrap();
url_regex.is_match(url)
}
pub fn is_valid_email(email: &str) -> bool {
let email_regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
email_regex.is_match(email)
}
pub fn is_valid_url(url: &str) -> bool {
url::Url::parse(url).is_ok()
}
pub fn sanitize_for_url(input: &str) -> String {
let normalized = input
.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() {
c
} else if c.is_whitespace() || c == '-' || c == '_' {
'-'
} else {
'-'
}
})
.collect::<String>();
let mut result = String::new();
let mut last_was_dash = false;
for ch in normalized.chars() {
if ch == '-' {
if !last_was_dash {
result.push(ch);
last_was_dash = true;
}
} else {
result.push(ch);
last_was_dash = false;
}
}
result.trim_matches('-').to_string()
}
pub fn clean_and_validate_text(text: &str, max_length: usize) -> Option<String> {
let cleaned = text.trim();
if cleaned.is_empty() || cleaned.len() > max_length {
return None;
}
let normalized = cleaned
.split_whitespace()
.collect::<Vec<&str>>()
.join(" ");
Some(normalized)
}
pub fn is_valid_list_slug(slug: &str) -> bool {
if slug.is_empty() || slug.len() > 100 {
return false;
}
let slug_regex = Regex::new(r"^[a-z0-9\-]*$").unwrap();
slug_regex.is_match(slug)
}
pub fn normalize_rating(rating: f32) -> Option<f32> {
if rating < 0.0 || rating > 5.0 {
return None;
}
let rounded = (rating * 2.0).round() / 2.0;
if is_valid_rating(rounded) {
Some(rounded)
} else {
None
}
}
pub fn is_safe_text(text: &str) -> bool {
let dangerous_patterns = [
"<script", "</script>", "javascript:", "onload=", "onerror=",
"<iframe", "</iframe>", "<embed", "</embed>", "<object", "</object>"
];
let lower_text = text.to_lowercase();
!dangerous_patterns.iter().any(|&pattern| lower_text.contains(pattern))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_username_validation() {
assert!(is_valid_username("testuser"));
assert!(is_valid_username("test_user"));
assert!(is_valid_username("test123"));
assert!(!is_valid_username("test-user")); assert!(!is_valid_username("test user")); assert!(!is_valid_username(""));
}
#[test]
fn test_rating_validation() {
assert!(is_valid_rating(4.5));
assert!(is_valid_rating(5.0));
assert!(is_valid_rating(0.5));
assert!(!is_valid_rating(4.3)); assert!(!is_valid_rating(5.5)); }
#[test]
fn test_year_validation() {
assert!(is_valid_year(2023));
assert!(is_valid_year(1925));
assert!(!is_valid_year(1800)); assert!(!is_valid_year(2050)); }
#[test]
fn test_sanitize_for_url() {
assert_eq!(sanitize_for_url("The Matrix"), "the-matrix");
assert_eq!(sanitize_for_url("Spider-Man: No Way Home"), "spider-man-no-way-home");
assert_eq!(sanitize_for_url("2001: A Space Odyssey"), "2001-a-space-odyssey");
}
}