use chrono::{NaiveDate, NaiveDateTime};
use serde_json::Value;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum FieldValue<T> {
Absent,
Null,
Present(T),
}
fn coerce_json_string(value: &Value) -> Result<String, FieldError> {
match value {
Value::String(value) => Ok(value.trim().to_owned()),
Value::Number(value) => Ok(value.to_string()),
_ => Err(FieldError::Custom("Not a valid string".to_owned())),
}
}
fn coerce_json_choice(value: &Value) -> Result<String, FieldError> {
match value {
Value::String(value) => Ok(value.clone()),
Value::Number(value) => Ok(value.to_string()),
_ => Err(FieldError::Custom("Not a valid string".to_owned())),
}
}
const F64_MAX_SAFE_INTEGER: f64 = (1_i64 << 53) as f64;
fn json_number_as_exact_i64(number: &serde_json::Number) -> Option<i64> {
if let Some(value) = number.as_i64() {
return Some(value);
}
let value = number.as_f64()?;
if !value.is_finite() || value.fract() != 0.0 || value.abs() > F64_MAX_SAFE_INTEGER {
return None;
}
Some(value as i64)
}
fn coerce_json_integer(value: &Value) -> Option<i64> {
match value {
Value::Number(number) => json_number_as_exact_i64(number),
Value::String(value) => {
let value = value.trim();
let value = value
.rsplit_once('.')
.filter(|(_, fraction)| fraction.chars().all(|character| character == '0'))
.map_or(value, |(integer, _)| integer);
value.parse().ok()
}
_ => None,
}
}
#[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),
WithMessage {
source: Box<FieldError>,
message: 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),
FieldError::WithMessage { message, .. } => f.write_str(message),
}
}
}
impl FieldError {
pub fn original(&self) -> &Self {
match self {
Self::WithMessage { source, .. } => source.original(),
_ => self,
}
}
pub fn is(&self, other: &Self) -> bool {
self.original() == other.original()
}
}
type FieldErrorFormatter = dyn Fn(&FieldError) -> Option<String> + Send + Sync;
#[derive(Clone, Default)]
pub struct FieldErrorMessages {
formatter: Option<Arc<FieldErrorFormatter>>,
}
impl fmt::Debug for FieldErrorMessages {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FieldErrorMessages")
.field("formatter", &self.formatter.as_ref().map(|_| "<closure>"))
.finish()
}
}
impl FieldErrorMessages {
fn from_formatter<F>(formatter: F) -> Self
where
F: Fn(&FieldError) -> Option<String> + Send + Sync + 'static,
{
Self {
formatter: Some(Arc::new(formatter)),
}
}
fn apply<T>(&self, result: Result<T, FieldError>) -> Result<T, FieldError> {
result.map_err(|error| match &self.formatter {
Some(formatter) => match formatter(&error) {
Some(message) => FieldError::WithMessage {
source: Box::new(error),
message,
},
None => error,
},
None => error,
})
}
}
impl std::error::Error for FieldError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::WithMessage { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
#[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>,
pub error_messages: FieldErrorMessages,
}
impl CharField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
min_length: None,
max_length: None,
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(
&self,
value: Option<&Value>,
) -> Result<FieldValue<String>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(value) => value,
};
let value = coerce_json_string(value)?;
self.validate(&value)?;
Ok(FieldValue::Present(value))
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
self.error_messages.apply((|| {
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>,
pub error_messages: FieldErrorMessages,
}
impl IntegerField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
min_value: None,
max_value: None,
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(&self, value: Option<&Value>) -> Result<FieldValue<i64>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(value) => value,
};
let parsed = coerce_json_integer(value)
.ok_or_else(|| FieldError::Custom("A valid integer is required".to_owned()))?;
self.validate(parsed)?;
Ok(FieldValue::Present(parsed))
}
pub fn validate(&self, value: i64) -> Result<(), FieldError> {
self.error_messages.apply((|| {
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>,
pub error_messages: FieldErrorMessages,
}
impl FloatField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
min_value: None,
max_value: None,
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(&self, value: Option<&Value>) -> Result<FieldValue<f64>, FieldError> {
let parsed = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(Value::Number(value)) => value.as_f64(),
Some(Value::String(value)) => value.trim().parse().ok(),
Some(Value::Bool(value)) => Some(if *value { 1.0 } else { 0.0 }),
Some(_) => None,
}
.ok_or_else(|| FieldError::Custom("A valid number is required".to_owned()))?;
if !parsed.is_finite() {
return Err(FieldError::Custom("A valid number is required".to_owned()));
}
self.validate(parsed)?;
Ok(FieldValue::Present(parsed))
}
pub fn validate(&self, value: f64) -> Result<(), FieldError> {
self.error_messages.apply((|| {
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 to_internal_value(&self, value: Option<&Value>) -> Result<FieldValue<bool>, FieldError> {
let parsed = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(Value::Bool(value)) => Some(*value),
Some(Value::Number(value)) if value.as_f64() == Some(1.0) => Some(true),
Some(Value::Number(value)) if value.as_f64() == Some(0.0) => Some(false),
Some(Value::String(value)) => {
let value = value.trim();
if value.is_empty() && self.allow_null {
return Ok(FieldValue::Null);
}
if ["t", "y", "yes", "true", "on", "1"]
.iter()
.any(|candidate| value.eq_ignore_ascii_case(candidate))
{
Some(true)
} else if ["f", "n", "no", "false", "off", "0"]
.iter()
.any(|candidate| value.eq_ignore_ascii_case(candidate))
{
Some(false)
} else {
None
}
}
Some(_) => None,
}
.ok_or_else(|| FieldError::Custom("Must be a valid boolean".to_owned()))?;
self.validate(parsed)?;
Ok(FieldValue::Present(parsed))
}
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>,
pub error_messages: FieldErrorMessages,
}
impl EmailField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(
&self,
value: Option<&Value>,
) -> Result<FieldValue<String>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(value) => value,
};
let value = coerce_json_string(value)?;
self.validate(&value)?;
Ok(FieldValue::Present(value))
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
self.error_messages.apply((|| {
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>,
pub error_messages: FieldErrorMessages,
}
impl URLField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(
&self,
value: Option<&Value>,
) -> Result<FieldValue<String>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(value) => value,
};
let value = coerce_json_string(value)?;
self.validate(&value)?;
Ok(FieldValue::Present(value))
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
self.error_messages.apply((|| {
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>,
pub error_messages: FieldErrorMessages,
}
impl ChoiceField {
pub fn new(choices: Vec<String>) -> Self {
Self {
required: true,
allow_null: false,
allow_blank: false,
choices,
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(
&self,
value: Option<&Value>,
) -> Result<FieldValue<String>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(value) => value,
};
let value = coerce_json_choice(value)?;
self.validate(&value)?;
Ok(FieldValue::Present(value))
}
pub fn validate(&self, value: &str) -> Result<(), FieldError> {
self.error_messages.apply((|| {
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>,
pub error_messages: FieldErrorMessages,
}
impl DateField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
format: "%Y-%m-%d".to_string(),
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(
&self,
value: Option<&Value>,
) -> Result<FieldValue<NaiveDate>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(Value::String(value)) => value,
Some(_) => return Err(FieldError::InvalidDate),
};
if value.is_empty() {
return Err(FieldError::InvalidDate);
}
Ok(FieldValue::Present(self.parse(value)?))
}
pub fn parse(&self, value: &str) -> Result<NaiveDate, FieldError> {
self.error_messages.apply((|| {
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>,
pub error_messages: FieldErrorMessages,
}
impl DateTimeField {
pub fn new() -> Self {
Self {
required: true,
allow_null: false,
format: "%Y-%m-%d %H:%M:%S".to_string(),
default: None,
error_messages: FieldErrorMessages::default(),
}
}
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 to_internal_value(
&self,
value: Option<&Value>,
) -> Result<FieldValue<NaiveDateTime>, FieldError> {
let value = match value {
None => return Ok(FieldValue::Absent),
Some(Value::Null) if self.allow_null => return Ok(FieldValue::Null),
Some(Value::Null) => return Err(FieldError::Null),
Some(Value::String(value)) => value,
Some(_) => return Err(FieldError::InvalidDateTime),
};
if value.is_empty() {
return Err(FieldError::InvalidDateTime);
}
Ok(FieldValue::Present(self.parse(value)?))
}
pub fn parse(&self, value: &str) -> Result<NaiveDateTime, FieldError> {
self.error_messages.apply((|| {
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()
}
}
macro_rules! impl_error_messages {
($($field:ty),+ $(,)?) => {
$(
impl $field {
pub fn error_messages<F>(mut self, formatter: F) -> Self
where
F: Fn(&FieldError) -> Option<String> + Send + Sync + 'static,
{
self.error_messages = FieldErrorMessages::from_formatter(formatter);
self
}
}
)*
};
}
impl_error_messages!(
CharField,
IntegerField,
FloatField,
EmailField,
URLField,
ChoiceField,
DateField,
DateTimeField,
);
#[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);
}
}