use crate::migration::{
utils::to_pascal_case,
{
RelationKind, column::ColumnDef, foreign_key::ForeignKeyDef, hooks::HooksDef,
index::IndexDef, primary_key::PrimaryKeyDef, relation::RelationDef,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderDir {
Asc,
Desc,
}
#[derive(Debug, Clone)]
pub struct ModelSchema {
pub model_name: String,
pub table_name: String, pub schema: Option<String>, pub primary_key: Option<PrimaryKeyDef>,
pub columns: Vec<ColumnDef>,
pub foreign_keys: Vec<ForeignKeyDef>,
pub relations: Vec<RelationDef>,
pub indexes: Vec<IndexDef>,
pub hooks: Option<HooksDef>,
pub ordering: Vec<(String, OrderDir)>,
pub unique_together: Vec<Vec<String>>,
pub verbose_name: Option<String>,
pub verbose_name_plural: Option<String>,
}
impl ModelSchema {
pub fn new(model_name: impl Into<String>) -> Self {
let name: String = model_name.into();
let table_name = to_snake_case(&name);
Self {
model_name: name,
table_name,
schema: None,
primary_key: None,
columns: Vec::new(),
foreign_keys: Vec::new(),
relations: Vec::new(),
indexes: Vec::new(),
hooks: None,
ordering: Vec::new(),
unique_together: Vec::new(),
verbose_name: None,
verbose_name_plural: None,
}
}
pub fn table_name(mut self, name: impl Into<String>) -> Self {
self.table_name = name.into();
self
}
pub fn schema(mut self, schema: impl Into<String>) -> Self {
self.schema = Some(schema.into());
self
}
pub fn primary_key(mut self, pk: PrimaryKeyDef) -> Self {
self.primary_key = Some(pk);
self
}
pub fn column(mut self, col: ColumnDef) -> Self {
self.columns.push(col);
self
}
pub fn foreign_key(mut self, fk: ForeignKeyDef) -> Self {
self.foreign_keys.push(fk);
self
}
pub fn relation(mut self, rel: RelationDef) -> Self {
self.relations.push(rel);
self
}
pub fn index(mut self, idx: IndexDef) -> Self {
self.indexes.push(idx);
self
}
pub fn hooks(mut self, hooks: HooksDef) -> Self {
self.hooks = Some(hooks);
self
}
pub fn order_by(mut self, field: impl Into<String>, dir: OrderDir) -> Self {
self.ordering.push((field.into(), dir));
self
}
pub fn unique_together(mut self, fields: Vec<String>) -> Self {
self.unique_together.push(fields);
self
}
pub fn verbose_name(mut self, name: impl Into<String>) -> Self {
self.verbose_name = Some(name.into());
self
}
pub fn verbose_name_plural(mut self, name: impl Into<String>) -> Self {
self.verbose_name_plural = Some(name.into());
self
}
pub fn build(self) -> Result<ModelSchema, String> {
if self.primary_key.is_none() {
return Err(format!(
"ModelSchema '{}' : missing primary key",
self.model_name
));
}
Ok(self)
}
pub fn to_migration(&self) -> sea_query::TableCreateStatement {
let mut table = sea_query::Table::create();
table
.table(sea_query::Alias::new(&self.table_name))
.if_not_exists();
if let Some(ref pk) = self.primary_key {
table.col(pk.to_sea_column());
}
for col in &self.columns {
if !col.ignored {
table.col(col.to_sea_column());
}
}
for fk in &self.foreign_keys {
table.foreign_key(&mut fk.to_sea_foreign_key(&self.table_name));
}
table.to_owned()
}
pub fn fill_form(
&self,
form: &mut crate::forms::Forms,
fields: Option<&[&str]>,
exclude: Option<&[&str]>,
) {
let pk_name = self.primary_key.as_ref().map(|pk| pk.name.as_str());
if let Some(field_names) = fields {
for &field_name in field_names {
let col = self.columns.iter().find(|c| c.name == field_name);
match col {
None => panic!(
"ModelForm '{}' : field '{}' does not exist in the schema",
self.model_name, field_name
),
Some(col) => {
if let Some(generic) = col.to_form_field() {
form.field_generic(generic);
}
}
}
}
} else {
let excluded: &[&str] = exclude.unwrap_or(&[]);
for col in &self.columns {
if pk_name == Some(col.name.as_str()) {
continue;
}
if excluded.contains(&col.name.as_str()) {
continue;
}
if let Some(generic) = col.to_form_field() {
form.field_generic(generic);
}
}
}
}
pub fn diff(&self, other: &ModelSchema) -> SchemaDiff {
let mut diff = SchemaDiff::new(&self.table_name);
let self_cols: std::collections::HashSet<&str> =
self.columns.iter().map(|c| c.name.as_str()).collect();
let other_cols: std::collections::HashSet<&str> =
other.columns.iter().map(|c| c.name.as_str()).collect();
for name in other_cols.difference(&self_cols) {
let col = other.columns.iter().find(|c| c.name == *name).unwrap();
diff.added_columns.push(col.clone());
}
for name in self_cols.difference(&other_cols) {
diff.dropped_columns.push(name.to_string());
}
diff
}
fn col_to_rust_type(col: &ColumnDef) -> String {
use sea_query::ColumnType::*;
let base = match &col.col_type {
String(_) | Text | Char(_) => "String".to_string(),
Integer | TinyInteger | SmallInteger => "i32".to_string(),
BigInteger => "i64".to_string(),
Unsigned => "u32".to_string(),
BigUnsigned => "u64".to_string(),
Float => "f32".to_string(),
Double => "f64".to_string(),
Boolean => "bool".to_string(),
Date => "chrono::NaiveDate".to_string(),
Time => "chrono::NaiveTime".to_string(),
DateTime | Timestamp | TimestampWithTimeZone => "chrono::NaiveDateTime".to_string(),
Uuid => "Uuid".to_string(),
Json | JsonBinary => "serde_json::Value".to_string(),
Decimal(_) => "rust_decimal::Decimal".to_string(),
Enum { .. } => "String".to_string(),
_ => "String".to_string(),
};
if col.nullable {
format!("Option<{}>", base)
} else {
base
}
}
pub fn to_model(&self) -> String {
let mut out = String::new();
let table_name = &self.table_name;
out.push_str("use sea_orm::entity::prelude::*;\n");
out.push_str("use serde::{Serialize, Deserialize};\n");
out.push_str("use runique::impl_objects;\n\n");
out.push_str(
"#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]\n",
);
out.push_str(&format!("#[sea_orm(table_name = \"{}\")]\n", table_name));
out.push_str("pub struct Model {\n");
if let Some(ref pk) = self.primary_key {
if pk.auto_increment {
out.push_str(" #[sea_orm(primary_key)]\n");
} else {
out.push_str(" #[sea_orm(primary_key, auto_increment = false)]\n");
}
out.push_str(&format!(
" pub {}: {},\n",
pk.name,
Self::pk_to_rust_type(pk)
));
}
for col in &self.columns {
if col.ignored {
continue;
}
let rust_type = Self::col_to_rust_type(col);
out.push_str(&format!(" pub {}: {},\n", col.name, rust_type));
}
out.push_str("}\n\n");
out.push_str("#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\n");
out.push_str("pub enum Relation {\n");
for rel in &self.relations {
match &rel.kind {
RelationKind::BelongsTo { from, to } => {
out.push_str(&format!(
" #[sea_orm(belongs_to = \"super::{}::Entity\", from = \"Column::{}\", to = \"super::{}::Column::{}\")]\n {},\n",
rel.target,
to_pascal_case(from),
rel.target,
to_pascal_case(to),
to_pascal_case(&rel.target)
));
}
RelationKind::HasMany | RelationKind::HasOne => {
out.push_str(&format!(
" #[sea_orm(has_many = \"super::{}::Entity\")]\n {},\n",
rel.target,
to_pascal_case(&rel.target)
));
}
RelationKind::ManyToMany { via } => {
out.push_str(&format!(
" #[sea_orm(many_to_many = \"super::{}::Entity\", via = \"super::{}::Entity\")]\n {},\n",
rel.target,
via,
to_pascal_case(&rel.target)
));
}
}
}
out.push_str("}\n\n");
out.push_str("impl ActiveModelBehavior for ActiveModel {}\n\n");
out.push_str("impl_objects!(Entity);\n");
out
}
fn pk_to_rust_type(pk: &PrimaryKeyDef) -> &'static str {
use sea_query::ColumnType::*;
match &pk.col_type {
Integer | TinyInteger | SmallInteger => "i32",
BigInteger => "i64",
Unsigned => "u32",
BigUnsigned => "u64",
Uuid => "Uuid",
_ => "i32",
}
}
pub fn auto_now_columns(&self) -> Vec<&ColumnDef> {
self.columns.iter().filter(|c| c.auto_now).collect()
}
pub fn auto_now_update_columns(&self) -> Vec<&ColumnDef> {
self.columns.iter().filter(|c| c.auto_now_update).collect()
}
pub fn has_auto_timestamps(&self) -> bool {
self.columns.iter().any(|c| c.auto_now || c.auto_now_update)
}
}
#[derive(Debug)]
pub struct SchemaDiff {
pub table_name: String,
pub added_columns: Vec<ColumnDef>,
pub dropped_columns: Vec<String>,
pub modified_columns: Vec<(ColumnDef, ColumnDef)>, }
impl SchemaDiff {
pub fn new(table_name: &str) -> Self {
Self {
table_name: table_name.to_string(),
added_columns: Vec::new(),
dropped_columns: Vec::new(),
modified_columns: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.added_columns.is_empty()
&& self.dropped_columns.is_empty()
&& self.modified_columns.is_empty()
}
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
result.push('_');
}
result.push(ch.to_lowercase().next().unwrap());
}
result
}