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 let mut composite_uniques: Vec<Vec<String>> = Vec::new();
142 let mut composite_indexes: Vec<Vec<String>> = Vec::new();
143 for attr in item.attrs.iter().filter(|a| a.path().is_ident("orm")) {
144 attr.parse_nested_meta(|meta| {
145 if meta.path.is_ident("table") {
146 let lit: syn::LitStr = meta.value()?.parse()?;
147 table_name = Some(lit.value());
148 Ok(())
149 } else if meta.path.is_ident("unique") {
150 composite_uniques.push(parse_struct_column_list(&meta, "unique")?);
151 Ok(())
152 } else if meta.path.is_ident("index") {
153 composite_indexes.push(parse_struct_column_list(&meta, "index")?);
154 Ok(())
155 } else {
156 Err(meta.error(
157 "unknown #[orm(...)] key on a struct; expected `table = \"...\"`, \
158 `unique(col, ...)` or `index(col, ...)`",
159 ))
160 }
161 })
162 .map_err(|e| Error::new(format!("{struct_name}: {e}")))?;
163 }
164 let name = table_name.unwrap_or_else(|| pluralize(&to_snake_case(&struct_name)));
165
166 let syn::Fields::Named(fields) = &item.fields else {
167 return Err(Error::new(format!("{struct_name}: a Table struct needs named fields")));
168 };
169
170 let mut columns = Vec::new();
171 let mut relations = Vec::new();
172 for field in &fields.named {
173 match field_from_field(&struct_name, field, enums)? {
174 ParsedField::Column(c) => columns.push(*c),
175 ParsedField::Relation(r) => relations.push(r),
176 }
177 }
178
179 for rel in &mut relations {
182 let Some(col) = columns.iter().find(|c| c.name == rel.local_column) else {
183 return Err(Error::new(format!(
184 "{struct_name}.{}: relation column `{}` is not a field on this struct",
185 rel.field, rel.local_column
186 )));
187 };
188 if col.references.is_none() {
189 return Err(Error::new(format!(
190 "{struct_name}.{}: relation column `{}` has no #[orm(references(...))] — a relation \
191 must join through a foreign key",
192 rel.field, rel.local_column
193 )));
194 }
195 rel.nullable = col.nullable;
196 }
197
198 for cols in composite_uniques.iter().chain(composite_indexes.iter()) {
201 for col in cols {
202 if columns.iter().all(|c| &c.name != col) {
203 return Err(Error::new(format!(
204 "{struct_name}: unique(...)/index(...) references unknown column `{col}`"
205 )));
206 }
207 }
208 }
209
210 let table = TableDef {
211 name,
212 struct_name: struct_name.clone(),
213 columns,
214 relations,
215 composite_uniques,
216 composite_indexes,
217 };
218 if table.primary_key().is_empty() {
219 return Err(Error::new(format!(
220 "{struct_name}: no primary key — add an `id: i64` field or mark fields with #[orm(primary_key)]"
221 )));
222 }
223 Ok(table)
224}
225
226fn parse_struct_column_list(meta: &syn::meta::ParseNestedMeta, key: &str) -> syn::Result<Vec<String>> {
229 let mut cols = Vec::new();
230 meta.parse_nested_meta(|m| {
231 let Some(ident) = m.path.get_ident() else {
232 return Err(m.error("expected a column name"));
233 };
234 cols.push(ident.to_string());
235 Ok(())
236 })?;
237 if cols.is_empty() {
238 return Err(meta.error(format!("{key}(...) needs at least one column, e.g. {key}(a, b)")));
239 }
240 Ok(cols)
241}
242
243fn field_from_field(
246 struct_name: &str,
247 field: &syn::Field,
248 enums: Option<&[EnumDef]>,
249) -> Result<ParsedField, Error> {
250 if let Some(rel) = relation_from_field(struct_name, field)? {
251 return Ok(ParsedField::Relation(rel));
252 }
253 Ok(ParsedField::Column(Box::new(column_from_field(struct_name, field, enums)?)))
254}
255
256fn relation_from_field(
268 struct_name: &str,
269 field: &syn::Field,
270) -> Result<Option<RelationDef>, Error> {
271 let field_name = field.ident.as_ref().expect("named field").to_string();
272 let ctx = format!("{struct_name}.{field_name}");
273
274 let mut local_column: Option<String> = None;
275 let mut other_key = false;
276 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orm")) {
277 let metas = attr
278 .parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
279 .map_err(|e| Error::new(format!("{ctx}: {e}")))?;
280 for meta in &metas {
281 if meta.path().is_ident("relation") {
282 let syn::Meta::NameValue(nv) = meta else {
283 return Err(Error::new(format!("{ctx}: expected `relation = fk_column`")));
284 };
285 let syn::Expr::Path(p) = &nv.value else {
286 return Err(Error::new(format!("{ctx}: relation value must be a column name")));
287 };
288 let Some(ident) = p.path.get_ident() else {
289 return Err(Error::new(format!("{ctx}: relation value must be a column name")));
290 };
291 local_column = Some(ident.to_string());
292 } else {
293 other_key = true;
294 }
295 }
296 }
297 let Some(local_column) = local_column else {
298 return Ok(None);
299 };
300 if other_key {
301 return Err(Error::new(format!(
302 "{ctx}: a #[orm(relation = ...)] field takes no other orm keys"
303 )));
304 }
305
306 let (nullable, inner) = unwrap_option(&field.ty);
307 if !nullable {
308 return Err(Error::new(format!(
309 "{ctx}: a relation field must be `Option<Target>` (it is `None` until loaded)"
310 )));
311 }
312 let Some(target_struct) = last_segment_ident(inner) else {
313 return Err(Error::new(format!("{ctx}: relation target must be a struct type")));
314 };
315
316 Ok(Some(RelationDef {
317 field: field_name,
318 target_struct,
319 target_table: String::new(),
321 local_column,
322 nullable: false,
324 }))
325}
326
327fn column_from_field(
328 struct_name: &str,
329 field: &syn::Field,
330 enums: Option<&[EnumDef]>,
331) -> Result<ColumnDef, Error> {
332 let name = field
333 .ident
334 .as_ref()
335 .expect("named field")
336 .to_string();
337 let ctx = format!("{struct_name}.{name}");
338
339 let mut unique = false;
340 let mut json = false;
341 let mut text = false;
342 let mut index = false;
343 let mut explicit_pk = false;
344 let mut default: Option<DefaultValue> = None;
345 let mut references: Option<ForeignKey> = None;
346 let mut renamed_from: Option<String> = None;
347
348 for attr in field.attrs.iter().filter(|a| a.path().is_ident("orm")) {
349 attr.parse_nested_meta(|meta| {
350 if meta.path.is_ident("unique") {
351 unique = true;
352 } else if meta.path.is_ident("json") {
353 json = true;
354 } else if meta.path.is_ident("text") {
355 text = true;
356 } else if meta.path.is_ident("index") {
357 index = true;
358 } else if meta.path.is_ident("primary_key") {
359 explicit_pk = true;
360 } else if meta.path.is_ident("renamed_from") {
361 let lit: syn::LitStr = meta.value()?.parse()?;
362 renamed_from = Some(lit.value());
363 } else if meta.path.is_ident("default") {
364 let expr: syn::Expr = meta.value()?.parse()?;
365 default = Some(parse_default(&expr).map_err(|m| meta.error(m))?);
366 } else if meta.path.is_ident("references") {
367 let mut target: Option<String> = None;
368 let mut on_delete: Option<OnDelete> = None;
369 meta.parse_nested_meta(|m| {
370 if m.path.is_ident("on_delete") {
371 let ident: syn::Ident = m.value()?.parse()?;
372 on_delete = Some(match ident.to_string().as_str() {
373 "cascade" => OnDelete::Cascade,
374 "set_null" => OnDelete::SetNull,
375 "restrict" => OnDelete::Restrict,
376 other => {
377 return Err(m.error(format!(
378 "unknown on_delete `{other}`; expected cascade, set_null or restrict"
379 )));
380 }
381 });
382 } else if let Some(ident) = m.path.get_ident() {
383 target = Some(ident.to_string());
384 } else {
385 return Err(m.error("expected a struct name, e.g. references(Event)"));
386 }
387 Ok(())
388 })?;
389 let Some(target) = target else {
390 return Err(meta.error("references(...) needs a target, e.g. references(Event)"));
391 };
392 references = Some(ForeignKey { table: target, column: "id".into(), on_delete });
393 } else {
394 return Err(meta.error(
395 "unknown #[orm(...)] key; expected unique, json, text, index, primary_key, default, references or renamed_from",
396 ));
397 }
398 Ok(())
399 })
400 .map_err(|e| Error::new(format!("{ctx}: {e}")))?;
401 }
402
403 let (nullable, inner) = unwrap_option(&field.ty);
404 let rust_type = inner.to_token_stream().to_string().replace(' ', "");
405
406 let (ty, is_enum, check_in) = if json {
407 (SqlType::Text, false, None)
408 } else if text {
409 if native_sql_type(inner).is_some() {
413 return Err(Error::new(format!(
414 "{ctx}: #[orm(text)] is for non-native types (this one maps natively already)"
415 )));
416 }
417 (SqlType::Text, true, None)
418 } else if let Some(t) = native_sql_type(inner) {
419 (t, false, None)
420 } else if let Some(enums) = enums {
421 let type_name = last_segment_ident(inner);
422 match enums.iter().find(|e| Some(e.rust_name.as_str()) == type_name.as_deref()) {
423 Some(e) => (SqlType::Text, true, Some(e.values.clone())),
424 None => {
425 return Err(Error::new(format!(
426 "{ctx}: unsupported type `{rust_type}` — use a native type, derive DbEnum on it, or mark the field #[orm(json)]"
427 )));
428 }
429 }
430 } else {
431 (SqlType::Text, true, None)
433 };
434
435 let mut primary_key = explicit_pk;
436 if name == "id" && ty == SqlType::Integer && !nullable {
437 primary_key = true;
438 }
439 if primary_key && nullable {
440 return Err(Error::new(format!("{ctx}: a primary key cannot be Option")));
441 }
442
443 if let Some(d) = &default {
444 validate_default(&ctx, ty, d, check_in.as_deref())?;
445 }
446 if index && (unique || primary_key) {
447 return Err(Error::new(format!(
448 "{ctx}: #[orm(index)] is redundant — unique/primary key columns are already indexed"
449 )));
450 }
451
452 Ok(ColumnDef {
453 name,
454 rust_type,
455 ty,
456 nullable,
457 primary_key,
458 unique,
459 json,
460 is_enum,
461 index,
462 default,
463 references,
464 check_in,
465 renamed_from,
466 })
467}
468
469fn parse_default(expr: &syn::Expr) -> Result<DefaultValue, String> {
470 match expr {
471 syn::Expr::Path(p) if p.path.is_ident("now") => Ok(DefaultValue::Now),
472 syn::Expr::Lit(l) => match &l.lit {
473 syn::Lit::Int(i) => Ok(DefaultValue::Int(i.base10_parse().map_err(|e| e.to_string())?)),
474 syn::Lit::Float(f) => {
475 Ok(DefaultValue::Float(f.base10_parse().map_err(|e| e.to_string())?))
476 }
477 syn::Lit::Str(s) => Ok(DefaultValue::Text(s.value())),
478 syn::Lit::Bool(b) => Ok(DefaultValue::Bool(b.value)),
479 _ => Err("unsupported default literal".into()),
480 },
481 syn::Expr::Unary(u) if matches!(u.op, syn::UnOp::Neg(_)) => match parse_default(&u.expr)? {
482 DefaultValue::Int(i) => Ok(DefaultValue::Int(-i)),
483 DefaultValue::Float(f) => Ok(DefaultValue::Float(-f)),
484 _ => Err("cannot negate this default".into()),
485 },
486 _ => Err("expected a literal or `now`, e.g. default = 0 or default = now".into()),
487 }
488}
489
490fn validate_default(
491 ctx: &str,
492 ty: SqlType,
493 d: &DefaultValue,
494 check_in: Option<&[String]>,
495) -> Result<(), Error> {
496 let ok = matches!(
497 (ty, d),
498 (SqlType::Integer, DefaultValue::Int(_))
499 | (SqlType::Real, DefaultValue::Float(_))
500 | (SqlType::Real, DefaultValue::Int(_))
501 | (SqlType::Text, DefaultValue::Text(_))
502 | (SqlType::Boolean, DefaultValue::Bool(_))
503 | (SqlType::Timestamp, DefaultValue::Now)
504 );
505 if !ok {
506 return Err(Error::new(format!(
507 "{ctx}: default {d:?} does not fit column type {ty:?}"
508 )));
509 }
510 if let (Some(values), DefaultValue::Text(s)) = (check_in, d)
511 && !values.iter().any(|v| v == s)
512 {
513 return Err(Error::new(format!(
514 "{ctx}: default '{s}' is not one of the enum values {values:?}"
515 )));
516 }
517 Ok(())
518}
519
520pub fn native_sql_type(ty: &syn::Type) -> Option<SqlType> {
523 let seg = last_segment(ty)?;
524 Some(match seg.ident.to_string().as_str() {
525 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize" => {
526 SqlType::Integer
527 }
528 "f32" | "f64" => SqlType::Real,
529 "bool" => SqlType::Boolean,
530 "String" => SqlType::Text,
531 "Uuid" => SqlType::Text,
532 "NaiveDateTime" | "DateTime" => SqlType::Timestamp,
533 "NaiveDate" | "NaiveTime" => SqlType::Text,
534 "Vec" => {
535 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
536 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
537 && last_segment(inner).is_some_and(|s| s.ident == "u8")
538 {
539 SqlType::Blob
540 } else {
541 return None;
542 }
543 }
544 _ => return None,
545 })
546}
547
548fn unwrap_option(ty: &syn::Type) -> (bool, &syn::Type) {
549 if let Some(seg) = last_segment(ty)
550 && seg.ident == "Option"
551 && let syn::PathArguments::AngleBracketed(args) = &seg.arguments
552 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
553 {
554 return (true, inner);
555 }
556 (false, ty)
557}
558
559fn last_segment(ty: &syn::Type) -> Option<&syn::PathSegment> {
560 if let syn::Type::Path(p) = ty {
561 p.path.segments.last()
562 } else {
563 None
564 }
565}
566
567fn last_segment_ident(ty: &syn::Type) -> Option<String> {
568 last_segment(ty).map(|s| s.ident.to_string())
569}
570
571pub fn has_derive(attrs: &[syn::Attribute], name: &str) -> bool {
574 attrs
575 .iter()
576 .filter(|a| a.path().is_ident("derive"))
577 .any(|a| {
578 let mut found = false;
579 let _ = a.parse_nested_meta(|meta| {
580 if meta.path.segments.last().is_some_and(|s| s.ident == name) {
581 found = true;
582 }
583 Ok(())
584 });
585 found
586 })
587}
588
589pub fn to_snake_case(s: &str) -> String {
590 let mut out = String::new();
591 for (i, ch) in s.chars().enumerate() {
592 if ch.is_uppercase() {
593 if i != 0 {
594 out.push('_');
595 }
596 out.extend(ch.to_lowercase());
597 } else {
598 out.push(ch);
599 }
600 }
601 out
602}
603
604pub fn pluralize(s: &str) -> String {
608 if let Some(stem) = s.strip_suffix('y')
609 && stem.chars().last().is_some_and(|c| !"aeiou".contains(c))
610 {
611 return format!("{stem}ies");
612 }
613 if s.ends_with('s')
614 || s.ends_with('x')
615 || s.ends_with('z')
616 || s.ends_with("ch")
617 || s.ends_with("sh")
618 {
619 return format!("{s}es");
620 }
621 format!("{s}s")
622}