use chrono::{NaiveDate, NaiveDateTime};
use std::fmt;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum FieldError {
Required,
Null,
TooShort(usize),
TooLong(usize),
TooSmall(i64),
TooLarge(i64),
TooSmallFloat(f64),
TooLargeFloat(f64),
InvalidEmail,
InvalidUrl,
InvalidChoice,
InvalidDate,
InvalidDateTime,
Custom(String),
}
impl fmt::Display for FieldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FieldError::Required => write!(f, "This field is required"),
FieldError::Null => write!(f, "This field may not be null"),
FieldError::TooShort(min) => write!(f, "String is too short (min: {})", min),
FieldError::TooLong(max) => write!(f, "String is too long (max: {})", max),
FieldError::TooSmall(min) => write!(f, "Value is too small (min: {})", min),
FieldError::TooLarge(max) => write!(f, "Value is too large (max: {})", max),
FieldError::TooSmallFloat(min) => write!(f, "Value is too small (min: {})", min),
FieldError::TooLargeFloat(max) => write!(f, "Value is too large (max: {})", max),
FieldError::InvalidEmail => write!(f, "Enter a valid email address"),
FieldError::InvalidUrl => write!(f, "Enter a valid URL"),
FieldError::InvalidChoice => write!(f, "Invalid choice"),
FieldError::InvalidDate => write!(f, "Invalid date format"),
FieldError::InvalidDateTime => write!(f, "Invalid datetime format"),
FieldError::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for FieldError {}
#[derive(Debug, Clone)]
pub struct CharField {
pub required: bool,
pub allow_null: bool,
pub allow_blank: bool,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub default: Option<String>,
}
impl CharField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
min_length: None,
max_length: None,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn allow_blank(mut self, allow_blank: bool) -> Self {
self.allow_blank = allow_blank;
self
}
pub fn min_length(mut self, min_length: usize) -> Self {
self.min_length = Some(min_length);
self
}
pub fn max_length(mut self, max_length: usize) -> Self {
self.max_length = Some(max_length);
self
}
pub fn default(mut self, default: String) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
if value.is_empty() && !self.allow_blank {
return Err(FieldError::Required);
}
let char_count = value.chars().count();
if let Some(min) = self.min_length
&& char_count < min
{
return Err(FieldError::TooShort(min));
}
if let Some(max) = self.max_length
&& char_count > max
{
return Err(FieldError::TooLong(max));
}
Ok(())
}
}
impl Default for CharField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct IntegerField {
pub required: bool,
pub allow_null: bool,
pub min_value: Option<i64>,
pub max_value: Option<i64>,
pub default: Option<i64>,
}
impl IntegerField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
min_value: None,
max_value: None,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn min_value(mut self, min_value: i64) -> Self {
self.min_value = Some(min_value);
self
}
pub fn max_value(mut self, max_value: i64) -> Self {
self.max_value = Some(max_value);
self
}
pub fn default(mut self, default: i64) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, value: i64) -> Result<(), FieldError> {
if let Some(min) = self.min_value
&& value < min
{
return Err(FieldError::TooSmall(min));
}
if let Some(max) = self.max_value
&& value > max
{
return Err(FieldError::TooLarge(max));
}
Ok(())
}
}
impl Default for IntegerField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FloatField {
pub required: bool,
pub allow_null: bool,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
pub default: Option<f64>,
}
impl FloatField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
min_value: None,
max_value: None,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn min_value(mut self, min_value: f64) -> Self {
self.min_value = Some(min_value);
self
}
pub fn max_value(mut self, max_value: f64) -> Self {
self.max_value = Some(max_value);
self
}
pub fn default(mut self, default: f64) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, value: f64) -> Result<(), FieldError> {
if let Some(min) = self.min_value
&& value < min
{
return Err(FieldError::TooSmallFloat(min));
}
if let Some(max) = self.max_value
&& value > max
{
return Err(FieldError::TooLargeFloat(max));
}
Ok(())
}
}
impl Default for FloatField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BooleanField {
pub required: bool,
pub allow_null: bool,
pub default: Option<bool>,
}
impl BooleanField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn default(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, _value: bool) -> Result<(), FieldError> {
Ok(())
}
}
impl Default for BooleanField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct EmailField {
pub required: bool,
pub allow_null: bool,
pub allow_blank: bool,
pub default: Option<String>,
}
impl EmailField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn allow_blank(mut self, allow_blank: bool) -> Self {
self.allow_blank = allow_blank;
self
}
pub fn default(mut self, default: String) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
if value.is_empty() {
if !self.allow_blank {
return Err(FieldError::Required);
}
return Ok(());
}
if !value.contains('@') {
return Err(FieldError::InvalidEmail);
}
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 {
return Err(FieldError::InvalidEmail);
}
let local = parts[0];
let domain = parts[1];
if local.is_empty() || domain.is_empty() {
return Err(FieldError::InvalidEmail);
}
if !domain.contains('.') {
return Err(FieldError::InvalidEmail);
}
Ok(())
}
}
impl Default for EmailField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct URLField {
pub required: bool,
pub allow_null: bool,
pub allow_blank: bool,
pub default: Option<String>,
}
impl URLField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn allow_blank(mut self, allow_blank: bool) -> Self {
self.allow_blank = allow_blank;
self
}
pub fn default(mut self, default: String) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
if value.is_empty() {
if !self.allow_blank {
return Err(FieldError::Required);
}
return Ok(());
}
if !value.starts_with("http://") && !value.starts_with("https://") {
return Err(FieldError::InvalidUrl);
}
let without_protocol = value
.strip_prefix("https://")
.or_else(|| value.strip_prefix("http://"))
.expect("URL must start with http:// or https://");
if without_protocol.is_empty() {
return Err(FieldError::InvalidUrl);
}
Ok(())
}
}
impl Default for URLField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ChoiceField {
pub required: bool,
pub allow_null: bool,
pub allow_blank: bool,
pub choices: Vec<String>,
pub default: Option<String>,
}
impl ChoiceField {
pub fn new(choices: Vec<String>) -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
choices,
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn allow_blank(mut self, allow_blank: bool) -> Self {
self.allow_blank = allow_blank;
self
}
pub fn default(mut self, default: String) -> Self {
self.default = Some(default);
self
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
if value.is_empty() {
if !self.allow_blank {
return Err(FieldError::Required);
}
return Ok(());
}
if !self.choices.iter().any(|c| c == value) {
return Err(FieldError::InvalidChoice);
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DateField {
pub required: bool,
pub allow_null: bool,
pub format: String,
pub default: Option<NaiveDate>,
}
impl DateField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
format: "%Y-%m-%d".to_string(),
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn format(mut self, format: &str) -> Self {
self.format = format.to_string();
self
}
pub fn default(mut self, default: NaiveDate) -> Self {
self.default = Some(default);
self
}
pub fn parse(&self, value: &str) -> Result<NaiveDate, FieldError> {
if value.is_empty() {
if !self.required
&& let Some(default) = self.default
{
return Ok(default);
}
return Err(FieldError::Required);
}
NaiveDate::parse_from_str(value, &self.format).map_err(|_| FieldError::InvalidDate)
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
self.parse(value)?;
Ok(())
}
}
impl Default for DateField {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct DateTimeField {
pub required: bool,
pub allow_null: bool,
pub format: String,
pub default: Option<NaiveDateTime>,
}
impl DateTimeField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
format: "%Y-%m-%d %H:%M:%S".to_string(),
default: None,
}
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn allow_null(mut self, allow_null: bool) -> Self {
self.allow_null = allow_null;
self
}
pub fn format(mut self, format: &str) -> Self {
self.format = format.to_string();
self
}
pub fn default(mut self, default: NaiveDateTime) -> Self {
self.default = Some(default);
self
}
pub fn parse(&self, value: &str) -> Result<NaiveDateTime, FieldError> {
if value.is_empty() {
if !self.required
&& let Some(default) = self.default
{
return Ok(default);
}
return Err(FieldError::Required);
}
NaiveDateTime::parse_from_str(value, &self.format).map_err(|_| FieldError::InvalidDateTime)
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
self.parse(value)?;
Ok(())
}
}
impl Default for DateTimeField {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
use rstest::rstest;
#[test]
fn test_char_field_valid() {
let field = CharField::new().min_length(3).max_length(10);
assert!(field.validate("hello").is_ok());
}
#[test]
fn test_char_field_too_short() {
let field = CharField::new().min_length(5);
assert_eq!(field.validate("hi"), Err(FieldError::TooShort(5)));
}
#[test]
fn test_char_field_too_long() {
let field = CharField::new().max_length(5);
assert_eq!(field.validate("hello world"), Err(FieldError::TooLong(5)));
}
#[rstest]
#[case::cjk_within_max("こんにちは世", 10, true)]
#[case::cjk_exceeding_max("こんにちはこんにちはあ", 10, false)]
#[case::emoji_within_max("🎉🎊🎈", 5, true)]
#[case::emoji_exceeding_max("🎉🎊🎈🎁🎀🎆", 5, false)]
#[case::mixed_ascii_cjk("hello世界", 10, true)]
#[case::boundary_exact_max("あいうえお", 5, true)]
#[case::boundary_one_over_max("あいうえおか", 5, false)]
fn test_char_field_unicode_max_length(
#[case] input: &str,
#[case] max_length: usize,
#[case] should_pass: bool,
) {
let field = CharField::new().max_length(max_length);
let result = field.validate(input);
assert_eq!(result.is_ok(), should_pass);
}
#[rstest]
#[case::cjk_meets_min("こんにちは", 5, true)]
#[case::cjk_below_min("こん", 5, false)]
#[case::boundary_exact_min("あいう", 3, true)]
fn test_char_field_unicode_min_length(
#[case] input: &str,
#[case] min_length: usize,
#[case] should_pass: bool,
) {
let field = CharField::new().min_length(min_length);
let result = field.validate(input);
assert_eq!(result.is_ok(), should_pass);
}
#[test]
fn test_integer_field_valid() {
let field = IntegerField::new().min_value(0).max_value(100);
assert!(field.validate(50).is_ok());
}
#[test]
fn test_integer_field_too_small() {
let field = IntegerField::new().min_value(0);
assert_eq!(field.validate(-1), Err(FieldError::TooSmall(0)));
}
#[test]
fn test_integer_field_too_large() {
let field = IntegerField::new().max_value(100);
assert_eq!(field.validate(101), Err(FieldError::TooLarge(100)));
}
#[test]
fn test_float_field_valid() {
let field = FloatField::new().min_value(0.0).max_value(1.0);
assert!(field.validate(0.5).is_ok());
}
#[test]
fn test_boolean_field() {
let field = BooleanField::new();
assert!(field.validate(true).is_ok());
assert!(field.validate(false).is_ok());
}
#[test]
fn test_email_field_valid() {
let field = EmailField::new();
assert!(field.validate("user@example.com").is_ok());
}
#[test]
fn test_email_field_invalid() {
let field = EmailField::new();
assert_eq!(field.validate("invalid"), Err(FieldError::InvalidEmail));
assert_eq!(
field.validate("@example.com"),
Err(FieldError::InvalidEmail)
);
assert_eq!(field.validate("user@"), Err(FieldError::InvalidEmail));
}
#[test]
fn test_url_field_valid() {
let field = URLField::new();
assert!(field.validate("https://example.com").is_ok());
assert!(field.validate("http://localhost:8000").is_ok());
}
#[test]
fn test_url_field_invalid() {
let field = URLField::new();
assert_eq!(field.validate("invalid"), Err(FieldError::InvalidUrl));
assert_eq!(
field.validate("ftp://example.com"),
Err(FieldError::InvalidUrl)
);
}
#[test]
fn test_choice_field_valid() {
let field = ChoiceField::new(vec![
"red".to_string(),
"green".to_string(),
"blue".to_string(),
]);
assert!(field.validate("red").is_ok());
assert!(field.validate("green").is_ok());
}
#[test]
fn test_choice_field_invalid() {
let field = ChoiceField::new(vec!["red".to_string(), "green".to_string()]);
assert_eq!(field.validate("blue"), Err(FieldError::InvalidChoice));
}
#[test]
fn test_date_field_valid() {
let field = DateField::new();
let date = field.parse("2024-01-15").unwrap();
assert_eq!(date.year(), 2024);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 15);
}
#[test]
fn test_date_field_invalid() {
let field = DateField::new();
assert_eq!(field.validate("invalid-date"), Err(FieldError::InvalidDate));
}
#[test]
fn test_date_field_custom_format() {
let field = DateField::new().format("%d/%m/%Y");
let date = field.parse("15/01/2024").unwrap();
assert_eq!(date.year(), 2024);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 15);
}
#[test]
fn test_datetime_field_valid() {
let field = DateTimeField::new();
let dt = field.parse("2024-01-15 14:30:00").unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 15);
assert_eq!(dt.hour(), 14);
assert_eq!(dt.minute(), 30);
assert_eq!(dt.second(), 0);
}
#[test]
fn test_datetime_field_invalid() {
let field = DateTimeField::new();
assert_eq!(
field.validate("invalid-datetime"),
Err(FieldError::InvalidDateTime)
);
}
#[test]
fn test_datetime_field_custom_format() {
let field = DateTimeField::new().format("%d/%m/%Y %H:%M");
let dt = field.parse("15/01/2024 14:30").unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.hour(), 14);
assert_eq!(dt.minute(), 30);
}
}