use crate::error::{Error, Result};
use url::Url;
pub const MAX_URLS: usize = 50_000;
pub const MAX_SIZE_BYTES: usize = 52_428_800;
pub const MAX_URL_LENGTH: usize = 2048;
pub const MAX_NEWS_URLS: usize = 1_000;
pub struct Validator;
impl Validator {
pub fn validate_url(url: &str) -> Result<()> {
if url.len() > MAX_URL_LENGTH {
return Err(Error::UrlTooLong(url.to_string()));
}
Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_string()))?;
Ok(())
}
pub fn validate_priority(priority: f32) -> Result<()> {
if !(0.0..=1.0).contains(&priority) {
return Err(Error::InvalidPriority(priority));
}
Ok(())
}
pub fn validate_date(date: &str) -> Result<()> {
if date.len() < 10 {
return Err(Error::InvalidDate(date.to_string()));
}
let parts: Vec<&str> = date.split('-').collect();
if parts.len() < 3 {
return Err(Error::InvalidDate(date.to_string()));
}
if parts[0].len() != 4 || !parts[0].chars().all(|c| c.is_numeric()) {
return Err(Error::InvalidDate(date.to_string()));
}
if parts[1].len() != 2 || !parts[1].chars().all(|c| c.is_numeric()) {
return Err(Error::InvalidDate(date.to_string()));
}
if parts[2].len() < 2 || !parts[2].chars().take(2).all(|c| c.is_numeric()) {
return Err(Error::InvalidDate(date.to_string()));
}
Ok(())
}
pub fn validate_url_count(count: usize) -> Result<()> {
if count > MAX_URLS {
return Err(Error::TooManyUrls(count));
}
Ok(())
}
pub fn validate_size(size: usize) -> Result<()> {
if size > MAX_SIZE_BYTES {
return Err(Error::SizeExceeded(size));
}
Ok(())
}
pub fn validate_video_duration(duration: u32) -> Result<()> {
if duration > 28_800 {
return Err(Error::Validation(format!(
"Video duration exceeds maximum (28800 seconds): {}",
duration
)));
}
Ok(())
}
pub fn validate_video_rating(rating: f32) -> Result<()> {
if !(0.0..=5.0).contains(&rating) {
return Err(Error::Validation(format!(
"Video rating must be between 0.0 and 5.0: {}",
rating
)));
}
Ok(())
}
pub fn validate_video_title(title: &str) -> Result<()> {
if title.len() > 100 {
return Err(Error::Validation(format!(
"Video title exceeds 100 characters: {}",
title.len()
)));
}
Ok(())
}
pub fn validate_video_description(description: &str) -> Result<()> {
if description.len() > 2048 {
return Err(Error::Validation(format!(
"Video description exceeds 2048 characters: {}",
description.len()
)));
}
Ok(())
}
pub fn validate_news_url_count(count: usize) -> Result<()> {
if count > MAX_NEWS_URLS {
return Err(Error::Validation(format!(
"News sitemap exceeds maximum of {} URLs: {}",
MAX_NEWS_URLS, count
)));
}
Ok(())
}
pub fn validate_language_code(code: &str) -> Result<()> {
let len = code.len();
if (len == 2 || len == 3) && code.chars().all(|c| c.is_ascii_lowercase()) {
return Ok(());
}
if code == "zh-cn" || code == "zh-tw" {
return Ok(());
}
Err(Error::Validation(format!(
"Invalid language code (must be ISO 639 2-3 letters or zh-cn/zh-tw): {}",
code
)))
}
pub fn validate_stock_tickers(tickers: &str) -> Result<()> {
let ticker_list: Vec<&str> = tickers.split(',').map(|s| s.trim()).collect();
if ticker_list.len() > 5 {
return Err(Error::Validation(format!(
"Stock tickers exceed maximum of 5: {}",
ticker_list.len()
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_url() {
assert!(Validator::validate_url("https://example.com").is_ok());
assert!(Validator::validate_url("https://example.com/page").is_ok());
assert!(Validator::validate_url("not a url").is_err());
let long_url = format!("https://example.com/{}", "a".repeat(2100));
assert!(Validator::validate_url(&long_url).is_err());
}
#[test]
fn test_validate_priority() {
assert!(Validator::validate_priority(0.0).is_ok());
assert!(Validator::validate_priority(0.5).is_ok());
assert!(Validator::validate_priority(1.0).is_ok());
assert!(Validator::validate_priority(-0.1).is_err());
assert!(Validator::validate_priority(1.1).is_err());
}
#[test]
fn test_validate_date() {
assert!(Validator::validate_date("2025-11-01").is_ok());
assert!(Validator::validate_date("2024-12-31").is_ok());
assert!(Validator::validate_date("2025-11-01T12:00:00Z").is_ok());
assert!(Validator::validate_date("invalid").is_err());
assert!(Validator::validate_date("24-01-01").is_err());
}
#[test]
fn test_validate_url_count() {
assert!(Validator::validate_url_count(1000).is_ok());
assert!(Validator::validate_url_count(50_000).is_ok());
assert!(Validator::validate_url_count(50_001).is_err());
}
#[test]
fn test_validate_video_rating() {
assert!(Validator::validate_video_rating(0.0).is_ok());
assert!(Validator::validate_video_rating(2.5).is_ok());
assert!(Validator::validate_video_rating(5.0).is_ok());
assert!(Validator::validate_video_rating(-0.1).is_err());
assert!(Validator::validate_video_rating(5.1).is_err());
}
#[test]
fn test_validate_news_url_count() {
assert!(Validator::validate_news_url_count(100).is_ok());
assert!(Validator::validate_news_url_count(1_000).is_ok());
assert!(Validator::validate_news_url_count(1_001).is_err());
}
#[test]
fn test_validate_language_code() {
assert!(Validator::validate_language_code("en").is_ok());
assert!(Validator::validate_language_code("vi").is_ok());
assert!(Validator::validate_language_code("fr").is_ok());
assert!(Validator::validate_language_code("eng").is_ok());
assert!(Validator::validate_language_code("vie").is_ok());
assert!(Validator::validate_language_code("zh-cn").is_ok());
assert!(Validator::validate_language_code("zh-tw").is_ok());
assert!(Validator::validate_language_code("EN").is_err()); assert!(Validator::validate_language_code("e").is_err()); assert!(Validator::validate_language_code("engl").is_err()); assert!(Validator::validate_language_code("en-us").is_err()); }
#[test]
fn test_validate_stock_tickers() {
assert!(Validator::validate_stock_tickers("AAPL").is_ok());
assert!(Validator::validate_stock_tickers("AAPL,GOOGL,MSFT").is_ok());
assert!(Validator::validate_stock_tickers("AAPL, GOOGL, MSFT, TSLA, AMZN").is_ok());
assert!(Validator::validate_stock_tickers("AAPL,GOOGL,MSFT,TSLA,AMZN,FB").is_err()); }
}