1use quote::ToTokens;
7
8use crate::error::Error;
9use crate::model::{
10 ColumnDef, DefaultValue, EnumDef, ForeignKey, OnDelete, RelationDef, Schema, SqlType, TableDef,
11};
12
13enum ParsedField {
16 Column(Box<ColumnDef>),
17 Relation(RelationDef),
18}
19
20pub fn parse_sources(sources: &[(String, String)]) -> Result<Schema, Error> {
25 let mut files = Vec::new();
26 for (name, code) in sources {
27 let file =
28 syn::parse_file(code).map_err(|e| Error::new(format!("{name}: {e}")))?;
29 files.push((name, file));
30 }
31
32 let mut enums: Vec<EnumDef> = Vec::new();
33 for (name, file) in &files {
34 for item in &file.items {
35 if let syn::Item::Enum(e) = item
36 && has_derive(&e.attrs, "DbEnum")
37 {
38 let def = enum_from_item(e).map_err(|e| Error::new(format!("{name}: {e}")))?;
39 if enums.iter().any(|x| x.rust_name == def.rust_name) {
40 return Err(Error::new(format!("{name}: duplicate DbEnum {}", def.rust_name)));
41 }
42 enums.push(def);
43 }
44 }
45 }
46 enums.sort_by(|a, b| a.rust_name.cmp(&b.rust_name));
47
48 let mut tables: Vec<TableDef> = Vec::new();
49 for (name, file) in &files {
50 for item in &file.items {
51 if let syn::Item::Struct(s) = item
52 && has_derive(&s.attrs, "Table")
53 {
54 let table = table_from_struct(s, Some(&enums))
55 .map_err(|e| Error::new(format!("{name}: {e}")))?;
56 if tables.iter().any(|t| t.name == table.name) {
57 return Err(Error::new(format!("{name}: duplicate table {}", table.name)));
58 }
59 tables.push(table);
60 }
61 }
62 }
63 tables.sort_by(|a, b| a.name.cmp(&b.name));
64
65 let by_struct: Vec<(String, String)> = tables
69 .iter()
70 .map(|t| (t.struct_name.clone(), t.name.clone()))
71 .collect();
72 let table_names: Vec<String> = tables.iter().map(|t| t.name.clone()).collect();
73 for table in &mut tables {
74 for col in &mut table.columns {
75 if let Some(fk) = &mut col.references {
76 if let Some((_, tn)) = by_struct.iter().find(|(s, _)| *s == fk.table) {
77 fk.table = tn.clone();
78 } else if !table_names.contains(&fk.table) {
79 return Err(Error::new(format!(
80 "{}.{}: references unknown table/struct `{}`",
81 table.struct_name, col.name, fk.table
82 )));
83 }
84 }
85 }
86 }
87
88 for table in &mut tables {
92 let fk_targets: Vec<(String, String)> = table
93 .columns
94 .iter()
95 .filter_map(|c| c.references.as_ref().map(|fk| (c.name.clone(), fk.table.clone())))
96 .collect();
97 for rel in &mut table.relations {
98 if let Some((_, target)) = fk_targets.iter().find(|(name, _)| *name == rel.local_column)
99 {
100 rel.target_table = target.clone();
101 }
102 }
103 }
104
105 Ok(Schema { tables, enums })
106}
107
108pub fn enum_from_item(item: &syn::ItemEnum) -> Result<EnumDef, Error> {
111 let rust_name = item.ident.to_string();
112 let mut values = Vec::new();
113 for v in &item.variants {
114 if !matches!(v.fields, syn::Fields::Unit) {
115 return Err(Error::new(format!(
116 "{rust_name}::{}: DbEnum variants must be unit variants",
117 v.ident
118 )));
119 }
120 values.push(to_snake_case(&v.ident.to_string()));
121 }
122 if values.is_empty() {
123 return Err(Error::new(format!("{rust_name}: DbEnum needs at least one variant")));
124 }
125 Ok(EnumDef { rust_name, values })
126}
127
128pub fn table_from_struct(
135 item: &syn::ItemStruct,
136 enums: Option<&[EnumDef]>,
137) -> Result<TableDef, Error> {
138 let struct_name = item.ident.to_string();
139
140 let mut table_name: Option<String> = None;
141 for attr in item.attrs.iter().filter(|a| a.path().is_ident("orm")) {
142 attr.parse_nested_meta(|meta| {
143 if meta.path.is_ident("table") {
144 let lit: syn::LitStr = meta.value()?.parse()?;
145 table_name = Some(lit.value());
146 Ok(())
147 } else {
148 Err(meta.error("unknown #[orm(...)] key on a struct; expected `table = \"...\"`"))
149 }
150 })
151 .map_err(|e| Error::new(format!("{struct_name}: {e}")))?;
152 }
153 let name = table_name.unwrap_or_else(|| pluralize(&to_snake_case(&struct_name)));
154
155 let syn::Fields::Named(fields) = &item.fields else {
156 return Err(Error::new(format!("{struct_name}: a Table struct needs named fields")));
157 };
158
159 let mut columns = Vec::new();
160 let mut relations = Vec::new();
161 for field in &fields.named {
162 match field_from_field(&struct_name, field, enums)? {
163 ParsedField::Column(c) => columns.push(*c),
164 ParsedField::Relation(r) => relations.push(r),
165 }
166 }
167
168 for rel in &mut relations {
171 let Some(col) = columns.iter().find(|c| c.name == rel.local_column) else {
172 return Err(Error::new(format!(
173 "{struct_name}.{}: relation column `{}` is not a field on this struct",
174 rel.field, rel.local_column
175 )));
176 };
177 if col.references.is_none() {
178 return Err(Error::new(format!(
179 "{struct_name}.{}: relation column `{}` has no #[orm(references(...))] — a relation \
180 must join through a foreign key",
181 rel.field, rel.local_column
182 )));
183 }
184 rel.nullable = col.nullable;
185 }
186
187 let table = TableDef { name, struct_name: struct_name.clone(), columns, relations };
188 if table.primary_key().is_empty() {
189 return Err(Error::new(format!(
190 "{struct_name}: no primary key — add an `id: i64` field or mark fields with #[orm(primary_key)]"
191 )));
192 }
193 Ok(table)
194}
195
196fn field_from_field(
199 struct_name: &str,
200 field: &syn::Field,
201 enums: Option<&[EnumDef]>,
202) -> Result<ParsedField, Error> {
203 if let Some(rel) = relation_from_field(struct_name, field)? {
204 return Ok(ParsedField::Relation(rel));
205 }
206 Ok(ParsedField::Column(Box::new(column_from_field(struct_name, field, enums)?)))
207}
208
209fn relation_from_field(
221 struct_name: &str,
222 field: &syn::Field,
223) -> Result<Option<RelationDef>, Error> {
224 let field_name = field.ident.as_ref().expect("named field").to_string();
225 let ctx = format!("{struct_name}.{field_name}");
226
227 let mut local_column: Option<String> = None;
228 let mut other_key = false;
229 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orm")) {
230 let metas = attr
231 .parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
232 .map_err(|e| Error::new(format!("{ctx}: {e}")))?;
233 for meta in &metas {
234 if meta.path().is_ident("relation") {
235 let syn::Meta::NameValue(nv) = meta else {
236 return Err(Error::new(format!("{ctx}: expected `relation = fk_column`")));
237 };
238 let syn::Expr::Path(p) = &nv.value else {
239 return Err(Error::new(format!("{ctx}: relation value must be a column name")));
240 };
241 let Some(ident) = p.path.get_ident() else {
242 return Err(Error::new(format!("{ctx}: relation value must be a column name")));
243 };
244 local_column = Some(ident.to_string());
245 } else {
246 other_key = true;
247 }
248 }
249 }
250 let Some(local_column) = local_column else {
251 return Ok(None);
252 };
253 if other_key {
254 return Err(Error::new(format!(
255 "{ctx}: a #[orm(relation = ...)] field takes no other orm keys"
256 )));
257 }
258
259 let (nullable, inner) = unwrap_option(&field.ty);
260 if !nullable {
261 return Err(Error::new(format!(
262 "{ctx}: a relation field must be `Option<Target>` (it is `None` until loaded)"
263 )));
264 }
265 let Some(target_struct) = last_segment_ident(inner) else {
266 return Err(Error::new(format!("{ctx}: relation target must be a struct type")));
267 };
268
269 Ok(Some(RelationDef {
270 field: field_name,
271 target_struct,
272 target_table: String::new(),
274 local_column,
275 nullable: false,
277 }))
278}
279
280fn column_from_field(
281 struct_name: &str,
282 field: &syn::Field,
283 enums: Option<&[EnumDef]>,
284) -> Result<ColumnDef, Error> {
285 let name = field
286 .ident
287 .as_ref()
288 .expect("named field")
289 .to_string();
290 let ctx = format!("{struct_name}.{name}");
291
292 let mut unique = false;
293 let mut json = false;
294 let mut text = false;
295 let mut index = false;
296 let mut explicit_pk = false;
297 let mut default: Option<DefaultValue> = None;
298 let mut references: Option<ForeignKey> = None;
299 let mut renamed_from: Option<String> = None;
300
301 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orm")) {
302 attr.parse_nested_meta(|meta| {
303 if meta.path.is_ident("unique") {
304 unique = true;
305 } else if meta.path.is_ident("json") {
306 json = true;
307 } else if meta.path.is_ident("text") {
308 text = true;
309 } else if meta.path.is_ident("index") {
310 index = true;
311 } else if meta.path.is_ident("primary_key") {
312 explicit_pk = true;
313 } else if meta.path.is_ident("renamed_from") {
314 let lit: syn::LitStr = meta.value()?.parse()?;
315 renamed_from = Some(lit.value());
316 } else if meta.path.is_ident("default") {
317 let expr: syn::Expr = meta.value()?.parse()?;
318 default = Some(parse_default(&expr).map_err(|m| meta.error(m))?);
319 } else if meta.path.is_ident("references") {
320 let mut target: Option<String> = None;
321 let mut on_delete: Option<OnDelete> = None;
322 meta.parse_nested_meta(|m| {
323 if m.path.is_ident("on_delete") {
324 let ident: syn::Ident = m.value()?.parse()?;
325 on_delete = Some(match ident.to_string().as_str() {
326 "cascade" => OnDelete::Cascade,
327 "set_null" => OnDelete::SetNull,
328 "restrict" => OnDelete::Restrict,
329 other => {
330 return Err(m.error(format!(
331 "unknown on_delete `{other}`; expected cascade, set_null or restrict"
332 )));
333 }
334 });
335 } else if let Some(ident) = m.path.get_ident() {
336 target = Some(ident.to_string());
337 } else {
338 return Err(m.error("expected a struct name, e.g. references(Event)"));
339 }
340 Ok(())
341 })?;
342 let Some(target) = target else {
343 return Err(meta.error("references(...) needs a target, e.g. references(Event)"));
344 };
345 references = Some(ForeignKey { table: target, column: "id".into(), on_delete });
346 } else {
347 return Err(meta.error(
348 "unknown #[orm(...)] key; expected unique, json, text, index, primary_key, default, references or renamed_from",
349 ));
350 }
351 Ok(())
352 })
353 .map_err(|e| Error::new(format!("{ctx}: {e}")))?;
354 }
355
356 let (nullable, inner) = unwrap_option(&field.ty);
357 let rust_type = inner.to_token_stream().to_string().replace(' ', "");
358
359 let (ty, is_enum, check_in) = if json {
360 (SqlType::Text, false, None)
361 } else if text {
362 if native_sql_type(inner).is_some() {
366 return Err(Error::new(format!(
367 "{ctx}: #[orm(text)] is for non-native types (this one maps natively already)"
368 )));
369 }
370 (SqlType::Text, true, None)
371 } else if let Some(t) = native_sql_type(inner) {
372 (t, false, None)
373 } else if let Some(enums) = enums {
374 let type_name = last_segment_ident(inner);
375 match enums.iter().find(|e| Some(e.rust_name.as_str()) == type_name.as_deref()) {
376 Some(e) => (SqlType::Text, true, Some(e.values.clone())),
377 None => {
378 return Err(Error::new(format!(
379 "{ctx}: unsupported type `{rust_type}` — use a native type, derive DbEnum on it, or mark the field #[orm(json)]"
380 )));
381 }
382 }
383 } else {
384 (SqlType::Text, true, None)
386 };
387
388 let mut primary_key = explicit_pk;
389 if name == "id" && ty == SqlType::Integer && !nullable {
390 primary_key = true;
391 }
392 if primary_key && nullable {
393 return Err(Error::new(format!("{ctx}: a primary key cannot be Option")));
394 }
395
396 if let Some(d) = &default {
397 validate_default(&ctx, ty, d, check_in.as_deref())?;
398 }
399 if index && (unique || primary_key) {
400 return Err(Error::new(format!(
401 "{ctx}: #[orm(index)] is redundant — unique/primary key columns are already indexed"
402 )));
403 }
404
405 Ok(ColumnDef {
406 name,
407 rust_type,
408 ty,
409 nullable,
410 primary_key,
411 unique,
412 json,
413 is_enum,
414 index,
415 default,
416 references,
417 check_in,
418 renamed_from,
419 })
420}
421
422fn parse_default(expr: &syn::Expr) -> Result<DefaultValue, String> {
423 match expr {
424 syn::Expr::Path(p) if p.path.is_ident("now") => Ok(DefaultValue::Now),
425 syn::Expr::Lit(l) => match &l.lit {
426 syn::Lit::Int(i) => Ok(DefaultValue::Int(i.base10_parse().map_err(|e| e.to_string())?)),
427 syn::Lit::Float(f) => {
428 Ok(DefaultValue::Float(f.base10_parse().map_err(|e| e.to_string())?))
429 }
430 syn::Lit::Str(s) => Ok(DefaultValue::Text(s.value())),
431 syn::Lit::Bool(b) => Ok(DefaultValue::Bool(b.value)),
432 _ => Err("unsupported default literal".into()),
433 },
434 syn::Expr::Unary(u) if matches!(u.op, syn::UnOp::Neg(_)) => match parse_default(&u.expr)? {
435 DefaultValue::Int(i) => Ok(DefaultValue::Int(-i)),
436 DefaultValue::Float(f) => Ok(DefaultValue::Float(-f)),
437 _ => Err("cannot negate this default".into()),
438 },
439 _ => Err("expected a literal or `now`, e.g. default = 0 or default = now".into()),
440 }
441}
442
443fn validate_default(
444 ctx: &str,
445 ty: SqlType,
446 d: &DefaultValue,
447 check_in: Option<&[String]>,
448) -> Result<(), Error> {
449 let ok = matches!(
450 (ty, d),
451 (SqlType::Integer, DefaultValue::Int(_))
452 | (SqlType::Real, DefaultValue::Float(_))
453 | (SqlType::Real, DefaultValue::Int(_))
454 | (SqlType::Text, DefaultValue::Text(_))
455 | (SqlType::Boolean, DefaultValue::Bool(_))
456 | (SqlType::Timestamp, DefaultValue::Now)
457 );
458 if !ok {
459 return Err(Error::new(format!(
460 "{ctx}: default {d:?} does not fit column type {ty:?}"
461 )));
462 }
463 if let (Some(values), DefaultValue::Text(s)) = (check_in, d)
464 && !values.iter().any(|v| v == s)
465 {
466 return Err(Error::new(format!(
467 "{ctx}: default '{s}' is not one of the enum values {values:?}"
468 )));
469 }
470 Ok(())
471}
472
473pub fn native_sql_type(ty: &syn::Type) -> Option<SqlType> {
476 let seg = last_segment(ty)?;
477 Some(match seg.ident.to_string().as_str() {
478 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize" => {
479 SqlType::Integer
480 }
481 "f32" | "f64" => SqlType::Real,
482 "bool" => SqlType::Boolean,
483 "String" => SqlType::Text,
484 "Uuid" => SqlType::Text,
485 "NaiveDateTime" | "DateTime" => SqlType::Timestamp,
486 "NaiveDate" | "NaiveTime" => SqlType::Text,
487 "Vec" => {
488 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
489 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
490 && last_segment(inner).is_some_and(|s| s.ident == "u8")
491 {
492 SqlType::Blob
493 } else {
494 return None;
495 }
496 }
497 _ => return None,
498 })
499}
500
501fn unwrap_option(ty: &syn::Type) -> (bool, &syn::Type) {
502 if let Some(seg) = last_segment(ty)
503 && seg.ident == "Option"
504 && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
505 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
506 {
507 return (true, inner);
508 }
509 (false, ty)
510}
511
512fn last_segment(ty: &syn::Type) -> Option<&syn::PathSegment> {
513 if let syn::Type::Path(p) = ty {
514 p.path.segments.last()
515 } else {
516 None
517 }
518}
519
520fn last_segment_ident(ty: &syn::Type) -> Option<String> {
521 last_segment(ty).map(|s| s.ident.to_string())
522}
523
524pub fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
527 attrs
528 .iter()
529 .filter(|a| a.path().is_ident("derive"))
530 .any(|a| {
531 let mut found = false;
532 let _ = a.parse_nested_meta(|meta| {
533 if meta.path.segments.last().is_some_and(|s| s.ident == name) {
534 found = true;
535 }
536 Ok(())
537 });
538 found
539 })
540}
541
542pub fn to_snake_case(s: &str) -> String {
543 let mut out = String::new();
544 for (i, ch) in s.chars().enumerate() {
545 if ch.is_uppercase() {
546 if i != 0 {
547 out.push('_');
548 }
549 out.extend(ch.to_lowercase());
550 } else {
551 out.push(ch);
552 }
553 }
554 out
555}
556
557pub fn pluralize(s: &str) -> String {
561 if let Some(stem) = s.strip_suffix('y')
562 && stem.chars().last().is_some_and(|c| !"aeiou".contains(c))
563 {
564 return format!("{stem}ies");
565 }
566 if s.ends_with('s')
567 || s.ends_with('x')
568 || s.ends_with('z')
569 || s.ends_with("ch")
570 || s.ends_with("sh")
571 {
572 return format!("{s}es");
573 }
574 format!("{s}s")
575}