pub mod auto;
pub mod binary;
pub mod boolean;
pub mod char;
pub mod composite;
pub mod datetime;
pub mod decimal;
pub mod duration;
pub mod email;
pub mod file;
pub mod float;
pub mod generated;
pub mod integer;
pub mod ip;
pub mod json;
pub mod mixins;
pub mod positive;
pub mod related;
pub mod related_descriptors;
pub mod related_lookups;
pub mod slug;
pub mod text;
pub mod url;
pub mod uuid;
use crate::core::validators::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldDescriptor {
pub name: String,
pub column_name: String,
pub field_type: FieldType,
pub null: bool,
pub blank: bool,
pub default: Option<String>,
pub primary_key: bool,
pub unique: bool,
pub db_index: bool,
pub max_length: Option<usize>,
pub help_text: String,
pub verbose_name: String,
}
impl FieldDescriptor {
#[must_use]
pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
let name = name.into();
let max_length = field_type.max_length();
Self {
column_name: name.clone(),
verbose_name: default_verbose_name(&name),
help_text: String::new(),
default: None,
null: false,
blank: false,
primary_key: false,
unique: false,
db_index: false,
max_length,
name,
field_type,
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
self.field_type.validate()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldType {
Auto,
BigAuto,
SmallAuto,
Char {
max_length: usize,
},
Text,
Integer,
BigInteger,
SmallInteger,
PositiveInteger,
PositiveBigInteger,
PositiveSmallInteger,
CommaSeparatedInteger {
max_length: usize,
},
Float,
Decimal {
max_digits: u32,
decimal_places: u32,
},
Boolean,
NullBoolean,
DateTime {
auto_now: bool,
auto_now_add: bool,
},
Date {
auto_now: bool,
auto_now_add: bool,
},
Time,
Duration,
Uuid,
Json,
Binary,
File {
upload_to: String,
},
Image {
upload_to: String,
},
FilePath {
path: String,
},
Email,
Url,
Slug {
max_length: usize,
},
Ip,
GenericIp,
ForeignKey {
to: String,
on_delete: OnDelete,
},
OneToOne {
to: String,
on_delete: OnDelete,
},
ManyToMany {
to: String,
through: Option<String>,
},
}
impl FieldType {
#[must_use]
pub fn max_length(&self) -> Option<usize> {
match self {
Self::Char { max_length }
| Self::Slug { max_length }
| Self::CommaSeparatedInteger { max_length } => Some(*max_length),
_ => None,
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
match self {
Self::DateTime {
auto_now,
auto_now_add,
}
| Self::Date {
auto_now,
auto_now_add,
} => datetime::validate_temporal_flags(*auto_now, *auto_now_add),
Self::ForeignKey { to, .. }
| Self::OneToOne { to, .. }
| Self::ManyToMany { to, .. } => related::validate_related_target(to),
_ => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnDelete {
Cascade,
Protect,
SetNull,
SetDefault,
DoNothing,
}
fn default_verbose_name(name: &str) -> String {
let normalized = name.replace('_', " ");
let mut chars = normalized.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::{
FieldDescriptor, FieldType, char::validate_char_field, datetime::validate_temporal_flags,
float::validate_decimal_field, integer::validate_integer_field,
};
#[test]
fn field_descriptor_new_sets_django_defaults() {
let descriptor = FieldDescriptor::new("blog_title", FieldType::Char { max_length: 200 });
assert_eq!(descriptor.name, "blog_title");
assert_eq!(descriptor.column_name, "blog_title");
assert_eq!(descriptor.max_length, Some(200));
assert_eq!(descriptor.verbose_name, "Blog title");
assert!(!descriptor.null);
assert!(!descriptor.blank);
assert!(!descriptor.primary_key);
}
#[test]
fn char_field_validation_rejects_too_long_values() {
let error = validate_char_field("abcdef", 5).expect_err("value should exceed max_length");
assert_eq!(error.code, "max_length");
}
#[test]
fn integer_field_validation_enforces_minimum() {
let error =
validate_integer_field(0, Some(1), Some(10)).expect_err("value should be below min");
assert_eq!(error.code, "min_value");
}
#[test]
fn decimal_field_validation_enforces_decimal_places() {
let error =
validate_decimal_field("12.345", 5, 2).expect_err("value should exceed decimal places");
assert_eq!(error.code, "max_decimal_places");
}
#[test]
fn temporal_flags_cannot_both_be_enabled() {
let error =
validate_temporal_flags(true, true).expect_err("auto_now flags should conflict");
assert_eq!(error.code, "invalid");
}
}