use crate::utils::trad::tf;
use syn::{
Ident, LitStr, Token, braced, bracketed, parenthesized,
parse::{Parse, ParseStream},
visit::Visit,
};
use crate::migration::utils::types::{ParsedColumn, ParsedFk, ParsedIndex, ParsedSchema};
struct DslModel {
name: String,
table: String,
pk: DslPk,
enum_types: Vec<(String, String, Vec<String>)>, fields: Vec<DslField>,
relations: Vec<DslRelation>,
unique_together: Vec<Vec<String>>,
indexes: Vec<Vec<String>>,
}
struct DslPk {
name: String,
ty: String, }
struct DslField {
name: String,
ty: String,
enum_name: Option<String>, options: Vec<String>,
default_value: Option<String>, renamed_from: Option<String>, }
fn capture_tokens_string(buf: &syn::parse::ParseBuffer) -> String {
let mut s = String::new();
while !buf.is_empty() {
match buf.parse::<proc_macro2::TokenTree>() {
Ok(tt) => s.push_str(&tt.to_string()),
Err(_) => break,
}
}
s
}
fn capture_value_until_comma(buf: &syn::parse::ParseBuffer) -> String {
let mut s = String::new();
while !buf.is_empty() && !buf.peek(Token![,]) {
match buf.parse::<proc_macro2::TokenTree>() {
Ok(tt) => s.push_str(&tt.to_string()),
Err(_) => break,
}
}
s
}
enum DslRelationKind {
BelongsTo {
from_column: String,
on_delete: String,
on_update: String,
},
HasMany,
HasOne,
ManyToMany {
#[allow(dead_code)]
via: String,
},
}
struct DslRelation {
kind: DslRelationKind,
target: String,
}
impl Parse for DslModel {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let kw: Ident = input.parse()?;
if kw != "table" {
return Err(syn::Error::new(kw.span(), "expected 'table'"));
}
input.parse::<Token![:]>()?;
let table: LitStr = input.parse()?;
input.parse::<Token![,]>()?;
let kw: Ident = input.parse()?;
if kw != "pk" {
return Err(syn::Error::new(kw.span(), "expected 'pk'"));
}
input.parse::<Token![:]>()?;
let pk_name: Ident = input.parse()?;
input.parse::<Token![=>]>()?;
let pk_ty: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let mut enum_types: Vec<(String, String, Vec<String>)> = Vec::new();
if input.peek(Ident) {
let peek: Ident = input.fork().parse()?;
if peek == "enums" {
input.parse::<Ident>()?;
input.parse::<Token![:]>()?;
let enum_block;
braced!(enum_block in input);
while !enum_block.is_empty() {
if let Ok(enum_name) = enum_block.parse::<Ident>() {
let _ = enum_block.parse::<Token![:]>();
let backing = if enum_block.peek(Ident) {
let ty: Ident = enum_block.fork().parse().unwrap();
match ty.to_string().as_str() {
"String" | "i32" | "i64" | "pg" => {
enum_block.parse::<Ident>().ok();
ty.to_string()
}
_ => "Auto".to_string(),
}
} else {
"Auto".to_string()
};
let mut string_values: Vec<String> = Vec::new();
if enum_block.peek(syn::token::Bracket) {
let variants;
bracketed!(variants in enum_block);
while !variants.is_empty() {
if let Ok(vname) = variants.parse::<Ident>() {
if variants.peek(Token![=]) {
let _ = variants.parse::<Token![=]>();
if let Ok(lit) = variants.parse::<syn::Lit>() {
match lit {
syn::Lit::Str(s) => string_values.push(s.value()),
syn::Lit::Int(n) => {
string_values.push(n.to_string())
}
_ => string_values.push(vname.to_string()),
}
} else if variants.peek(syn::token::Paren) {
let tuple;
parenthesized!(tuple in variants);
if let Ok(syn::Lit::Str(s)) = tuple.parse::<syn::Lit>()
{
string_values.push(s.value());
} else {
string_values.push(vname.to_string());
}
while !tuple.is_empty() {
tuple.parse::<proc_macro2::TokenTree>().ok();
}
} else {
string_values.push(vname.to_string());
}
} else {
string_values.push(vname.to_string());
}
} else {
variants.parse::<proc_macro2::TokenTree>().ok();
}
let _ = variants.parse::<Token![,]>();
}
}
enum_types.push((enum_name.to_string(), backing, string_values));
let _ = enum_block.parse::<Token![,]>();
} else {
enum_block.parse::<proc_macro2::TokenTree>().ok();
}
}
let _ = input.parse::<Token![,]>();
}
}
let mut fields = Vec::new();
let fields_content;
braced!(fields_content in input);
while !fields_content.is_empty() {
fields.push(DslField::parse(&fields_content)?);
}
let mut relations = Vec::new();
let mut unique_together: Vec<Vec<String>> = Vec::new();
let mut indexes: Vec<Vec<String>> = Vec::new();
while !input.is_empty() {
let _ = input.parse::<Token![,]>();
if input.is_empty() {
break;
}
if !input.peek(Ident) {
input.parse::<proc_macro2::TokenTree>().ok();
continue;
}
let kw: Ident = input.parse()?;
input.parse::<Token![:]>()?;
let block_content;
braced!(block_content in input);
match kw.to_string().as_str() {
"relations" => {
while !block_content.is_empty() {
if let Ok(rel) = DslRelation::parse(&block_content) {
relations.push(rel);
} else {
while !block_content.is_empty() && !block_content.peek(Token![,]) {
block_content.parse::<proc_macro2::TokenTree>().ok();
}
let _ = block_content.parse::<Token![,]>();
}
}
}
"meta" => {
while !block_content.is_empty() {
let Ok(meta_key): syn::Result<Ident> = block_content.parse() else {
block_content.parse::<proc_macro2::TokenTree>().ok();
continue;
};
let _ = block_content.parse::<Token![:]>();
match meta_key.to_string().as_str() {
"unique_together" | "indexes" => {
let list_content;
bracketed!(list_content in block_content);
let mut groups: Vec<Vec<String>> = Vec::new();
while !list_content.is_empty() {
let tuple_content;
syn::parenthesized!(tuple_content in list_content);
let mut group = Vec::new();
while !tuple_content.is_empty() {
if let Ok(f) = tuple_content.parse::<Ident>() {
group.push(f.to_string());
}
let _ = tuple_content.parse::<Token![,]>();
}
if !group.is_empty() {
groups.push(group);
}
let _ = list_content.parse::<Token![,]>();
}
if meta_key == "unique_together" {
unique_together.extend(groups);
} else {
indexes.extend(groups);
}
}
_ => {
block_content.parse::<proc_macro2::TokenTree>().ok();
}
}
let _ = block_content.parse::<Token![,]>();
}
}
_ => {
while !block_content.is_empty() {
block_content.parse::<proc_macro2::TokenTree>().ok();
}
}
}
}
Ok(DslModel {
name: name.to_string(),
table: table.value(),
pk: DslPk {
name: pk_name.to_string(),
ty: pk_ty.to_string(),
},
enum_types,
fields,
relations,
unique_together,
indexes,
})
}
}
impl Parse for DslField {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
input.parse::<Token![:]>()?;
let (ty, mut enum_name) = if input.peek(Token![enum]) {
input.parse::<Token![enum]>()?;
let inner;
syn::parenthesized!(inner in input);
let ename: Ident = inner.parse()?;
("enum".to_string(), Some(ename.to_string()))
} else {
let ty: Ident = input.parse()?;
if input.peek(syn::token::Paren) {
let inner;
syn::parenthesized!(inner in input);
while !inner.is_empty() {
inner.parse::<proc_macro2::TokenTree>().ok();
}
}
(ty.to_string(), None)
};
let mut options = Vec::new();
let mut default_value: Option<String> = None;
let mut renamed_from: Option<String> = None;
if input.peek(syn::token::Bracket) {
let opts;
bracketed!(opts in input);
while !opts.is_empty() {
if opts.peek(Token![enum]) {
opts.parse::<Token![enum]>()?;
let inner;
syn::parenthesized!(inner in opts);
let ename: Ident = inner.parse()?;
enum_name = Some(ename.to_string());
let _ = opts.parse::<Token![,]>();
continue;
}
let opt: Ident = opts.parse()?;
let opt_str = opt.to_string();
options.push(opt_str.clone());
if opts.peek(syn::token::Paren) {
let inner;
syn::parenthesized!(inner in opts);
let captured = capture_tokens_string(&inner);
if opt_str == "default" {
default_value = Some(captured);
} else if opt_str == "renamed_from" {
renamed_from = Some(captured.trim_matches('"').to_string());
}
}
else if opts.peek(Token![:]) {
opts.parse::<Token![:]>()?;
let captured = capture_value_until_comma(&opts);
if opt_str == "default" {
default_value = Some(captured);
} else if opt_str == "renamed_from" {
renamed_from = Some(captured.trim_matches('"').to_string());
}
}
let _ = opts.parse::<Token![,]>();
}
}
let _ = input.parse::<Token![,]>();
Ok(DslField {
name: name.to_string(),
ty,
enum_name,
options,
default_value,
renamed_from,
})
}
}
impl Parse for DslRelation {
fn parse(input: ParseStream) -> syn::Result<Self> {
let kind_kw: Ident = input.parse()?;
input.parse::<Token![:]>()?;
let target: Ident = input.parse()?;
let rel = match kind_kw.to_string().as_str() {
"belongs_to" => {
let via_kw: Ident = input.parse()?;
if via_kw != "via" {
return Err(syn::Error::new(
via_kw.span(),
"expected 'via' after target model",
));
}
let from_col: Ident = input.parse()?;
let mut on_delete = "NoAction".to_string();
let mut on_update = "NoAction".to_string();
if input.peek(syn::token::Bracket) {
let opts;
bracketed!(opts in input);
let mut i: i32 = 0;
while !opts.is_empty() {
let opt: Ident = opts.parse()?;
let val = normalize_fk_action(&opt.to_string());
if i == 0 {
on_delete = val;
} else {
on_update = val;
}
i = i.saturating_add(1);
let _ = opts.parse::<Token![,]>();
}
}
let _ = input.parse::<Token![,]>();
DslRelation {
kind: DslRelationKind::BelongsTo {
from_column: from_col.to_string(),
on_delete,
on_update,
},
target: target.to_string(),
}
}
"has_many" => {
let _ = input.parse::<Token![,]>();
DslRelation {
kind: DslRelationKind::HasMany,
target: target.to_string(),
}
}
"has_one" => {
let _ = input.parse::<Token![,]>();
DslRelation {
kind: DslRelationKind::HasOne,
target: target.to_string(),
}
}
"many_to_many" => {
let via_kw: Ident = input.parse()?;
if via_kw != "via" {
return Err(syn::Error::new(
via_kw.span(),
"expected 'via' after target model",
));
}
let via: Ident = input.parse()?;
let _ = input.parse::<Token![,]>();
DslRelation {
kind: DslRelationKind::ManyToMany {
via: via.to_string(),
},
target: target.to_string(),
}
}
_ => {
while !input.is_empty() && !input.peek(Token![,]) {
input.parse::<proc_macro2::TokenTree>().ok();
}
let _ = input.parse::<Token![,]>();
DslRelation {
kind: DslRelationKind::HasMany,
target: target.to_string(),
}
}
};
Ok(rel)
}
}
fn normalize_fk_action(s: &str) -> String {
match s.to_lowercase().as_str() {
"cascade" => "Cascade".to_string(),
"restrict" => "Restrict".to_string(),
"set_null" | "setnull" => "SetNull".to_string(),
"set_default" | "setdefault" => "SetDefault".to_string(),
_ => "NoAction".to_string(),
}
}
fn pascal_to_snake(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
}
fn dsl_type_to_col_type(ty: &str, default: &str) -> String {
match ty {
"String" | "char" | "varchar" => "String".to_string(),
"text" => "String".to_string(),
"i8" => "TinyInteger".to_string(),
"i16" => "SmallInteger".to_string(),
"i32" | "integer" => "Integer".to_string(),
"i64" | "big_integer" | "bigint" => "BigInteger".to_string(),
"u32" => "Unsigned".to_string(),
"u64" => "BigUnsigned".to_string(),
"f32" => "Float".to_string(),
"f64" | "float" | "percent" => "Double".to_string(),
"decimal" => "Decimal".to_string(),
"bool" => "Boolean".to_string(),
"date" => "Date".to_string(),
"time" => "Time".to_string(),
"datetime" | "timestamp" => "DateTime".to_string(),
"timestamp_tz" => "TimestampWithTimeZone".to_string(),
"uuid" => "Uuid".to_string(),
"json" | "json_binary" => "Json".to_string(),
"binary" | "blob" => "Binary".to_string(),
"var_binary" => "VarBinary".to_string(),
"inet" | "cidr" | "mac_address" | "interval" | "ip" => "String".to_string(),
"email" | "url" | "password" | "slug" | "color" | "phone" => "String".to_string(),
"richtext" | "textarea" => "Text".to_string(),
"image" | "document" | "file" => "String".to_string(),
"choice" | "radio" => "String".to_string(),
"int" => "Integer".to_string(),
"Pk" => pk_alias_col_type(),
_ => {
eprintln!("{}", tf("makemigrations.unknown_dsl_type", &[ty, default]));
default.to_string()
}
}
}
fn dsl_field_type_to_col_type(ty: &str) -> String {
dsl_type_to_col_type(ty, "String")
}
fn dsl_pk_to_col_type(ty: &str) -> String {
dsl_type_to_col_type(ty, "Integer")
}
#[cfg(feature = "pk-uuid")]
fn pk_alias_col_type() -> String {
"Uuid".to_string()
}
#[cfg(all(feature = "big-pk", not(feature = "pk-uuid")))]
fn pk_alias_col_type() -> String {
"BigInteger".to_string()
}
#[cfg(not(any(feature = "big-pk", feature = "pk-uuid")))]
fn pk_alias_col_type() -> String {
"Integer".to_string()
}
fn dsl_to_parsed_schema(model: DslModel) -> ParsedSchema {
let primary_key = Some(ParsedColumn {
name: model.pk.name,
col_type: dsl_pk_to_col_type(&model.pk.ty),
nullable: false,
unique: false,
ignored: false,
created_at: false,
updated_at: false,
has_default_now: false,
default_value: None,
enum_name: None,
enum_string_values: Vec::new(),
enum_is_pg: false,
renamed_from: None,
});
let enum_types = model.enum_types;
let columns = model
.fields
.into_iter()
.map(|f| {
let has_auto_now = f.options.contains(&"auto_now".to_string());
let has_auto_now_update = f.options.contains(&"auto_now_update".to_string());
let has_required = f.options.contains(&"required".to_string());
let has_nullable = f.options.contains(&"nullable".to_string());
let is_created_at = f.name == "created_at";
let is_updated_at = f.name == "updated_at";
const V2_TYPES: &[&str] = &[
"text",
"email",
"password",
"richtext",
"textarea",
"url",
"int",
"bool",
"boolean",
"float",
"decimal",
"percent",
"date",
"time",
"datetime",
"timestamp",
"timestamp_tz",
"image",
"document",
"file",
"color",
"slug",
"uuid",
"json",
"json_binary",
"ip",
"choice",
"radio",
"bigint",
"binary",
"blob",
"inet",
"cidr",
"mac_address",
"interval",
"phone",
];
let is_v2 = V2_TYPES.contains(&f.ty.as_str());
let nullable = if has_auto_now || has_auto_now_update || has_required {
false
} else if has_nullable {
true
} else {
is_v2 };
let unique = f.options.contains(&"unique".to_string());
let is_enum_field = f.ty == "enum" || f.ty == "choice" || f.ty == "radio";
let enum_entry = if is_enum_field {
f.enum_name
.as_deref()
.and_then(|n| enum_types.iter().find(|(name, _, _)| name == n))
} else {
None
};
let col_type = if has_auto_now || has_auto_now_update {
"DateTime".to_string()
} else if is_enum_field {
match enum_entry.map(|(_, bt, _)| bt.as_str()).unwrap_or("Auto") {
"i32" => "Integer".to_string(),
"i64" => "BigInteger".to_string(),
_ => "String".to_string(), }
} else {
dsl_field_type_to_col_type(&f.ty)
};
let (enum_name, enum_string_values, enum_is_pg) = if is_enum_field {
match enum_entry {
Some((name, backing, values)) if backing != "i32" && backing != "i64" => {
(Some(name.clone()), values.clone(), false)
}
_ => (None, Vec::new(), false),
}
} else {
(None, Vec::new(), false)
};
let ignored = f.options.contains(&"readonly".to_string()) || f.name == "cache_key";
ParsedColumn {
name: f.name,
col_type,
nullable,
unique,
ignored,
created_at: has_auto_now || is_created_at,
updated_at: has_auto_now_update || is_updated_at,
has_default_now: has_auto_now
|| has_auto_now_update
|| is_created_at
|| is_updated_at,
default_value: f.default_value,
enum_name,
enum_string_values,
enum_is_pg,
renamed_from: f.renamed_from,
}
})
.collect();
let foreign_keys = model
.relations
.iter()
.filter_map(|rel| {
if let DslRelationKind::BelongsTo {
from_column,
on_delete,
on_update,
} = &rel.kind
{
Some(ParsedFk {
from_column: from_column.clone(),
to_table: pascal_to_snake(&rel.target),
to_column: "id".to_string(),
on_delete: on_delete.clone(),
on_update: on_update.clone(),
})
} else {
None
}
})
.collect();
let table = model.table.clone();
let mut parsed_indexes: Vec<ParsedIndex> = model
.unique_together
.iter()
.map(|cols| ParsedIndex {
name: format!("{}_{}_uniq", table, cols.join("_")),
columns: cols.clone(),
unique: true,
})
.collect();
for cols in &model.indexes {
parsed_indexes.push(ParsedIndex {
name: format!("idx_{}_{}", table, cols.join("_")),
columns: cols.clone(),
unique: false,
});
}
ParsedSchema {
table_name: table,
primary_key,
columns,
foreign_keys,
indexes: parsed_indexes,
}
}
struct DslVisitor {
pub schema: Option<ParsedSchema>,
pub model_name: Option<String>,
}
impl DslVisitor {
fn new() -> Self {
Self {
schema: None,
model_name: None,
}
}
}
impl<'ast> Visit<'ast> for DslVisitor {
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
if self.schema.is_some() {
return;
}
let is_model = mac
.path
.segments
.last()
.map(|s| s.ident == "model")
.unwrap_or(false);
if is_model && let Ok(model) = syn::parse2::<DslModel>(mac.tokens.clone()) {
self.model_name = Some(pascal_to_snake(&model.name));
self.schema = Some(dsl_to_parsed_schema(model));
}
syn::visit::visit_macro(self, mac);
}
}
pub fn parse_schema_from_source(source: &str) -> Option<(String, ParsedSchema)> {
let file = syn::parse_str::<syn::File>(source).ok()?;
let mut visitor = DslVisitor::new();
visitor.visit_file(&file);
let schema = visitor.schema?;
let model_name = visitor.model_name.unwrap_or_default();
Some((model_name, schema))
}