use crate::error::ValidationErrors;
pub trait Validate {
fn validate(&self) -> Result<(), Box<ValidationErrors>>;
}
#[must_use]
pub fn is_valid_email(value: &str) -> bool {
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 {
return false;
}
let local = parts[0];
let domain = parts[1];
if local.is_empty() || local.starts_with('.') || local.ends_with('.') {
return false;
}
if domain.is_empty() || !domain.contains('.') {
return false;
}
if domain.starts_with('.') || domain.ends_with('.') || domain.contains("..") {
return false;
}
for c in local.chars() {
if !c.is_alphanumeric() && !".!#$%&'*+/=?^_`{|}~-".contains(c) {
return false;
}
}
for c in domain.chars() {
if !c.is_alphanumeric() && c != '.' && c != '-' {
return false;
}
}
for part in domain.split('.') {
if part.is_empty() || part.starts_with('-') || part.ends_with('-') {
return false;
}
}
true
}
#[must_use]
pub fn is_valid_url(value: &str) -> bool {
let rest = if let Some(rest) = value.strip_prefix("https://") {
rest
} else if let Some(rest) = value.strip_prefix("http://") {
rest
} else {
return false;
};
if rest.is_empty() {
return false;
}
let host = rest
.split('/')
.next()
.unwrap_or("")
.split('?')
.next()
.unwrap_or("")
.split('#')
.next()
.unwrap_or("");
let host = host.split(':').next().unwrap_or("");
if host.is_empty() {
return false;
}
for c in host.chars() {
if !c.is_alphanumeric() && c != '.' && c != '-' {
return false;
}
}
if host != "localhost" && !host.contains('.') {
return false;
}
true
}
#[must_use]
pub fn matches_pattern(value: &str, pattern: &str) -> bool {
if pattern.is_empty() {
return true;
}
if pattern.starts_with('^') && pattern.ends_with('$') {
let inner = &pattern[1..pattern.len() - 1];
if !inner.contains(['[', ']', '*', '+', '?', '\\', '(', ')', '|', '.']) {
return value == inner;
}
}
let Ok(compiled) = SimpleRegex::compile(pattern) else {
return false;
};
compiled.is_match(value)
}
#[must_use]
pub fn is_valid_phone(value: &str) -> bool {
let s = value.trim();
if s.is_empty() {
return false;
}
if let Some(pos) = s.find('+') {
if pos != 0 {
return false;
}
if s[1..].contains('+') {
return false;
}
}
let first = s.chars().next().unwrap();
if first != '+' && matches!(first, '-' | '.' | ' ') {
return false;
}
let last = s.chars().last().unwrap();
if matches!(last, '-' | '.' | ' ') {
return false;
}
let mut digits = 0usize;
let mut open_parens = 0usize;
let mut last_sep: Option<char> = None; let mut paren_digit_count: usize = 0;
for (i, c) in s.chars().enumerate() {
match c {
'0'..='9' => {
digits += 1;
if open_parens > 0 {
paren_digit_count += 1;
}
last_sep = None;
}
'+' => {
if i != 0 {
return false;
}
last_sep = None;
}
' ' => {
last_sep = None;
}
'-' | '.' => {
if let Some(prev) = last_sep {
if matches!(prev, '-' | '.') {
return false;
}
}
last_sep = Some(c);
}
'(' => {
open_parens += 1;
paren_digit_count = 0;
last_sep = None;
}
')' => {
if open_parens == 0 {
return false;
}
if paren_digit_count == 0 {
return false;
}
open_parens -= 1;
last_sep = None;
}
_ => return false, }
}
if open_parens != 0 {
return false;
}
digits >= 10
}
#[derive(Debug, Clone)]
struct SimpleRegex {
anchored_start: bool,
anchored_end: bool,
tokens: Vec<Token>,
}
#[derive(Debug, Clone)]
struct Token {
atom: Atom,
min: usize,
max: Option<usize>, }
#[derive(Debug, Clone)]
enum Atom {
Any,
Literal(char),
Digit,
CharClass(CharClass),
}
#[derive(Debug, Clone)]
struct CharClass {
parts: Vec<CharClassPart>,
}
#[derive(Debug, Clone)]
enum CharClassPart {
Single(char),
Range(char, char),
}
impl CharClass {
fn matches(&self, c: char) -> bool {
for part in &self.parts {
match *part {
CharClassPart::Single(x) if c == x => return true,
CharClassPart::Range(a, b) if a <= c && c <= b => return true,
_ => {}
}
}
false
}
}
impl Atom {
fn matches(&self, c: char) -> bool {
match self {
Atom::Any => true,
Atom::Literal(x) => *x == c,
Atom::Digit => c.is_ascii_digit(),
Atom::CharClass(cc) => cc.matches(c),
}
}
}
impl SimpleRegex {
fn compile(pattern: &str) -> Result<Self, ()> {
let mut chars: Vec<char> = pattern.chars().collect();
let mut anchored_start = false;
let mut anchored_end = false;
if chars.first() == Some(&'^') {
anchored_start = true;
chars.remove(0);
}
if chars.last() == Some(&'$') {
anchored_end = true;
chars.pop();
}
let mut i = 0usize;
let mut tokens = Vec::<Token>::new();
while i < chars.len() {
let atom = match chars[i] {
'.' => {
i += 1;
Atom::Any
}
'\\' => {
i += 1;
if i >= chars.len() {
return Err(());
}
let esc = chars[i];
i += 1;
match esc {
'd' => Atom::Digit,
other => Atom::Literal(other),
}
}
'[' => {
i += 1;
let (cc, next) = parse_char_class(&chars, i)?;
i = next;
Atom::CharClass(cc)
}
c => {
i += 1;
Atom::Literal(c)
}
};
let mut min = 1usize;
let mut max: Option<usize> = Some(1);
if i < chars.len() {
match chars[i] {
'+' => {
min = 1;
max = None;
i += 1;
}
'*' => {
min = 0;
max = None;
i += 1;
}
'?' => {
min = 0;
max = Some(1);
i += 1;
}
'{' => {
i += 1;
let (n, next) = parse_braced_number(&chars, i)?;
i = next;
min = n;
max = Some(n);
}
_ => {}
}
}
tokens.push(Token { atom, min, max });
}
Ok(Self {
anchored_start,
anchored_end,
tokens,
})
}
fn is_match(&self, value: &str) -> bool {
let s: Vec<char> = value.chars().collect();
if self.anchored_start {
return self.is_match_at(&s, 0) && (!self.anchored_end || self.matches_end(&s));
}
for start in 0..=s.len() {
if self.is_match_at(&s, start) && (!self.anchored_end || self.matches_end(&s)) {
return true;
}
}
false
}
fn matches_end(&self, s: &[char]) -> bool {
let _ = s;
true
}
fn is_match_at(&self, s: &[char], start: usize) -> bool {
use std::collections::HashMap;
fn dp(
tokens: &[Token],
s: &[char],
ti: usize,
si: usize,
memo: &mut HashMap<(usize, usize), bool>,
) -> bool {
if let Some(&v) = memo.get(&(ti, si)) {
return v;
}
let ans = if ti == tokens.len() {
si == s.len()
} else {
let t = &tokens[ti];
let remaining = s.len().saturating_sub(si);
let max_rep = t.max.unwrap_or(remaining).min(remaining);
let mut ok = false;
for rep in t.min..=max_rep {
let mut good = true;
for k in 0..rep {
if !t.atom.matches(s[si + k]) {
good = false;
break;
}
}
if good && dp(tokens, s, ti + 1, si + rep, memo) {
ok = true;
break;
}
}
ok
};
memo.insert((ti, si), ans);
ans
}
if self.anchored_end {
let mut memo = HashMap::new();
dp(&self.tokens, s, 0, start, &mut memo)
} else {
for end in start..=s.len() {
let slice = &s[..end];
let mut memo = HashMap::new();
if dp(&self.tokens, slice, 0, start, &mut memo) {
return true;
}
}
false
}
}
}
fn parse_char_class(chars: &[char], mut i: usize) -> Result<(CharClass, usize), ()> {
let mut parts = Vec::<CharClassPart>::new();
if i >= chars.len() {
return Err(());
}
while i < chars.len() {
if chars[i] == ']' {
return Ok((CharClass { parts }, i + 1));
}
let first = chars[i];
i += 1;
if i + 1 < chars.len() && chars[i] == '-' && chars[i + 1] != ']' {
let second = chars[i + 1];
i += 2;
parts.push(CharClassPart::Range(first, second));
} else {
parts.push(CharClassPart::Single(first));
}
}
Err(())
}
fn parse_braced_number(chars: &[char], mut i: usize) -> Result<(usize, usize), ()> {
let mut n: usize = 0;
let mut saw_digit = false;
while i < chars.len() {
let c = chars[i];
if c == '}' {
if !saw_digit {
return Err(());
}
return Ok((n, i + 1));
}
if let Some(d) = c.to_digit(10) {
saw_digit = true;
n = n
.checked_mul(10)
.and_then(|x| x.checked_add(d as usize))
.ok_or(())?;
i += 1;
} else {
return Err(());
}
}
Err(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_emails() {
assert!(is_valid_email("user@example.com"));
assert!(is_valid_email("user.name@example.com"));
assert!(is_valid_email("user+tag@example.com"));
assert!(is_valid_email("user@sub.domain.org"));
}
#[test]
fn test_invalid_emails() {
assert!(!is_valid_email(""));
assert!(!is_valid_email("invalid"));
assert!(!is_valid_email("@domain.com"));
assert!(!is_valid_email("user@"));
assert!(!is_valid_email("user@@domain.com"));
assert!(!is_valid_email(".user@domain.com"));
assert!(!is_valid_email("user.@domain.com"));
assert!(!is_valid_email("user@.domain.com"));
assert!(!is_valid_email("user@domain.com."));
assert!(!is_valid_email("user@domain..com"));
}
#[test]
fn test_valid_urls() {
assert!(is_valid_url("https://example.com"));
assert!(is_valid_url("http://example.com"));
assert!(is_valid_url("https://sub.domain.org"));
assert!(is_valid_url("https://example.com/path"));
assert!(is_valid_url("https://example.com/path?query=value"));
assert!(is_valid_url("https://example.com:8080"));
assert!(is_valid_url("http://localhost"));
}
#[test]
fn test_invalid_urls() {
assert!(!is_valid_url(""));
assert!(!is_valid_url("not-a-url"));
assert!(!is_valid_url("ftp://example.com"));
assert!(!is_valid_url("https://"));
assert!(!is_valid_url("http://"));
}
#[test]
fn test_simple_patterns() {
assert!(matches_pattern("hello", "^hello$"));
assert!(matches_pattern("test", "^test$"));
assert!(!matches_pattern("hello", "^world$"));
assert!(matches_pattern("abc", "abc"));
}
}