use proc_macro2::Ident;
#[derive(Debug)]
pub struct SchemaInput {
pub models: Vec<ModelDef>,
}
#[derive(Debug)]
pub struct ModelDef {
pub name: Ident,
pub table_name: String,
pub db_columns: Vec<FieldDef>,
pub relationships: Vec<RelDef>,
pub constraints: Vec<TableConstraint>,
pub has_custom_hooks: bool,
}
#[derive(Debug)]
pub struct FieldDef {
pub rust_name: Ident,
pub column_name: String,
pub type_info: TypeInfo,
pub modifiers: Vec<Modifier>,
}
#[derive(Debug)]
pub struct RelDef {
pub rust_name: Ident,
pub target_model: Ident,
pub fk_column: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TypeInfo {
Integer,
Short,
BigInt,
Real,
Double,
Decimal { precision: u32, scale: u32 },
Varchar { max_length: u32 },
Text,
Bool,
Date,
Time,
DateTime,
Uuid,
Binary,
Json,
Jsonb,
Enum { rust_type: String },
Ltree,
Col { rust_type: String },
TextArray,
IntArray,
BigIntArray,
UuidArray,
BoolArray,
RealArray,
DoubleArray,
ShortArray,
VarcharArray,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Modifier {
Primary,
AutoIncrement,
Nullable,
Unique,
Default(String),
Now,
Tz,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TableConstraint {
PrimaryKey(Vec<String>),
Unique(Vec<String>),
Index(Vec<String>),
}
impl FieldDef {
pub fn is_primary(&self) -> bool {
self.modifiers.iter().any(|m| matches!(m, Modifier::Primary))
}
pub fn is_nullable(&self) -> bool {
self.modifiers.iter().any(|m| matches!(m, Modifier::Nullable))
}
pub fn is_auto_increment(&self) -> bool {
self.modifiers.iter().any(|m| matches!(m, Modifier::AutoIncrement))
}
pub fn is_unique(&self) -> bool {
self.modifiers.iter().any(|m| matches!(m, Modifier::Unique))
}
pub fn is_tz(&self) -> bool {
self.modifiers.iter().any(|m| matches!(m, Modifier::Tz))
}
}
impl ModelDef {
pub fn primary_key_columns(&self) -> Vec<&FieldDef> {
for constraint in &self.constraints {
if let TableConstraint::PrimaryKey(cols) = constraint {
return self.db_columns.iter()
.filter(|f| cols.contains(&f.rust_name.to_string()))
.collect();
}
}
self.db_columns.iter()
.filter(|f| f.is_primary())
.collect()
}
pub fn has_primary_key(&self) -> bool {
!self.primary_key_columns().is_empty()
}
pub fn column_count(&self) -> usize {
self.db_columns.len()
}
}
impl TypeInfo {
pub fn rust_type(&self, nullable: bool) -> String {
let base = match self {
TypeInfo::Integer => "i32".to_string(),
TypeInfo::Short => "i16".to_string(),
TypeInfo::BigInt => "i64".to_string(),
TypeInfo::Real => "f32".to_string(),
TypeInfo::Double => "f64".to_string(),
TypeInfo::Decimal { .. } => "sqlx::types::BigDecimal".to_string(),
TypeInfo::Varchar { .. } | TypeInfo::Text => "String".to_string(),
TypeInfo::Bool => "bool".to_string(),
TypeInfo::Date => "chrono::NaiveDate".to_string(),
TypeInfo::Time => "chrono::NaiveTime".to_string(),
TypeInfo::DateTime => "chrono::NaiveDateTime".to_string(), TypeInfo::Uuid => "uuid::Uuid".to_string(),
TypeInfo::Binary => "Vec<u8>".to_string(),
TypeInfo::Col { rust_type } => rust_type.clone(),
TypeInfo::TextArray | TypeInfo::VarcharArray => "Vec<String>".to_string(),
TypeInfo::IntArray => "Vec<i32>".to_string(),
TypeInfo::ShortArray => "Vec<i16>".to_string(),
TypeInfo::BigIntArray => "Vec<i64>".to_string(),
TypeInfo::UuidArray => "Vec<uuid::Uuid>".to_string(),
TypeInfo::BoolArray => "Vec<bool>".to_string(),
TypeInfo::RealArray => "Vec<f32>".to_string(),
TypeInfo::DoubleArray => "Vec<f64>".to_string(),
TypeInfo::Json | TypeInfo::Jsonb => "serde_json::Value".to_string(),
TypeInfo::Enum { rust_type } => rust_type.clone(),
TypeInfo::Ltree => "String".to_string(),
};
if nullable {
format!("Option<{}>", base)
} else {
base
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::Span;
fn make_ident(name: &str) -> Ident {
Ident::new(name, Span::call_site())
}
#[test]
fn test_field_def_modifiers() {
let mut field = FieldDef {
rust_name: make_ident("id"),
column_name: "id".to_string(),
type_info: TypeInfo::Integer,
modifiers: vec![],
};
assert!(!field.is_primary());
assert!(!field.is_auto_increment());
assert!(!field.is_nullable());
assert!(!field.is_unique());
assert!(!field.is_tz());
field.modifiers.push(Modifier::Primary);
field.modifiers.push(Modifier::AutoIncrement);
field.modifiers.push(Modifier::Nullable);
field.modifiers.push(Modifier::Unique);
field.modifiers.push(Modifier::Tz);
assert!(field.is_primary());
assert!(field.is_auto_increment());
assert!(field.is_nullable());
assert!(field.is_unique());
assert!(field.is_tz());
}
#[test]
fn test_model_def_primary_key() {
let field_id = FieldDef {
rust_name: make_ident("id"),
column_name: "id".to_string(),
type_info: TypeInfo::Integer,
modifiers: vec![Modifier::Primary],
};
let field_name = FieldDef {
rust_name: make_ident("name"),
column_name: "name".to_string(),
type_info: TypeInfo::Text,
modifiers: vec![],
};
let model = ModelDef {
name: make_ident("User"),
table_name: "users".to_string(),
db_columns: vec![field_id, field_name],
relationships: vec![],
constraints: vec![],
has_custom_hooks: false,
};
assert!(model.has_primary_key());
let pk_cols = model.primary_key_columns();
assert_eq!(pk_cols.len(), 1);
assert_eq!(pk_cols[0].rust_name.to_string(), "id");
}
#[test]
fn test_model_def_composite_primary_key() {
let field_c1 = FieldDef {
rust_name: make_ident("c1"),
column_name: "c1".to_string(),
type_info: TypeInfo::Integer,
modifiers: vec![],
};
let field_c2 = FieldDef {
rust_name: make_ident("c2"),
column_name: "c2".to_string(),
type_info: TypeInfo::Integer,
modifiers: vec![],
};
let model = ModelDef {
name: make_ident("Composite"),
table_name: "composites".to_string(),
db_columns: vec![field_c1, field_c2],
relationships: vec![],
constraints: vec![TableConstraint::PrimaryKey(vec!["c1".to_string(), "c2".to_string()])],
has_custom_hooks: false,
};
assert!(model.has_primary_key());
let pk_cols = model.primary_key_columns();
assert_eq!(pk_cols.len(), 2);
assert_eq!(pk_cols[0].rust_name.to_string(), "c1");
assert_eq!(pk_cols[1].rust_name.to_string(), "c2");
}
}