use std::fmt;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone)]
pub struct CompiledPattern {
source: String,
regex: Regex,
}
impl CompiledPattern {
pub fn new(pattern: impl Into<String>) -> Result<Self, regex::Error> {
let source = pattern.into();
let regex = Regex::new(&source)?;
Ok(Self { source, regex })
}
#[must_use]
pub const fn regex(&self) -> &Regex {
&self.regex
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.source
}
#[must_use]
pub fn is_match(&self, value: &str) -> bool {
self.regex.is_match(value)
}
}
impl fmt::Display for CompiledPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.source)
}
}
impl PartialEq for CompiledPattern {
fn eq(&self, other: &Self) -> bool {
self.source == other.source
}
}
impl Eq for CompiledPattern {}
impl Serialize for CompiledPattern {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.source)
}
}
impl<'de> Deserialize<'de> for CompiledPattern {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let source = String::deserialize(deserializer)?;
Self::new(source).map_err(D::Error::custom)
}
}
impl TryFrom<String> for CompiledPattern {
type Error = regex::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<&str> for CompiledPattern {
type Error = regex::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
#[non_exhaustive]
pub enum ValidationRule {
#[serde(rename = "required")]
Required,
#[serde(rename = "pattern")]
Pattern {
pattern: CompiledPattern,
message: Option<String>,
},
#[serde(rename = "length")]
Length {
min: Option<usize>,
max: Option<usize>,
},
#[serde(rename = "range")]
Range {
min: Option<i64>,
max: Option<i64>,
},
#[serde(rename = "enum")]
Enum {
values: Vec<String>,
},
#[serde(rename = "checksum")]
Checksum {
algorithm: String,
},
#[serde(rename = "cross_field")]
CrossField {
field: String,
operator: String,
},
#[serde(rename = "conditional")]
Conditional {
condition: String,
then_rules: Vec<Box<ValidationRule>>,
},
#[serde(rename = "all")]
All(Vec<ValidationRule>),
#[serde(rename = "any")]
Any(Vec<ValidationRule>),
#[serde(rename = "one_of")]
OneOf {
fields: Vec<String>,
},
#[serde(rename = "any_of")]
AnyOf {
fields: Vec<String>,
},
#[serde(rename = "conditional_required")]
ConditionalRequired {
if_field_present: String,
then_required: Vec<String>,
},
#[serde(rename = "required_if_absent")]
RequiredIfAbsent {
absent_field: String,
then_required: Vec<String>,
},
#[serde(rename = "email")]
Email,
#[serde(rename = "phone")]
Phone,
}
impl ValidationRule {
#[must_use]
pub const fn is_required(&self) -> bool {
matches!(self, Self::Required)
}
#[must_use]
pub fn description(&self) -> String {
match self {
Self::Required => "Field is required".to_string(),
Self::Pattern { message, .. } => {
message.clone().unwrap_or_else(|| "Must match pattern".to_string())
},
Self::Length { min, max } => match (min, max) {
(Some(m), Some(max_val)) => format!("Length between {} and {}", m, max_val),
(Some(m), None) => format!("Length at least {}", m),
(None, Some(max_val)) => format!("Length at most {}", max_val),
(None, None) => "Length constraint".to_string(),
},
Self::Range { min, max } => match (min, max) {
(Some(m), Some(max_val)) => format!("Value between {} and {}", m, max_val),
(Some(m), None) => format!("Value at least {}", m),
(None, Some(max_val)) => format!("Value at most {}", max_val),
(None, None) => "Range constraint".to_string(),
},
Self::Enum { values } => format!("Must be one of: {}", values.join(", ")),
Self::Checksum { algorithm } => format!("Invalid {}", algorithm),
Self::CrossField { field, operator } => format!("Must be {} {}", operator, field),
Self::Conditional { .. } => "Conditional validation".to_string(),
Self::All(_) => "All rules must pass".to_string(),
Self::Any(_) => "At least one rule must pass".to_string(),
Self::OneOf { fields } => {
format!("Exactly one of these must be provided: {}", fields.join(", "))
},
Self::AnyOf { fields } => {
format!("At least one of these must be provided: {}", fields.join(", "))
},
Self::ConditionalRequired {
if_field_present,
then_required,
} => {
format!(
"If '{}' is provided, then {} must be provided",
if_field_present,
then_required.join(", ")
)
},
Self::RequiredIfAbsent {
absent_field,
then_required,
} => {
format!(
"If '{}' is absent, then {} must be provided",
absent_field,
then_required.join(", ")
)
},
Self::Email => "Must be a valid email address".to_string(),
Self::Phone => "Must be a valid E.164 phone number (e.g. +14155552671)".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LengthCheck {
Ok,
TooShort {
min: usize,
actual: usize,
},
TooLong {
max: usize,
actual: usize,
},
}
#[must_use]
pub fn check_length(value: &str, min: Option<usize>, max: Option<usize>) -> LengthCheck {
let actual = value.chars().count();
if let Some(min) = min {
if actual < min {
return LengthCheck::TooShort { min, actual };
}
}
if let Some(max) = max {
if actual > max {
return LengthCheck::TooLong { max, actual };
}
}
LengthCheck::Ok
}