1use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20 let input = parse_macro_input!(input as DeriveInput);
21 expand(&input)
22 .unwrap_or_else(syn::Error::into_compile_error)
23 .into()
24}
25
26#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60 let input = parse_macro_input!(input as DeriveInput);
61 expand_viewset(&input)
62 .unwrap_or_else(syn::Error::into_compile_error)
63 .into()
64}
65
66#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95 let input = parse_macro_input!(input as DeriveInput);
96 expand_form(&input)
97 .unwrap_or_else(syn::Error::into_compile_error)
98 .into()
99}
100
101#[proc_macro_derive(Serializer, attributes(serializer))]
121pub fn derive_serializer(input: TokenStream) -> TokenStream {
122 let input = parse_macro_input!(input as DeriveInput);
123 expand_serializer(&input)
124 .unwrap_or_else(syn::Error::into_compile_error)
125 .into()
126}
127
128#[proc_macro]
163pub fn embed_migrations(input: TokenStream) -> TokenStream {
164 expand_embed_migrations(input.into())
165 .unwrap_or_else(syn::Error::into_compile_error)
166 .into()
167}
168
169#[allow(non_snake_case)]
219#[proc_macro]
220pub fn Q(input: TokenStream) -> TokenStream {
221 expand_q(input.into())
222 .unwrap_or_else(syn::Error::into_compile_error)
223 .into()
224}
225
226#[proc_macro_attribute]
249pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
250 expand_main(args.into(), item.into())
251 .unwrap_or_else(syn::Error::into_compile_error)
252 .into()
253}
254
255fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
256 let mut input: syn::ItemFn = syn::parse2(item)?;
257 if input.sig.asyncness.is_none() {
258 return Err(syn::Error::new(
259 input.sig.ident.span(),
260 "`#[rustango::main]` must wrap an `async fn`",
261 ));
262 }
263
264 let flavor = parse_flavor(&args);
276 let builder_call = match flavor {
277 Flavor::CurrentThread => quote! {
278 ::rustango::__private_runtime::tokio::runtime::Builder::new_current_thread()
279 },
280 Flavor::MultiThread => quote! {
281 ::rustango::__private_runtime::tokio::runtime::Builder::new_multi_thread()
282 },
283 };
284
285 let user_body = input.block.clone();
288 input.sig.asyncness = None;
289 input.block = syn::parse2(quote! {{
290 {
291 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
292 let _ = tracing_subscriber::fmt()
295 .with_env_filter(
296 EnvFilter::try_from_default_env()
297 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
298 )
299 .try_init();
300 }
301 let __rt = #builder_call
302 .enable_all()
303 .build()
304 .expect("failed to build tokio runtime");
305 __rt.block_on(async move #user_body)
306 }})?;
307
308 Ok(quote! {
309 #input
310 })
311}
312
313enum Flavor {
314 MultiThread,
315 CurrentThread,
316}
317
318fn parse_flavor(args: &TokenStream2) -> Flavor {
319 let s = args.to_string();
323 if s.contains("current_thread") {
324 Flavor::CurrentThread
325 } else {
326 Flavor::MultiThread
327 }
328}
329
330struct QInput {
332 base_path: syn::Path,
333 field: syn::Ident,
334 value: syn::Expr,
335}
336
337impl syn::parse::Parse for QInput {
338 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
339 let base_path: syn::Path = input.parse()?;
340 input.parse::<syn::Token![.]>()?;
341 let field: syn::Ident = input.parse()?;
342 input.parse::<syn::Token![=]>()?;
343 let value: syn::Expr = input.parse()?;
344 Ok(QInput {
345 base_path,
346 field,
347 value,
348 })
349 }
350}
351
352fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
353 let q: QInput = syn::parse2(input)?;
354 let field_str = q.field.to_string();
355 let field_span = q.field.span();
356 let (base, suffix) = match field_str.find("__") {
357 Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
358 None => (field_str.as_str(), ""),
359 };
360 if base.is_empty() {
361 return Err(syn::Error::new(
362 field_span,
363 "Q!(): field name is empty before `__` suffix",
364 ));
365 }
366 let base_ident = syn::Ident::new(base, field_span);
367 let value = &q.value;
368 let path = &q.base_path;
369
370 let expanded = match suffix {
375 "" | "exact" => quote! {
376 ::rustango::core::Column::eq(#path::#base_ident, #value)
377 },
378 "ne" => quote! {
379 ::rustango::core::Column::ne(#path::#base_ident, #value)
380 },
381 "gt" => quote! {
382 ::rustango::core::Column::gt(#path::#base_ident, #value)
383 },
384 "gte" => quote! {
385 ::rustango::core::Column::gte(#path::#base_ident, #value)
386 },
387 "lt" => quote! {
388 ::rustango::core::Column::lt(#path::#base_ident, #value)
389 },
390 "lte" => quote! {
391 ::rustango::core::Column::lte(#path::#base_ident, #value)
392 },
393 "iexact" => quote! {
394 ::rustango::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
399 },
400 "contains" => quote! {
401 ::rustango::core::Column::like(
402 #path::#base_ident,
403 ::std::format!("%{}%", #value),
404 )
405 },
406 "icontains" => quote! {
407 ::rustango::core::Column::ilike(
408 #path::#base_ident,
409 ::std::format!("%{}%", #value),
410 )
411 },
412 "startswith" => quote! {
413 ::rustango::core::Column::like(
414 #path::#base_ident,
415 ::std::format!("{}%", #value),
416 )
417 },
418 "istartswith" => quote! {
419 ::rustango::core::Column::ilike(
420 #path::#base_ident,
421 ::std::format!("{}%", #value),
422 )
423 },
424 "endswith" => quote! {
425 ::rustango::core::Column::like(
426 #path::#base_ident,
427 ::std::format!("%{}", #value),
428 )
429 },
430 "iendswith" => quote! {
431 ::rustango::core::Column::ilike(
432 #path::#base_ident,
433 ::std::format!("%{}", #value),
434 )
435 },
436 "in" => quote! {
437 ::rustango::core::Column::is_in(#path::#base_ident, #value)
438 },
439 "not_in" => quote! {
440 ::rustango::core::Column::not_in(#path::#base_ident, #value)
441 },
442 "isnull" => {
443 let b = match value {
446 syn::Expr::Lit(syn::ExprLit {
447 lit: syn::Lit::Bool(b),
448 ..
449 }) => b.value(),
450 _ => {
451 return Err(syn::Error::new_spanned(
452 value,
453 "Q!(): `__isnull` requires a `true` or `false` literal",
454 ));
455 }
456 };
457 if b {
458 quote! { ::rustango::core::Column::is_null(#path::#base_ident) }
459 } else {
460 quote! { ::rustango::core::Column::is_not_null(#path::#base_ident) }
461 }
462 }
463 "between" => {
464 let tuple = match value {
466 syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
467 _ => {
468 return Err(syn::Error::new_spanned(
469 value,
470 "Q!(): `__between` requires a tuple literal `(lo, hi)`",
471 ));
472 }
473 };
474 let lo = &tuple.elems[0];
475 let hi = &tuple.elems[1];
476 quote! { ::rustango::core::Column::between(#path::#base_ident, #lo, #hi) }
477 }
478 "regex" => quote! {
479 ::rustango::core::Column::regex(#path::#base_ident, #value)
480 },
481 "iregex" => quote! {
482 ::rustango::core::Column::iregex(#path::#base_ident, #value)
483 },
484 _ => {
485 return Err(syn::Error::new(
486 field_span,
487 format!(
488 "Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
489 suffix
490 ),
491 ));
492 }
493 };
494 Ok(expanded)
495}
496
497fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
498 let path_str = if input.is_empty() {
500 "./migrations".to_string()
501 } else {
502 let lit: LitStr = syn::parse2(input)?;
503 lit.value()
504 };
505
506 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
507 syn::Error::new(
508 proc_macro2::Span::call_site(),
509 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
510 )
511 })?;
512 let abs = std::path::Path::new(&manifest).join(&path_str);
513
514 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
515 if abs.is_dir() {
516 let read = std::fs::read_dir(&abs).map_err(|e| {
517 syn::Error::new(
518 proc_macro2::Span::call_site(),
519 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
520 )
521 })?;
522 for entry in read.flatten() {
523 let path = entry.path();
524 if !path.is_file() {
525 continue;
526 }
527 if path.extension().and_then(|s| s.to_str()) != Some("json") {
528 continue;
529 }
530 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
531 continue;
532 };
533 entries.push((stem.to_owned(), path));
534 }
535 }
536 entries.sort_by(|a, b| a.0.cmp(&b.0));
537
538 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
551 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
552 for (stem, path) in &entries {
553 let raw = std::fs::read_to_string(path).map_err(|e| {
554 syn::Error::new(
555 proc_macro2::Span::call_site(),
556 format!(
557 "embed_migrations!: cannot read {} for chain validation: {e}",
558 path.display()
559 ),
560 )
561 })?;
562 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
563 syn::Error::new(
564 proc_macro2::Span::call_site(),
565 format!(
566 "embed_migrations!: {} is not valid JSON: {e}",
567 path.display()
568 ),
569 )
570 })?;
571 let name = json
572 .get("name")
573 .and_then(|v| v.as_str())
574 .ok_or_else(|| {
575 syn::Error::new(
576 proc_macro2::Span::call_site(),
577 format!(
578 "embed_migrations!: {} is missing the `name` field",
579 path.display()
580 ),
581 )
582 })?
583 .to_owned();
584 if name != *stem {
585 return Err(syn::Error::new(
586 proc_macro2::Span::call_site(),
587 format!(
588 "embed_migrations!: file stem `{stem}` does not match the migration's \
589 `name` field `{name}` — rename the file or fix the JSON",
590 ),
591 ));
592 }
593 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
594 chain_names.push(name.clone());
595 prev_refs.push((name, prev));
596 }
597
598 let name_set: std::collections::HashSet<&str> =
599 chain_names.iter().map(String::as_str).collect();
600 for (name, prev) in &prev_refs {
601 if let Some(p) = prev {
602 if !name_set.contains(p.as_str()) {
603 return Err(syn::Error::new(
604 proc_macro2::Span::call_site(),
605 format!(
606 "embed_migrations!: broken migration chain — `{name}` declares \
607 prev=`{p}` but no migration with that name exists in {}",
608 abs.display()
609 ),
610 ));
611 }
612 }
613 }
614
615 let pairs: Vec<TokenStream2> = entries
616 .iter()
617 .map(|(name, path)| {
618 let path_lit = path.display().to_string();
619 quote! { (#name, ::core::include_str!(#path_lit)) }
620 })
621 .collect();
622
623 Ok(quote! {
624 {
625 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
626 __RUSTANGO_EMBEDDED
627 }
628 })
629}
630
631fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
632 let struct_name = &input.ident;
633
634 let Data::Struct(data) = &input.data else {
635 return Err(syn::Error::new_spanned(
636 struct_name,
637 "Model can only be derived on structs",
638 ));
639 };
640 let Fields::Named(named) = &data.fields else {
641 return Err(syn::Error::new_spanned(
642 struct_name,
643 "Model requires a struct with named fields",
644 ));
645 };
646
647 let container = parse_container_attrs(input)?;
648 let table = container
649 .table
650 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
651 let model_name = struct_name.to_string();
652
653 let collected = collect_fields(named, &table)?;
654
655 if let Some((ref display, span)) = container.display {
657 if !collected.field_names.iter().any(|n| n == display) {
658 return Err(syn::Error::new(
659 span,
660 format!("`display = \"{display}\"` does not match any field on this struct"),
661 ));
662 }
663 }
664 let display = container.display.map(|(name, _)| name);
665 let app_label = container.app.clone();
666
667 if let Some(admin) = &container.admin {
677 for (label, list) in [
678 ("search_fields", &admin.search_fields),
679 ("readonly_fields", &admin.readonly_fields),
680 ("list_filter", &admin.list_filter),
681 ] {
682 if let Some((names, span)) = list {
683 for name in names {
684 if !collected.field_names.iter().any(|n| n == name) {
685 return Err(syn::Error::new(
686 *span,
687 format!(
688 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
689 ),
690 ));
691 }
692 }
693 }
694 }
695 if let Some((pairs, span)) = &admin.ordering {
696 for (name, _) in pairs {
697 if !collected.field_names.iter().any(|n| n == name) {
698 return Err(syn::Error::new(
699 *span,
700 format!(
701 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
702 ),
703 ));
704 }
705 }
706 }
707 if let Some((groups, span)) = &admin.fieldsets {
708 for (_, fields) in groups {
709 for name in fields {
710 if !collected.field_names.iter().any(|n| n == name) {
711 return Err(syn::Error::new(
712 *span,
713 format!(
714 "`fieldsets`: \"{name}\" is not a declared field on this struct"
715 ),
716 ));
717 }
718 }
719 }
720 }
721 }
722 if let Some(audit) = &container.audit {
723 if let Some((names, span)) = &audit.track {
724 for name in names {
725 if !collected.field_names.iter().any(|n| n == name) {
726 return Err(syn::Error::new(
727 *span,
728 format!(
729 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
730 ),
731 ));
732 }
733 }
734 }
735 }
736
737 for (col, _desc, span) in &container.default_order {
741 if !collected.field_names.iter().any(|n| n == col) {
742 return Err(syn::Error::new(
743 *span,
744 format!(
745 "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
746 ),
747 ));
748 }
749 }
750
751 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
754 audit
755 .track
756 .as_ref()
757 .map(|(names, _)| names.clone())
758 .unwrap_or_default()
759 });
760
761 let mut all_indexes: Vec<IndexAttr> = container.indexes;
763 for field in &named.named {
764 let ident = field.ident.as_ref().expect("named");
765 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
768 if fa.index {
769 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
770 let auto_name = if fa.index_unique {
771 format!("{table}_{col_name}_uq_idx")
772 } else {
773 format!("{table}_{col_name}_idx")
774 };
775 all_indexes.push(IndexAttr {
776 name: fa.index_name.or(Some(auto_name)),
777 columns: vec![col_name],
778 unique: fa.index_unique,
779 method: fa.index_method,
780 where_clause: None,
781 });
782 }
783 }
784 }
785
786 let model_impl = model_impl_tokens(
787 struct_name,
788 &model_name,
789 &table,
790 display.as_deref(),
791 app_label.as_deref(),
792 container.admin.as_ref(),
793 &container.default_order,
794 &collected.field_schemas,
795 collected.soft_delete_column.as_deref(),
796 container.permissions,
797 audit_track_names.as_deref(),
798 &container.m2m,
799 &all_indexes,
800 &container.checks,
801 &container.composite_fks,
802 &container.generic_fks,
803 container.scope.as_deref(),
804 container.is_view,
805 container.verbose_name.as_deref(),
806 container.verbose_name_plural.as_deref(),
807 );
808 let module_ident = column_module_ident(struct_name);
809 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
810 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
811 let track_set: Option<std::collections::HashSet<&str>> = audit
812 .track
813 .as_ref()
814 .map(|(names, _)| names.iter().map(String::as_str).collect());
815 collected
816 .column_entries
817 .iter()
818 .filter(|c| {
819 track_set
820 .as_ref()
821 .map_or(true, |s| s.contains(c.name.as_str()))
822 })
823 .collect()
824 });
825 let inherent_impl = inherent_impl_tokens(
826 struct_name,
827 &collected,
828 collected.primary_key.as_ref(),
829 &column_consts,
830 audited_fields.as_deref(),
831 &all_indexes,
832 &container.manager_fns,
833 );
834 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
835 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
836 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
837 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
838 let generic_fk_accessors = generic_fk_accessor_tokens(
839 struct_name,
840 &container.generic_fks,
841 &collected.column_entries,
842 );
843
844 let manager_trait = container.manager_ext.as_ref().map(|name| {
850 let model_name_str = struct_name.to_string();
851 let doc = format!(
852 "Custom-Manager extension trait for [`{model_name_str}`]. \
853 Generated by `#[rustango(manager(ext = ...))]`. Add methods \
854 via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
855 );
856 quote! {
857 #[doc = #doc]
858 pub trait #name: ::core::marker::Sized {}
859 }
860 });
861
862 Ok(quote! {
863 #model_impl
864 #inherent_impl
865 #from_row_impl
866 #column_module
867 #reverse_helpers
868 #m2m_accessors
869 #generic_fk_accessors
870 #manager_trait
871
872 ::rustango::core::inventory::submit! {
873 ::rustango::core::ModelEntry {
874 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
875 module_path: ::core::module_path!(),
880 }
881 }
882 })
883}
884
885fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
896 let arms = fk_relations.iter().map(|rel| {
897 let parent_ty = &rel.parent_type;
898 let fk_col = rel.fk_column.as_str();
899 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
902 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
903 let assign = if rel.nullable {
904 quote! {
905 self.#field_ident = ::core::option::Option::Some(
906 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
907 );
908 }
909 } else {
910 quote! {
911 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
912 }
913 };
914 quote! {
915 #fk_col => {
916 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
917 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
924 ::rustango::core::SqlValue::#variant_ident(v) => v,
925 _other => {
926 ::core::debug_assert!(
927 false,
928 "rustango macro bug: load_related on FK `{}` expected \
929 SqlValue::{} from parent's __rustango_pk_value but got \
930 {:?} — file a bug at https://github.com/ujeenet/rustango",
931 #fk_col,
932 ::core::stringify!(#variant_ident),
933 _other,
934 );
935 #default_expr
936 }
937 };
938 #assign
939 ::core::result::Result::Ok(true)
940 }
941 }
942 });
943 quote! {
944 #[cfg(feature = "postgres")]
945 impl ::rustango::sql::LoadRelated for #struct_name {
946 #[allow(unused_variables)]
947 fn __rustango_load_related(
948 &mut self,
949 row: &::rustango::sql::sqlx::postgres::PgRow,
950 field_name: &str,
951 alias: &str,
952 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
953 match field_name {
954 #( #arms )*
955 _ => ::core::result::Result::Ok(false),
956 }
957 }
958 }
959 }
960}
961
962fn load_related_impl_my_tokens(
970 struct_name: &syn::Ident,
971 fk_relations: &[FkRelation],
972) -> TokenStream2 {
973 let arms = fk_relations.iter().map(|rel| {
974 let parent_ty = &rel.parent_type;
975 let fk_col = rel.fk_column.as_str();
976 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
977 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
978 let assign = if rel.nullable {
979 quote! {
980 __self.#field_ident = ::core::option::Option::Some(
981 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
982 );
983 }
984 } else {
985 quote! {
986 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
987 }
988 };
989 quote! {
994 #fk_col => {
995 let _parent: #parent_ty =
996 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
997 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1000 ::rustango::core::SqlValue::#variant_ident(v) => v,
1001 _other => {
1002 ::core::debug_assert!(
1003 false,
1004 "rustango macro bug: load_related on FK `{}` expected \
1005 SqlValue::{} from parent's __rustango_pk_value but got \
1006 {:?} — file a bug at https://github.com/ujeenet/rustango",
1007 #fk_col,
1008 ::core::stringify!(#variant_ident),
1009 _other,
1010 );
1011 #default_expr
1012 }
1013 };
1014 #assign
1015 ::core::result::Result::Ok(true)
1016 }
1017 }
1018 });
1019 quote! {
1020 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
1021 #( #arms )*
1022 });
1023 }
1024}
1025
1026fn load_related_impl_sqlite_tokens(
1030 struct_name: &syn::Ident,
1031 fk_relations: &[FkRelation],
1032) -> TokenStream2 {
1033 let arms = fk_relations.iter().map(|rel| {
1034 let parent_ty = &rel.parent_type;
1035 let fk_col = rel.fk_column.as_str();
1036 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1037 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1038 let assign = if rel.nullable {
1039 quote! {
1040 __self.#field_ident = ::core::option::Option::Some(
1041 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
1042 );
1043 }
1044 } else {
1045 quote! {
1046 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
1047 }
1048 };
1049 quote! {
1050 #fk_col => {
1051 let _parent: #parent_ty =
1052 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1053 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1054 ::rustango::core::SqlValue::#variant_ident(v) => v,
1055 _other => {
1056 ::core::debug_assert!(
1057 false,
1058 "rustango macro bug: load_related on FK `{}` expected \
1059 SqlValue::{} from parent's __rustango_pk_value but got \
1060 {:?} — file a bug at https://github.com/ujeenet/rustango",
1061 #fk_col,
1062 ::core::stringify!(#variant_ident),
1063 _other,
1064 );
1065 #default_expr
1066 }
1067 };
1068 #assign
1069 ::core::result::Result::Ok(true)
1070 }
1071 }
1072 });
1073 quote! {
1074 ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
1075 #( #arms )*
1076 });
1077 }
1078}
1079
1080fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1088 let arms = fk_relations.iter().map(|rel| {
1089 let fk_col = rel.fk_column.as_str();
1090 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1091 if rel.pk_kind == DetectedKind::I64 {
1092 if rel.nullable {
1098 quote! {
1099 #fk_col => self.#field_ident
1100 .as_ref()
1101 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
1102 }
1103 } else {
1104 quote! {
1105 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1106 }
1107 }
1108 } else {
1109 quote! {
1117 #fk_col => ::core::option::Option::None,
1118 }
1119 }
1120 });
1121 let value_arms = fk_relations.iter().map(|rel| {
1127 let fk_col = rel.fk_column.as_str();
1128 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1129 if rel.nullable {
1130 quote! {
1131 #fk_col => self.#field_ident
1132 .as_ref()
1133 .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
1134 ::rustango::sql::ForeignKey::pk(fk)
1135 )),
1136 }
1137 } else {
1138 quote! {
1139 #fk_col => ::core::option::Option::Some(
1140 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1141 self.#field_ident.pk()
1142 )
1143 ),
1144 }
1145 }
1146 });
1147 quote! {
1148 impl ::rustango::sql::FkPkAccess for #struct_name {
1149 #[allow(unused_variables)]
1150 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1151 match field_name {
1152 #( #arms )*
1153 _ => ::core::option::Option::None,
1154 }
1155 }
1156 #[allow(unused_variables)]
1157 fn __rustango_fk_pk_value(
1158 &self,
1159 field_name: &str,
1160 ) -> ::core::option::Option<::rustango::core::SqlValue> {
1161 match field_name {
1162 #( #value_arms )*
1163 _ => ::core::option::Option::None,
1164 }
1165 }
1166 }
1167 }
1168}
1169
1170fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1184 if fk_relations.is_empty() {
1185 return TokenStream2::new();
1186 }
1187 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
1191 let method_ident = syn::Ident::new(&suffix, child_ident.span());
1192 let impls = fk_relations.iter().map(|rel| {
1193 let parent_ty = &rel.parent_type;
1194 let fk_col = rel.fk_column.as_str();
1195 let doc = format!(
1196 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1197 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1198 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1199 further `{child_ident}::objects()` filters via direct queryset use."
1200 );
1201 quote! {
1202 #[cfg(feature = "postgres")]
1203 impl #parent_ty {
1204 #[doc = #doc]
1205 pub async fn #method_ident<'_c, _E>(
1210 &self,
1211 _executor: _E,
1212 ) -> ::core::result::Result<
1213 ::std::vec::Vec<#child_ident>,
1214 ::rustango::sql::ExecError,
1215 >
1216 where
1217 _E: ::rustango::sql::sqlx::Executor<
1218 '_c,
1219 Database = ::rustango::sql::sqlx::Postgres,
1220 >,
1221 {
1222 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
1223 ::rustango::query::QuerySet::<#child_ident>::new()
1224 .filter_op(#fk_col, ::rustango::core::Op::Eq, _pk)
1225 .fetch_on(_executor)
1226 .await
1227 }
1228 }
1229 }
1230 });
1231 quote! { #( #impls )* }
1232}
1233
1234fn generic_fk_accessor_tokens(
1248 struct_name: &syn::Ident,
1249 generic_fks: &[GenericFkAttr],
1250 column_entries: &[ColumnEntry],
1251) -> TokenStream2 {
1252 if generic_fks.is_empty() {
1253 return TokenStream2::new();
1254 }
1255 let methods = generic_fks.iter().filter_map(|gfk| {
1256 let ct_ident = column_entries
1262 .iter()
1263 .find(|c| c.column == gfk.ct_column)
1264 .map(|c| c.ident.clone())?;
1265 let pk_ident = column_entries
1266 .iter()
1267 .find(|c| c.column == gfk.pk_column)
1268 .map(|c| c.ident.clone())?;
1269
1270 let accessor_ident =
1271 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1272 let setter_ident =
1273 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1274 let name_literal = gfk.name.as_str();
1275
1276 Some(quote! {
1277 #[doc = concat!(
1278 "Resolve the polymorphic `",
1279 #name_literal,
1280 "` relation. Reads `self.",
1281 stringify!(#ct_ident),
1282 "` + `self.",
1283 stringify!(#pk_ident),
1284 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1285 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1286 #name_literal,
1287 "\", ...))]`."
1288 )]
1289 pub async fn #accessor_ident(
1290 &self,
1291 pool: &::rustango::sql::Pool,
1292 ) -> ::core::result::Result<
1293 ::core::option::Option<::serde_json::Value>,
1294 ::rustango::sql::ExecError,
1295 > {
1296 let gfk = ::rustango::contenttypes::GenericForeignKey::new(
1297 self.#ct_ident as i64,
1298 self.#pk_ident as i64,
1299 );
1300 gfk.get_object(pool).await
1301 }
1302
1303 #[doc = concat!(
1304 "Set the polymorphic `",
1305 #name_literal,
1306 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1307 stringify!(#ct_ident),
1308 "` and `self.",
1309 stringify!(#pk_ident),
1310 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1311 #name_literal,
1312 "\", ...))]`."
1313 )]
1314 pub async fn #setter_ident<T: ::rustango::core::Model>(
1315 &mut self,
1316 pool: &::rustango::sql::Pool,
1317 target_pk: i64,
1318 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1319 let gfk = ::rustango::contenttypes::GenericForeignKey::for_target::<T>(
1320 pool,
1321 target_pk,
1322 ).await?;
1323 self.#ct_ident = gfk.content_type_id as _;
1324 self.#pk_ident = gfk.object_pk as _;
1325 ::core::result::Result::Ok(())
1326 }
1327 })
1328 });
1329 quote! {
1330 impl #struct_name {
1331 #( #methods )*
1332 }
1333 }
1334}
1335
1336fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1337 if m2m_relations.is_empty() {
1338 return TokenStream2::new();
1339 }
1340 let methods = m2m_relations.iter().map(|rel| {
1341 let method_name = format!("{}_m2m", rel.name);
1342 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1343 let through = rel.through.as_str();
1344 let src_col = rel.src.as_str();
1345 let dst_col = rel.dst.as_str();
1346 quote! {
1347 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
1348 ::rustango::sql::M2MManager {
1349 src_pk: self.__rustango_pk_value(),
1350 through: #through,
1351 src_col: #src_col,
1352 dst_col: #dst_col,
1353 }
1354 }
1355 }
1356 });
1357 quote! {
1358 impl #struct_name {
1359 #( #methods )*
1360 }
1361 }
1362}
1363
1364struct ColumnEntry {
1365 ident: syn::Ident,
1368 value_ty: Type,
1370 name: String,
1372 column: String,
1374 field_type_tokens: TokenStream2,
1376}
1377
1378struct CollectedFields {
1379 field_schemas: Vec<TokenStream2>,
1380 from_row_inits: Vec<TokenStream2>,
1381 from_aliased_row_inits: Vec<TokenStream2>,
1385 insert_columns: Vec<TokenStream2>,
1388 insert_values: Vec<TokenStream2>,
1391 insert_pushes: Vec<TokenStream2>,
1396 returning_cols: Vec<TokenStream2>,
1399 auto_assigns: Vec<TokenStream2>,
1402 auto_field_idents: Vec<(syn::Ident, String)>,
1406 first_auto_value_ty: Option<Type>,
1409 bulk_pushes_no_auto: Vec<TokenStream2>,
1413 bulk_pushes_all: Vec<TokenStream2>,
1417 bulk_columns_no_auto: Vec<TokenStream2>,
1420 bulk_columns_all: Vec<TokenStream2>,
1423 bulk_auto_uniformity: Vec<TokenStream2>,
1427 first_auto_ident: Option<syn::Ident>,
1430 has_auto: bool,
1432 pk_is_auto: bool,
1436 update_assignments: Vec<TokenStream2>,
1439 upsert_update_columns: Vec<TokenStream2>,
1442 primary_key: Option<(syn::Ident, String)>,
1443 column_entries: Vec<ColumnEntry>,
1444 field_names: Vec<String>,
1447 fk_relations: Vec<FkRelation>,
1452 soft_delete_column: Option<String>,
1457}
1458
1459#[derive(Clone)]
1460struct FkRelation {
1461 parent_type: Type,
1464 fk_column: String,
1467 pk_kind: DetectedKind,
1472 nullable: bool,
1477}
1478
1479fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1480 let cap = named.named.len();
1481 let mut out = CollectedFields {
1482 field_schemas: Vec::with_capacity(cap),
1483 from_row_inits: Vec::with_capacity(cap),
1484 from_aliased_row_inits: Vec::with_capacity(cap),
1485 insert_columns: Vec::with_capacity(cap),
1486 insert_values: Vec::with_capacity(cap),
1487 insert_pushes: Vec::with_capacity(cap),
1488 returning_cols: Vec::new(),
1489 auto_assigns: Vec::new(),
1490 auto_field_idents: Vec::new(),
1491 first_auto_value_ty: None,
1492 bulk_pushes_no_auto: Vec::with_capacity(cap),
1493 bulk_pushes_all: Vec::with_capacity(cap),
1494 bulk_columns_no_auto: Vec::with_capacity(cap),
1495 bulk_columns_all: Vec::with_capacity(cap),
1496 bulk_auto_uniformity: Vec::new(),
1497 first_auto_ident: None,
1498 has_auto: false,
1499 pk_is_auto: false,
1500 update_assignments: Vec::with_capacity(cap),
1501 upsert_update_columns: Vec::with_capacity(cap),
1502 primary_key: None,
1503 column_entries: Vec::with_capacity(cap),
1504 field_names: Vec::with_capacity(cap),
1505 fk_relations: Vec::new(),
1506 soft_delete_column: None,
1507 };
1508
1509 for field in &named.named {
1510 let info = process_field(field, table)?;
1511 out.field_names.push(info.ident.to_string());
1512 out.field_schemas.push(info.schema);
1513 out.from_row_inits.push(info.from_row_init);
1514 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1515 if let Some(parent_ty) = info.fk_inner.clone() {
1516 out.fk_relations.push(FkRelation {
1517 parent_type: parent_ty,
1518 fk_column: info.column.clone(),
1519 pk_kind: info.fk_pk_kind,
1520 nullable: info.nullable,
1521 });
1522 }
1523 if info.soft_delete {
1524 if out.soft_delete_column.is_some() {
1525 return Err(syn::Error::new_spanned(
1526 field,
1527 "only one field may be marked `#[rustango(soft_delete)]`",
1528 ));
1529 }
1530 out.soft_delete_column = Some(info.column.clone());
1531 }
1532 let column = info.column.as_str();
1533 let ident = info.ident;
1534 if info.generated_as.is_some() {
1543 out.column_entries.push(ColumnEntry {
1544 ident: ident.clone(),
1545 value_ty: info.value_ty.clone(),
1546 name: ident.to_string(),
1547 column: info.column.clone(),
1548 field_type_tokens: info.field_type_tokens,
1549 });
1550 continue;
1551 }
1552 out.insert_columns.push(quote!(#column));
1553 out.insert_values.push(quote! {
1554 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1555 ::core::clone::Clone::clone(&self.#ident)
1556 )
1557 });
1558 if info.auto {
1559 out.has_auto = true;
1560 if out.first_auto_ident.is_none() {
1561 out.first_auto_ident = Some(ident.clone());
1562 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1563 }
1564 out.returning_cols.push(quote!(#column));
1565 out.auto_field_idents
1566 .push((ident.clone(), info.column.clone()));
1567 out.auto_assigns.push(quote! {
1568 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1569 });
1570 out.insert_pushes.push(quote! {
1571 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1572 _columns.push(#column);
1573 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1574 ::core::clone::Clone::clone(_v)
1575 ));
1576 }
1577 });
1578 out.bulk_columns_all.push(quote!(#column));
1581 out.bulk_pushes_all.push(quote! {
1582 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1583 ::core::clone::Clone::clone(&_row.#ident)
1584 ));
1585 });
1586 let ident_clone = ident.clone();
1590 out.bulk_auto_uniformity.push(quote! {
1591 for _r in rows.iter().skip(1) {
1592 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1593 return ::core::result::Result::Err(
1594 ::rustango::sql::ExecError::Sql(
1595 ::rustango::sql::SqlError::BulkAutoMixed
1596 )
1597 );
1598 }
1599 }
1600 });
1601 } else {
1602 out.insert_pushes.push(quote! {
1603 _columns.push(#column);
1604 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1605 ::core::clone::Clone::clone(&self.#ident)
1606 ));
1607 });
1608 out.bulk_columns_no_auto.push(quote!(#column));
1610 out.bulk_columns_all.push(quote!(#column));
1611 let push_expr = quote! {
1612 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1613 ::core::clone::Clone::clone(&_row.#ident)
1614 ));
1615 };
1616 out.bulk_pushes_no_auto.push(push_expr.clone());
1617 out.bulk_pushes_all.push(push_expr);
1618 }
1619 if info.primary_key {
1620 if out.primary_key.is_some() {
1621 return Err(syn::Error::new_spanned(
1622 field,
1623 "only one field may be marked `#[rustango(primary_key)]`",
1624 ));
1625 }
1626 out.primary_key = Some((ident.clone(), info.column.clone()));
1627 if info.auto {
1628 out.pk_is_auto = true;
1629 }
1630 } else if info.auto_now_add {
1631 } else if info.auto_now {
1633 out.update_assignments.push(quote! {
1638 ::rustango::core::Assignment {
1639 column: #column,
1640 value: ::core::convert::Into::<::rustango::core::Expr>::into(
1641 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1642 ::chrono::Utc::now()
1643 )
1644 ),
1645 }
1646 });
1647 out.upsert_update_columns.push(quote!(#column));
1648 } else {
1649 out.update_assignments.push(quote! {
1650 ::rustango::core::Assignment {
1651 column: #column,
1652 value: ::core::convert::Into::<::rustango::core::Expr>::into(
1653 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1654 ::core::clone::Clone::clone(&self.#ident)
1655 )
1656 ),
1657 }
1658 });
1659 out.upsert_update_columns.push(quote!(#column));
1660 }
1661 out.column_entries.push(ColumnEntry {
1662 ident: ident.clone(),
1663 value_ty: info.value_ty.clone(),
1664 name: ident.to_string(),
1665 column: info.column.clone(),
1666 field_type_tokens: info.field_type_tokens,
1667 });
1668 }
1669 Ok(out)
1670}
1671
1672fn model_impl_tokens(
1673 struct_name: &syn::Ident,
1674 model_name: &str,
1675 table: &str,
1676 display: Option<&str>,
1677 app_label: Option<&str>,
1678 admin: Option<&AdminAttrs>,
1679 default_order: &[(String, bool, proc_macro2::Span)],
1680 field_schemas: &[TokenStream2],
1681 soft_delete_column: Option<&str>,
1682 permissions: bool,
1683 audit_track: Option<&[String]>,
1684 m2m_relations: &[M2MAttr],
1685 indexes: &[IndexAttr],
1686 checks: &[CheckAttr],
1687 composite_fks: &[CompositeFkAttr],
1688 generic_fks: &[GenericFkAttr],
1689 scope: Option<&str>,
1690 is_view: bool,
1691 verbose_name: Option<&str>,
1692 verbose_name_plural: Option<&str>,
1693) -> TokenStream2 {
1694 let display_tokens = if let Some(name) = display {
1695 quote!(::core::option::Option::Some(#name))
1696 } else {
1697 quote!(::core::option::Option::None)
1698 };
1699 let app_label_tokens = if let Some(name) = app_label {
1700 quote!(::core::option::Option::Some(#name))
1701 } else {
1702 quote!(::core::option::Option::None)
1703 };
1704 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1705 quote!(::core::option::Option::Some(#col))
1706 } else {
1707 quote!(::core::option::Option::None)
1708 };
1709 let audit_track_tokens = match audit_track {
1710 None => quote!(::core::option::Option::None),
1711 Some(names) => {
1712 let lits = names.iter().map(|n| n.as_str());
1713 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1714 }
1715 };
1716 let admin_tokens = admin_config_tokens(admin);
1717 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1721 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1722 _ => quote!(::rustango::core::ModelScope::Tenant),
1723 };
1724 let verbose_name_tokens = optional_str(verbose_name);
1725 let verbose_name_plural_tokens = optional_str(verbose_name_plural);
1726 let indexes_tokens = indexes.iter().map(|idx| {
1727 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1728 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1729 let unique = idx.unique;
1730 let method_variant = match idx.method.as_str() {
1734 "gin" => quote!(::rustango::core::IndexMethod::Gin),
1735 "gist" => quote!(::rustango::core::IndexMethod::Gist),
1736 "brin" => quote!(::rustango::core::IndexMethod::Brin),
1737 "spgist" => quote!(::rustango::core::IndexMethod::SpGist),
1738 "hash" => quote!(::rustango::core::IndexMethod::Hash),
1739 "bloom" => quote!(::rustango::core::IndexMethod::Bloom),
1740 _ => quote!(::rustango::core::IndexMethod::BTree),
1741 };
1742 let where_clause = match &idx.where_clause {
1743 Some(s) => quote!(::core::option::Option::Some(#s)),
1744 None => quote!(::core::option::Option::None),
1745 };
1746 quote! {
1747 ::rustango::core::IndexSchema {
1748 name: #name,
1749 columns: &[ #(#cols),* ],
1750 unique: #unique,
1751 method: #method_variant,
1752 where_clause: #where_clause,
1753 }
1754 }
1755 });
1756 let checks_tokens = checks.iter().map(|c| {
1757 let name = c.name.as_str();
1758 let expr = c.expr.as_str();
1759 quote! {
1760 ::rustango::core::CheckConstraint {
1761 name: #name,
1762 expr: #expr,
1763 }
1764 }
1765 });
1766 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1767 let name = rel.name.as_str();
1768 let to = rel.to.as_str();
1769 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1770 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1771 quote! {
1772 ::rustango::core::CompositeFkRelation {
1773 name: #name,
1774 to: #to,
1775 from: &[ #(#from_cols),* ],
1776 on: &[ #(#on_cols),* ],
1777 }
1778 }
1779 });
1780 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1781 let name = rel.name.as_str();
1782 let ct_col = rel.ct_column.as_str();
1783 let pk_col = rel.pk_column.as_str();
1784 quote! {
1785 ::rustango::core::GenericRelation {
1786 name: #name,
1787 ct_column: #ct_col,
1788 pk_column: #pk_col,
1789 }
1790 }
1791 });
1792 let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
1795 let col_lit = col.as_str();
1796 quote! { (#col_lit, #desc) }
1797 });
1798
1799 let m2m_tokens = m2m_relations.iter().map(|rel| {
1800 let name = rel.name.as_str();
1801 let to = rel.to.as_str();
1802 let through = rel.through.as_str();
1803 let src = rel.src.as_str();
1804 let dst = rel.dst.as_str();
1805 let auto_create = rel.auto_create;
1806 quote! {
1807 ::rustango::core::M2MRelation {
1808 name: #name,
1809 to: #to,
1810 through: #through,
1811 src_col: #src,
1812 dst_col: #dst,
1813 auto_create: #auto_create,
1814 }
1815 }
1816 });
1817 quote! {
1818 impl ::rustango::core::Model for #struct_name {
1819 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1820 name: #model_name,
1821 table: #table,
1822 fields: &[ #(#field_schemas),* ],
1823 display: #display_tokens,
1824 app_label: #app_label_tokens,
1825 admin: #admin_tokens,
1826 soft_delete_column: #soft_delete_tokens,
1827 permissions: #permissions,
1828 audit_track: #audit_track_tokens,
1829 m2m: &[ #(#m2m_tokens),* ],
1830 indexes: &[ #(#indexes_tokens),* ],
1831 check_constraints: &[ #(#checks_tokens),* ],
1832 composite_relations: &[ #(#composite_fk_tokens),* ],
1833 generic_relations: &[ #(#generic_fk_tokens),* ],
1834 scope: #scope_tokens,
1835 default_order: &[ #(#default_order_tokens),* ],
1836 is_view: #is_view,
1837 verbose_name: #verbose_name_tokens,
1838 verbose_name_plural: #verbose_name_plural_tokens,
1839 };
1840 }
1841 }
1842}
1843
1844fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1848 let Some(admin) = admin else {
1849 return quote!(::core::option::Option::None);
1850 };
1851
1852 let list_display = admin
1853 .list_display
1854 .as_ref()
1855 .map(|(v, _)| v.as_slice())
1856 .unwrap_or(&[]);
1857 let list_display_lits = list_display.iter().map(|s| s.as_str());
1858
1859 let search_fields = admin
1860 .search_fields
1861 .as_ref()
1862 .map(|(v, _)| v.as_slice())
1863 .unwrap_or(&[]);
1864 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1865
1866 let readonly_fields = admin
1867 .readonly_fields
1868 .as_ref()
1869 .map(|(v, _)| v.as_slice())
1870 .unwrap_or(&[]);
1871 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1872
1873 let list_filter = admin
1874 .list_filter
1875 .as_ref()
1876 .map(|(v, _)| v.as_slice())
1877 .unwrap_or(&[]);
1878 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1879
1880 let actions = admin
1881 .actions
1882 .as_ref()
1883 .map(|(v, _)| v.as_slice())
1884 .unwrap_or(&[]);
1885 let actions_lits = actions.iter().map(|s| s.as_str());
1886
1887 let fieldsets = admin
1888 .fieldsets
1889 .as_ref()
1890 .map(|(v, _)| v.as_slice())
1891 .unwrap_or(&[]);
1892 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1893 let title = title.as_str();
1894 let field_lits = fields.iter().map(|s| s.as_str());
1895 quote!(::rustango::core::Fieldset {
1896 title: #title,
1897 fields: &[ #( #field_lits ),* ],
1898 })
1899 });
1900
1901 let list_display_links = admin
1902 .list_display_links
1903 .as_ref()
1904 .map(|(v, _)| v.as_slice())
1905 .unwrap_or(&[]);
1906 let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
1907
1908 let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
1909 let actions_on_top = admin.actions_on_top.unwrap_or(true);
1910 let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
1911 let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
1912
1913 let prepopulated = admin
1914 .prepopulated_fields
1915 .as_ref()
1916 .map(|(v, _)| v.as_slice())
1917 .unwrap_or(&[]);
1918 let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
1919 let target = target.as_str();
1920 let source_lits = sources.iter().map(|s| s.as_str());
1921 quote!(::rustango::core::PrepopulatedField {
1922 target: #target,
1923 sources: &[ #( #source_lits ),* ],
1924 })
1925 });
1926
1927 let raw_id_fields = admin
1928 .raw_id_fields
1929 .as_ref()
1930 .map(|(v, _)| v.as_slice())
1931 .unwrap_or(&[]);
1932 let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
1933
1934 let autocomplete_fields = admin
1935 .autocomplete_fields
1936 .as_ref()
1937 .map(|(v, _)| v.as_slice())
1938 .unwrap_or(&[]);
1939 let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
1940
1941 let list_select_related_tokens = match admin.list_select_related.as_deref() {
1943 None | Some("all") => quote!(::rustango::core::ListSelectRelated::All),
1944 Some("none") => quote!(::rustango::core::ListSelectRelated::None),
1945 Some(raw) => {
1946 let names: Vec<&str> = raw
1947 .split(',')
1948 .map(str::trim)
1949 .filter(|s| !s.is_empty())
1950 .collect();
1951 quote!(::rustango::core::ListSelectRelated::Only(&[ #( #names ),* ]))
1952 }
1953 };
1954
1955 let formfield_pairs: Vec<(&str, &str)> = admin
1958 .formfield_overrides
1959 .as_ref()
1960 .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
1961 .unwrap_or_default();
1962 let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
1963 let field = *field;
1964 let widget = *widget;
1965 quote!((#field, #widget))
1966 });
1967
1968 let list_per_page = admin.list_per_page.unwrap_or(0);
1969
1970 let ordering_pairs = admin
1971 .ordering
1972 .as_ref()
1973 .map(|(v, _)| v.as_slice())
1974 .unwrap_or(&[]);
1975 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1976 let name = name.as_str();
1977 let desc = *desc;
1978 quote!((#name, #desc))
1979 });
1980
1981 quote! {
1982 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1983 list_display: &[ #( #list_display_lits ),* ],
1984 search_fields: &[ #( #search_fields_lits ),* ],
1985 list_per_page: #list_per_page,
1986 ordering: &[ #( #ordering_tokens ),* ],
1987 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1988 list_filter: &[ #( #list_filter_lits ),* ],
1989 actions: &[ #( #actions_lits ),* ],
1990 fieldsets: &[ #( #fieldset_tokens ),* ],
1991 list_display_links: &[ #( #list_display_links_lits ),* ],
1992 search_help_text: #search_help_text,
1993 actions_on_top: #actions_on_top,
1994 actions_on_bottom: #actions_on_bottom,
1995 date_hierarchy: #date_hierarchy,
1996 prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
1997 raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
1998 autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
1999 list_select_related: #list_select_related_tokens,
2000 formfield_overrides: &[ #( #formfield_tokens ),* ],
2001 })
2002 }
2003}
2004
2005fn inherent_impl_tokens(
2006 struct_name: &syn::Ident,
2007 fields: &CollectedFields,
2008 primary_key: Option<&(syn::Ident, String)>,
2009 column_consts: &TokenStream2,
2010 audited_fields: Option<&[&ColumnEntry]>,
2011 indexes: &[IndexAttr],
2012 manager_fns: &[syn::Ident],
2013) -> TokenStream2 {
2014 let executor_passes_to_data_write = if audited_fields.is_some() {
2020 quote!(&mut *_executor)
2021 } else {
2022 quote!(_executor)
2023 };
2024 let executor_param = if audited_fields.is_some() {
2025 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
2026 } else {
2027 quote!(_executor: _E)
2028 };
2029 let executor_generics = if audited_fields.is_some() {
2030 quote!()
2031 } else {
2032 quote!(<'_c, _E>)
2033 };
2034 let executor_where = if audited_fields.is_some() {
2035 quote!()
2036 } else {
2037 quote! {
2038 where
2039 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2040 }
2041 };
2042 let pool_to_save_on = if audited_fields.is_some() {
2047 quote! {
2048 let mut _conn = pool.acquire().await?;
2049 self.save_on(&mut *_conn).await
2050 }
2051 } else {
2052 quote!(self.save_on(pool).await)
2053 };
2054 let pool_to_insert_on = if audited_fields.is_some() {
2055 quote! {
2056 let mut _conn = pool.acquire().await?;
2057 self.insert_on(&mut *_conn).await
2058 }
2059 } else {
2060 quote!(self.insert_on(pool).await)
2061 };
2062 let pool_to_delete_on = if audited_fields.is_some() {
2063 quote! {
2064 let mut _conn = pool.acquire().await?;
2065 self.delete_on(&mut *_conn).await
2066 }
2067 } else {
2068 quote!(self.delete_on(pool).await)
2069 };
2070 let pool_to_bulk_insert_on = if audited_fields.is_some() {
2071 quote! {
2072 let mut _conn = pool.acquire().await?;
2073 Self::bulk_insert_on(rows, &mut *_conn).await
2074 }
2075 } else {
2076 quote!(Self::bulk_insert_on(rows, pool).await)
2077 };
2078 let pool_to_upsert_on = if audited_fields.is_some() {
2085 quote! {
2086 let mut _conn = pool.acquire().await?;
2087 self.upsert_on(&mut *_conn).await
2088 }
2089 } else {
2090 quote!(self.upsert_on(pool).await)
2091 };
2092
2093 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2111 quote!()
2120 } else if audited_fields.is_some() && fields.has_auto {
2121 quote!()
2124 } else if fields.has_auto {
2125 let pushes = &fields.insert_pushes;
2126 let returning_cols = &fields.returning_cols;
2127 quote! {
2128 pub async fn insert_pool(
2134 &mut self,
2135 pool: &::rustango::sql::Pool,
2136 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2137 let mut _columns: ::std::vec::Vec<&'static str> =
2138 ::std::vec::Vec::new();
2139 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2140 ::std::vec::Vec::new();
2141 #( #pushes )*
2142 let _query = ::rustango::core::InsertQuery {
2143 model: <Self as ::rustango::core::Model>::SCHEMA,
2144 columns: _columns,
2145 values: _values,
2146 returning: ::std::vec![ #( #returning_cols ),* ],
2147 on_conflict: ::core::option::Option::None,
2148 };
2149 let _result = ::rustango::sql::insert_returning_pool(
2150 pool, &_query,
2151 ).await?;
2152 ::rustango::sql::apply_auto_pk(_result, self)
2153 }
2154 }
2155 } else {
2156 let insert_columns = &fields.insert_columns;
2157 let insert_values = &fields.insert_values;
2158 quote! {
2159 pub async fn insert_pool(
2166 &self,
2167 pool: &::rustango::sql::Pool,
2168 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2169 let _query = ::rustango::core::InsertQuery {
2170 model: <Self as ::rustango::core::Model>::SCHEMA,
2171 columns: ::std::vec![ #( #insert_columns ),* ],
2172 values: ::std::vec![ #( #insert_values ),* ],
2173 returning: ::std::vec::Vec::new(),
2174 on_conflict: ::core::option::Option::None,
2175 };
2176 ::rustango::sql::insert_pool(pool, &_query).await
2177 }
2178 }
2179 };
2180
2181 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
2194 .map(|tracked| {
2195 tracked
2196 .iter()
2197 .map(|c| {
2198 let column_lit = c.column.as_str();
2199 let ident = &c.ident;
2200 quote! {
2201 (
2202 #column_lit,
2203 ::serde_json::to_value(&self.#ident)
2204 .unwrap_or(::serde_json::Value::Null),
2205 )
2206 }
2207 })
2208 .collect()
2209 })
2210 .unwrap_or_default();
2211 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
2212 if fields.pk_is_auto {
2213 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2214 } else {
2215 quote!(::std::format!("{}", &self.#pk_ident))
2216 }
2217 } else {
2218 quote!(::std::string::String::new())
2219 };
2220 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
2221 if audited_fields.is_some() {
2222 let pairs = audit_pair_tokens.iter();
2223 let pk_str = audit_pk_to_string.clone();
2224 quote! {
2225 let _audit_entry = ::rustango::audit::PendingEntry {
2226 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2227 entity_pk: #pk_str,
2228 operation: #op_path,
2229 source: ::rustango::audit::current_source(),
2230 changes: ::rustango::audit::snapshot_changes(&[
2231 #( #pairs ),*
2232 ]),
2233 };
2234 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2235 }
2236 } else {
2237 quote!()
2238 }
2239 };
2240 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
2241 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
2242 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
2243 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
2244
2245 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
2261 let pk_column_lit = pk_col.as_str();
2262 let assignments = &fields.update_assignments;
2263 if audited_fields.is_some() {
2264 if fields.pk_is_auto {
2265 quote!()
2269 } else {
2270 let pairs = audit_pair_tokens.iter();
2271 let pairs2 = audit_pair_tokens.iter();
2272 let pk_str = audit_pk_to_string.clone();
2273 let pk_str2 = audit_pk_to_string.clone();
2274 quote! {
2275 pub async fn save_pool(
2289 &mut self,
2290 pool: &::rustango::sql::Pool,
2291 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2292 let _query = ::rustango::core::UpdateQuery {
2293 model: <Self as ::rustango::core::Model>::SCHEMA,
2294 set: ::std::vec![ #( #assignments ),* ],
2295 where_clause: ::rustango::core::WhereExpr::Predicate(
2296 ::rustango::core::Filter {
2297 column: #pk_column_lit,
2298 op: ::rustango::core::Op::Eq,
2299 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2300 ::core::clone::Clone::clone(&self.#pk_ident)
2301 ),
2302 }
2303 ),
2304 };
2305 let _audit_entry = ::rustango::audit::PendingEntry {
2306 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2307 entity_pk: #pk_str,
2308 operation: ::rustango::audit::AuditOp::Update,
2309 source: ::rustango::audit::current_source(),
2310 changes: ::rustango::audit::snapshot_changes(&[
2311 #( #pairs ),*
2312 ]),
2313 };
2314 let _ = ::rustango::audit::save_one_with_audit(
2315 pool, &_query, &_audit_entry,
2316 ).await?;
2317 ::core::result::Result::Ok(())
2318 }
2319
2320 pub async fn save_partial(
2330 &mut self,
2331 fields: &[&str],
2332 pool: &::rustango::sql::Pool,
2333 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2334 if fields.is_empty() {
2335 ::tracing::warn!(
2336 target: "rustango::save_partial",
2337 model = <Self as ::rustango::core::Model>::SCHEMA.name,
2338 "save_partial called with empty field list — no-op"
2339 );
2340 return ::core::result::Result::Ok(());
2341 }
2342 let _schema = <Self as ::rustango::core::Model>::SCHEMA;
2343 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
2344 ::std::collections::HashSet::with_capacity(fields.len());
2345 for f in fields {
2346 match _schema.field(f) {
2347 ::core::option::Option::Some(fs) => {
2348 _wanted_cols.insert(fs.column);
2349 }
2350 ::core::option::Option::None => {
2351 return ::core::result::Result::Err(
2352 ::rustango::sql::ExecError::Query(
2353 ::rustango::core::QueryError::UnknownField {
2354 model: _schema.name,
2355 field: (*f).to_owned(),
2356 }
2357 )
2358 );
2359 }
2360 }
2361 }
2362 let _full: ::std::vec::Vec<::rustango::core::Assignment> =
2363 ::std::vec![ #( #assignments ),* ];
2364 let _filtered: ::std::vec::Vec<::rustango::core::Assignment> = _full
2365 .into_iter()
2366 .filter(|a| _wanted_cols.contains(a.column))
2367 .collect();
2368 if _filtered.is_empty() {
2369 ::tracing::warn!(
2370 target: "rustango::save_partial",
2371 model = _schema.name,
2372 "save_partial: every named field maps to a non-assignable column — no-op"
2373 );
2374 return ::core::result::Result::Ok(());
2375 }
2376 let _query = ::rustango::core::UpdateQuery {
2377 model: _schema,
2378 set: _filtered,
2379 where_clause: ::rustango::core::WhereExpr::Predicate(
2380 ::rustango::core::Filter {
2381 column: #pk_column_lit,
2382 op: ::rustango::core::Op::Eq,
2383 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2384 ::core::clone::Clone::clone(&self.#pk_ident)
2385 ),
2386 }
2387 ),
2388 };
2389 let _all_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2391 ::std::vec![ #( #pairs2 ),* ];
2392 let _narrowed: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2393 _all_pairs
2394 .into_iter()
2395 .filter(|(col, _)| _wanted_cols.contains(col))
2396 .collect();
2397 let _audit_entry = ::rustango::audit::PendingEntry {
2398 entity_table: _schema.table,
2399 entity_pk: #pk_str2,
2400 operation: ::rustango::audit::AuditOp::Update,
2401 source: ::rustango::audit::current_source(),
2402 changes: ::rustango::audit::snapshot_changes(&_narrowed),
2403 };
2404 let _ = ::rustango::audit::save_one_with_audit(
2405 pool, &_query, &_audit_entry,
2406 ).await?;
2407 ::core::result::Result::Ok(())
2408 }
2409
2410 pub async fn save_partial_typed<
2429 L: ::rustango::core::TypedFieldList<Self>,
2430 >(
2431 &mut self,
2432 fields: L,
2433 pool: &::rustango::sql::Pool,
2434 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2435 let _names = fields.rust_field_names();
2436 let _refs: ::std::vec::Vec<&str> =
2437 _names.iter().copied().collect();
2438 self.save_partial(&_refs, pool).await
2439 }
2440 }
2441 }
2442 } else {
2443 let dispatch_unset = if fields.pk_is_auto {
2444 quote! {
2445 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2446 return self.insert_pool(pool).await;
2447 }
2448 }
2449 } else {
2450 quote!()
2451 };
2452 quote! {
2453 pub async fn save_pool(
2460 &mut self,
2461 pool: &::rustango::sql::Pool,
2462 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2463 #dispatch_unset
2464 let _query = ::rustango::core::UpdateQuery {
2465 model: <Self as ::rustango::core::Model>::SCHEMA,
2466 set: ::std::vec![ #( #assignments ),* ],
2467 where_clause: ::rustango::core::WhereExpr::Predicate(
2468 ::rustango::core::Filter {
2469 column: #pk_column_lit,
2470 op: ::rustango::core::Op::Eq,
2471 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2472 ::core::clone::Clone::clone(&self.#pk_ident)
2473 ),
2474 }
2475 ),
2476 };
2477 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
2478 ::core::result::Result::Ok(())
2479 }
2480
2481 pub async fn save_partial(
2510 &mut self,
2511 fields: &[&str],
2512 pool: &::rustango::sql::Pool,
2513 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2514 if fields.is_empty() {
2515 ::tracing::warn!(
2516 target: "rustango::save_partial",
2517 model = <Self as ::rustango::core::Model>::SCHEMA.name,
2518 "save_partial called with empty field list — no-op"
2519 );
2520 return ::core::result::Result::Ok(());
2521 }
2522 let _schema = <Self as ::rustango::core::Model>::SCHEMA;
2523 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
2525 ::std::collections::HashSet::with_capacity(fields.len());
2526 for f in fields {
2527 match _schema.field(f) {
2528 ::core::option::Option::Some(fs) => {
2529 _wanted_cols.insert(fs.column);
2530 }
2531 ::core::option::Option::None => {
2532 return ::core::result::Result::Err(
2533 ::rustango::sql::ExecError::Query(
2534 ::rustango::core::QueryError::UnknownField {
2535 model: _schema.name,
2536 field: (*f).to_owned(),
2537 }
2538 )
2539 );
2540 }
2541 }
2542 }
2543 let _full: ::std::vec::Vec<::rustango::core::Assignment> =
2546 ::std::vec![ #( #assignments ),* ];
2547 let _filtered: ::std::vec::Vec<::rustango::core::Assignment> = _full
2548 .into_iter()
2549 .filter(|a| _wanted_cols.contains(a.column))
2550 .collect();
2551 if _filtered.is_empty() {
2552 ::tracing::warn!(
2557 target: "rustango::save_partial",
2558 model = _schema.name,
2559 "save_partial: every named field maps to a non-assignable column — no-op"
2560 );
2561 return ::core::result::Result::Ok(());
2562 }
2563 let _query = ::rustango::core::UpdateQuery {
2564 model: _schema,
2565 set: _filtered,
2566 where_clause: ::rustango::core::WhereExpr::Predicate(
2567 ::rustango::core::Filter {
2568 column: #pk_column_lit,
2569 op: ::rustango::core::Op::Eq,
2570 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2571 ::core::clone::Clone::clone(&self.#pk_ident)
2572 ),
2573 }
2574 ),
2575 };
2576 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
2577 ::core::result::Result::Ok(())
2578 }
2579
2580 pub async fn save_partial_typed<
2600 L: ::rustango::core::TypedFieldList<Self>,
2601 >(
2602 &mut self,
2603 fields: L,
2604 pool: &::rustango::sql::Pool,
2605 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2606 let _names = fields.rust_field_names();
2607 let _refs: ::std::vec::Vec<&str> =
2608 _names.iter().copied().collect();
2609 self.save_partial(&_refs, pool).await
2610 }
2611 }
2612 }
2613 } else {
2614 quote!()
2615 };
2616
2617 let pool_insert_method = if audited_fields.is_some() {
2624 if let Some(_) = primary_key {
2625 let pushes = if fields.has_auto {
2626 fields.insert_pushes.clone()
2627 } else {
2628 fields
2633 .insert_columns
2634 .iter()
2635 .zip(&fields.insert_values)
2636 .map(|(col, val)| {
2637 quote! {
2638 _columns.push(#col);
2639 _values.push(#val);
2640 }
2641 })
2642 .collect()
2643 };
2644 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
2645 fields.returning_cols.clone()
2646 } else {
2647 primary_key
2654 .map(|(_, col)| {
2655 let lit = col.as_str();
2656 vec![quote!(#lit)]
2657 })
2658 .unwrap_or_default()
2659 };
2660 let pairs = audit_pair_tokens.iter();
2661 let pk_str = audit_pk_to_string.clone();
2662 quote! {
2663 pub async fn insert_pool(
2672 &mut self,
2673 pool: &::rustango::sql::Pool,
2674 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2675 let mut _columns: ::std::vec::Vec<&'static str> =
2676 ::std::vec::Vec::new();
2677 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2678 ::std::vec::Vec::new();
2679 #( #pushes )*
2680 let _query = ::rustango::core::InsertQuery {
2681 model: <Self as ::rustango::core::Model>::SCHEMA,
2682 columns: _columns,
2683 values: _values,
2684 returning: ::std::vec![ #( #returning_cols ),* ],
2685 on_conflict: ::core::option::Option::None,
2686 };
2687 let _audit_entry = ::rustango::audit::PendingEntry {
2688 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2689 entity_pk: #pk_str,
2690 operation: ::rustango::audit::AuditOp::Create,
2691 source: ::rustango::audit::current_source(),
2692 changes: ::rustango::audit::snapshot_changes(&[
2693 #( #pairs ),*
2694 ]),
2695 };
2696 let _result = ::rustango::audit::insert_one_with_audit(
2697 pool, &_query, &_audit_entry,
2698 ).await?;
2699 ::rustango::sql::apply_auto_pk(_result, self)
2700 }
2701 }
2702 } else {
2703 quote!()
2704 }
2705 } else {
2706 pool_insert_method
2708 };
2709
2710 let pool_save_method = if let Some(tracked) = audited_fields {
2731 if let Some((pk_ident, pk_col)) = primary_key {
2732 let pk_column_lit = pk_col.as_str();
2733 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
2737 let pk_str = audit_pk_to_string.clone();
2738 let mk_before_pairs =
2743 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
2744 tracked
2745 .iter()
2746 .map(|c| {
2747 let column_lit = c.column.as_str();
2748 let value_ty = &c.value_ty;
2749 quote! {
2750 (
2751 #column_lit,
2752 match #getter::<#value_ty>(
2753 _audit_before_row, #column_lit,
2754 ) {
2755 ::core::result::Result::Ok(v) => {
2756 ::serde_json::to_value(&v)
2757 .unwrap_or(::serde_json::Value::Null)
2758 }
2759 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2760 },
2761 )
2762 }
2763 })
2764 .collect()
2765 };
2766 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
2767 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
2768 let before_pairs_my: Vec<proc_macro2::TokenStream> =
2769 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
2770 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
2771 mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
2772 let pg_select_cols: String = tracked
2773 .iter()
2774 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2775 .collect::<Vec<_>>()
2776 .join(", ");
2777 let my_select_cols: String = tracked
2778 .iter()
2779 .map(|c| format!("`{}`", c.column.replace('`', "``")))
2780 .collect::<Vec<_>>()
2781 .join(", ");
2782 let sqlite_select_cols: String = pg_select_cols.clone();
2786 let pk_value_for_bind = if fields.pk_is_auto {
2787 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2788 } else {
2789 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2790 };
2791 let assignments = &fields.update_assignments;
2792 let unset_dispatch = if fields.has_auto {
2793 quote! {
2794 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2795 return self.insert_pool(pool).await;
2796 }
2797 }
2798 } else {
2799 quote!()
2800 };
2801 quote! {
2802 pub async fn save_pool(
2816 &mut self,
2817 pool: &::rustango::sql::Pool,
2818 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2819 #unset_dispatch
2820 let _query = ::rustango::core::UpdateQuery {
2821 model: <Self as ::rustango::core::Model>::SCHEMA,
2822 set: ::std::vec![ #( #assignments ),* ],
2823 where_clause: ::rustango::core::WhereExpr::Predicate(
2824 ::rustango::core::Filter {
2825 column: #pk_column_lit,
2826 op: ::rustango::core::Op::Eq,
2827 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2828 ::core::clone::Clone::clone(&self.#pk_ident)
2829 ),
2830 }
2831 ),
2832 };
2833 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2834 ::std::vec![ #( #after_pairs_pg ),* ];
2835 ::rustango::audit::save_one_with_diff(
2836 pool,
2837 &_query,
2838 #pk_column_lit,
2839 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2840 #pk_value_for_bind,
2841 ),
2842 <Self as ::rustango::core::Model>::SCHEMA.table,
2843 #pk_str,
2844 _after_pairs,
2845 #pg_select_cols,
2846 #my_select_cols,
2847 #sqlite_select_cols,
2848 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2849 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2850 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2851 ).await
2852 }
2853 }
2854 } else {
2855 quote!()
2856 }
2857 } else {
2858 pool_save_method
2859 };
2860
2861 let pool_delete_method = {
2868 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2869 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2870 if let Some(pk_ident) = pk_ident_for_pool {
2871 if audited_fields.is_some() {
2872 let pairs = audit_pair_tokens.iter();
2873 let pk_str = audit_pk_to_string.clone();
2874 quote! {
2875 pub async fn delete_pool(
2882 &self,
2883 pool: &::rustango::sql::Pool,
2884 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2885 let _query = ::rustango::core::DeleteQuery {
2886 model: <Self as ::rustango::core::Model>::SCHEMA,
2887 where_clause: ::rustango::core::WhereExpr::Predicate(
2888 ::rustango::core::Filter {
2889 column: #pk_column_lit,
2890 op: ::rustango::core::Op::Eq,
2891 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2892 ::core::clone::Clone::clone(&self.#pk_ident)
2893 ),
2894 }
2895 ),
2896 };
2897 let _audit_entry = ::rustango::audit::PendingEntry {
2898 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2899 entity_pk: #pk_str,
2900 operation: ::rustango::audit::AuditOp::Delete,
2901 source: ::rustango::audit::current_source(),
2902 changes: ::rustango::audit::snapshot_changes(&[
2903 #( #pairs ),*
2904 ]),
2905 };
2906 ::rustango::audit::delete_one_with_audit(
2907 pool, &_query, &_audit_entry,
2908 ).await
2909 }
2910 }
2911 } else {
2912 quote! {
2913 pub async fn delete_pool(
2920 &self,
2921 pool: &::rustango::sql::Pool,
2922 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2923 let _query = ::rustango::core::DeleteQuery {
2924 model: <Self as ::rustango::core::Model>::SCHEMA,
2925 where_clause: ::rustango::core::WhereExpr::Predicate(
2926 ::rustango::core::Filter {
2927 column: #pk_column_lit,
2928 op: ::rustango::core::Op::Eq,
2929 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2930 ::core::clone::Clone::clone(&self.#pk_ident)
2931 ),
2932 }
2933 ),
2934 };
2935 ::rustango::sql::delete_pool(pool, &_query).await
2936 }
2937 }
2938 }
2939 } else {
2940 quote!()
2941 }
2942 };
2943
2944 let tx_insert_method = if fields.has_auto {
2950 let pushes = &fields.insert_pushes;
2951 let returning_cols = &fields.returning_cols;
2952 quote! {
2953 pub async fn insert_tx(
2960 &mut self,
2961 tx: &mut ::rustango::sql::PoolTx<'_>,
2962 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2963 let mut _columns: ::std::vec::Vec<&'static str> =
2964 ::std::vec::Vec::new();
2965 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2966 ::std::vec::Vec::new();
2967 #( #pushes )*
2968 let _query = ::rustango::core::InsertQuery {
2969 model: <Self as ::rustango::core::Model>::SCHEMA,
2970 columns: _columns,
2971 values: _values,
2972 returning: ::std::vec![ #( #returning_cols ),* ],
2973 on_conflict: ::core::option::Option::None,
2974 };
2975 let _result = ::rustango::sql::insert_returning_tx(tx, &_query).await?;
2976 ::rustango::sql::apply_auto_pk(_result, self)
2977 }
2978 }
2979 } else {
2980 let insert_columns = &fields.insert_columns;
2981 let insert_values = &fields.insert_values;
2982 quote! {
2983 pub async fn insert_tx(
2988 &self,
2989 tx: &mut ::rustango::sql::PoolTx<'_>,
2990 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2991 let _query = ::rustango::core::InsertQuery {
2992 model: <Self as ::rustango::core::Model>::SCHEMA,
2993 columns: ::std::vec![ #( #insert_columns ),* ],
2994 values: ::std::vec![ #( #insert_values ),* ],
2995 returning: ::std::vec::Vec::new(),
2996 on_conflict: ::core::option::Option::None,
2997 };
2998 ::rustango::sql::insert_tx(tx, &_query).await
2999 }
3000 }
3001 };
3002
3003 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3004 let pk_column_lit = pk_col.as_str();
3005 let assignments = &fields.update_assignments;
3006 let dispatch_unset = if fields.pk_is_auto {
3007 quote! {
3008 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
3009 return self.insert_tx(tx).await;
3010 }
3011 }
3012 } else {
3013 quote!()
3014 };
3015 quote! {
3016 pub async fn save_tx(
3023 &mut self,
3024 tx: &mut ::rustango::sql::PoolTx<'_>,
3025 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3026 #dispatch_unset
3027 let _query = ::rustango::core::UpdateQuery {
3028 model: <Self as ::rustango::core::Model>::SCHEMA,
3029 set: ::std::vec![ #( #assignments ),* ],
3030 where_clause: ::rustango::core::WhereExpr::Predicate(
3031 ::rustango::core::Filter {
3032 column: #pk_column_lit,
3033 op: ::rustango::core::Op::Eq,
3034 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3035 ::core::clone::Clone::clone(&self.#pk_ident)
3036 ),
3037 }
3038 ),
3039 };
3040 let _ = ::rustango::sql::update_tx(tx, &_query).await?;
3041 ::core::result::Result::Ok(())
3042 }
3043 }
3044 } else {
3045 quote!()
3046 };
3047
3048 let tx_delete_method = {
3049 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3050 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
3051 if let Some(pk_ident) = pk_ident_for_tx {
3052 quote! {
3053 pub async fn delete_tx(
3060 &self,
3061 tx: &mut ::rustango::sql::PoolTx<'_>,
3062 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
3063 let _query = ::rustango::core::DeleteQuery {
3064 model: <Self as ::rustango::core::Model>::SCHEMA,
3065 where_clause: ::rustango::core::WhereExpr::Predicate(
3066 ::rustango::core::Filter {
3067 column: #pk_column_lit,
3068 op: ::rustango::core::Op::Eq,
3069 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3070 ::core::clone::Clone::clone(&self.#pk_ident)
3071 ),
3072 }
3073 ),
3074 };
3075 ::rustango::sql::delete_tx(tx, &_query).await
3076 }
3077 }
3078 } else {
3079 quote!()
3080 }
3081 };
3082
3083 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
3093 audited_fields
3094 {
3095 if tracked.is_empty() {
3096 (quote!(), quote!())
3097 } else {
3098 let select_cols: String = tracked
3099 .iter()
3100 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3101 .collect::<Vec<_>>()
3102 .join(", ");
3103 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
3104 let select_cols_lit = select_cols;
3105 let pk_column_lit_for_select = pk_column_for_select;
3106 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
3107 if fields.pk_is_auto {
3108 quote!(self.#pk_ident.get().copied().unwrap_or_default())
3109 } else {
3110 quote!(::core::clone::Clone::clone(&self.#pk_ident))
3111 }
3112 } else {
3113 quote!(0_i64)
3114 };
3115 let before_pairs = tracked.iter().map(|c| {
3116 let column_lit = c.column.as_str();
3117 let value_ty = &c.value_ty;
3118 quote! {
3119 (
3120 #column_lit,
3121 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
3122 &_audit_before_row, #column_lit,
3123 ) {
3124 ::core::result::Result::Ok(v) => {
3125 ::serde_json::to_value(&v)
3126 .unwrap_or(::serde_json::Value::Null)
3127 }
3128 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
3129 },
3130 )
3131 }
3132 });
3133 let after_pairs = tracked.iter().map(|c| {
3134 let column_lit = c.column.as_str();
3135 let ident = &c.ident;
3136 quote! {
3137 (
3138 #column_lit,
3139 ::serde_json::to_value(&self.#ident)
3140 .unwrap_or(::serde_json::Value::Null),
3141 )
3142 }
3143 });
3144 let pk_str = audit_pk_to_string.clone();
3145 let pre = quote! {
3146 let _audit_select_sql = ::std::format!(
3147 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
3148 #select_cols_lit,
3149 <Self as ::rustango::core::Model>::SCHEMA.table,
3150 #pk_column_lit_for_select,
3151 );
3152 let _audit_before_pairs:
3153 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
3154 match ::rustango::sql::sqlx::query(&_audit_select_sql)
3155 .bind(#pk_value_for_bind)
3156 .fetch_optional(&mut *_executor)
3157 .await
3158 {
3159 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
3160 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
3161 }
3162 _ => ::core::option::Option::None,
3163 };
3164 };
3165 let post = quote! {
3166 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
3167 let _audit_after:
3168 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
3169 ::std::vec![ #( #after_pairs ),* ];
3170 let _audit_entry = ::rustango::audit::PendingEntry {
3171 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
3172 entity_pk: #pk_str,
3173 operation: ::rustango::audit::AuditOp::Update,
3174 source: ::rustango::audit::current_source(),
3175 changes: ::rustango::audit::diff_changes(
3176 &_audit_before,
3177 &_audit_after,
3178 ),
3179 };
3180 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3181 }
3182 };
3183 (pre, post)
3184 }
3185 } else {
3186 (quote!(), quote!())
3187 };
3188
3189 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
3193 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
3194 if fields.pk_is_auto {
3195 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3196 } else {
3197 quote!(::std::format!("{}", &_row.#pk_ident))
3198 }
3199 } else {
3200 quote!(::std::string::String::new())
3201 };
3202 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
3203 let column_lit = c.column.as_str();
3204 let ident = &c.ident;
3205 quote! {
3206 (
3207 #column_lit,
3208 ::serde_json::to_value(&_row.#ident)
3209 .unwrap_or(::serde_json::Value::Null),
3210 )
3211 }
3212 });
3213 quote! {
3214 let _audit_source = ::rustango::audit::current_source();
3215 let mut _audit_entries:
3216 ::std::vec::Vec<::rustango::audit::PendingEntry> =
3217 ::std::vec::Vec::with_capacity(rows.len());
3218 for _row in rows.iter() {
3219 _audit_entries.push(::rustango::audit::PendingEntry {
3220 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
3221 entity_pk: #row_pk_str,
3222 operation: ::rustango::audit::AuditOp::Create,
3223 source: _audit_source.clone(),
3224 changes: ::rustango::audit::snapshot_changes(&[
3225 #( #row_pairs ),*
3226 ]),
3227 });
3228 }
3229 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
3230 }
3231 } else {
3232 quote!()
3233 };
3234
3235 let save_method = if fields.pk_is_auto {
3236 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
3237 let pk_column_lit = pk_column.as_str();
3238 let assignments = &fields.update_assignments;
3239 let upsert_cols = &fields.upsert_update_columns;
3240 let upsert_pushes = &fields.insert_pushes;
3241 let upsert_returning = &fields.returning_cols;
3242 let upsert_auto_assigns = &fields.auto_assigns;
3243 let upsert_target_columns: Vec<String> = indexes
3252 .iter()
3253 .find(|i| i.unique && !i.columns.is_empty())
3254 .map(|i| i.columns.clone())
3255 .unwrap_or_else(|| vec![pk_column.clone()]);
3256 let upsert_target_lits = upsert_target_columns
3257 .iter()
3258 .map(String::as_str)
3259 .collect::<Vec<_>>();
3260 let conflict_clause = if fields.upsert_update_columns.is_empty() {
3261 quote!(::rustango::core::ConflictClause::DoNothing)
3262 } else {
3263 quote!(::rustango::core::ConflictClause::DoUpdate {
3264 target: ::std::vec![ #( #upsert_target_lits ),* ],
3265 update_columns: ::std::vec![ #( #upsert_cols ),* ],
3266 })
3267 };
3268 Some(quote! {
3269 #[cfg(feature = "postgres")]
3287 pub async fn save(
3288 &mut self,
3289 pool: &::rustango::sql::sqlx::PgPool,
3290 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3291 #pool_to_save_on
3292 }
3293
3294 #[cfg(feature = "postgres")]
3305 pub async fn save_on #executor_generics (
3306 &mut self,
3307 #executor_param,
3308 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3309 #executor_where
3310 {
3311 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
3312 return self.insert_on(#executor_passes_to_data_write).await;
3313 }
3314 #audit_update_pre
3315 let _query = ::rustango::core::UpdateQuery {
3316 model: <Self as ::rustango::core::Model>::SCHEMA,
3317 set: ::std::vec![ #( #assignments ),* ],
3318 where_clause: ::rustango::core::WhereExpr::Predicate(
3319 ::rustango::core::Filter {
3320 column: #pk_column_lit,
3321 op: ::rustango::core::Op::Eq,
3322 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3323 ::core::clone::Clone::clone(&self.#pk_ident)
3324 ),
3325 }
3326 ),
3327 };
3328 let _ = ::rustango::sql::__macro_internals::update_on(
3329 #executor_passes_to_data_write,
3330 &_query,
3331 ).await?;
3332 #audit_update_post
3333 ::core::result::Result::Ok(())
3334 }
3335
3336 #[cfg(feature = "postgres")]
3347 pub async fn save_on_with #executor_generics (
3348 &mut self,
3349 #executor_param,
3350 source: ::rustango::audit::AuditSource,
3351 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3352 #executor_where
3353 {
3354 ::rustango::audit::with_source(source, self.save_on(_executor)).await
3355 }
3356
3357 #[cfg(feature = "postgres")]
3367 pub async fn upsert(
3368 &mut self,
3369 pool: &::rustango::sql::sqlx::PgPool,
3370 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3371 #pool_to_upsert_on
3372 }
3373
3374 #[cfg(feature = "postgres")]
3380 pub async fn upsert_on #executor_generics (
3381 &mut self,
3382 #executor_param,
3383 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3384 #executor_where
3385 {
3386 let mut _columns: ::std::vec::Vec<&'static str> =
3387 ::std::vec::Vec::new();
3388 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
3389 ::std::vec::Vec::new();
3390 #( #upsert_pushes )*
3391 let query = ::rustango::core::InsertQuery {
3392 model: <Self as ::rustango::core::Model>::SCHEMA,
3393 columns: _columns,
3394 values: _values,
3395 returning: ::std::vec![ #( #upsert_returning ),* ],
3396 on_conflict: ::core::option::Option::Some(#conflict_clause),
3397 };
3398 let _returning_row_v = ::rustango::sql::__macro_internals::insert_returning_on(
3399 #executor_passes_to_data_write,
3400 &query,
3401 ).await?;
3402 let _returning_row = &_returning_row_v;
3403 #( #upsert_auto_assigns )*
3404 ::core::result::Result::Ok(())
3405 }
3406 })
3407 } else {
3408 None
3409 };
3410
3411 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
3412 let pk_column_lit = pk_column.as_str();
3413 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
3420 let col_lit = col;
3421 quote! {
3422 pub async fn soft_delete_on #executor_generics (
3432 &self,
3433 #executor_param,
3434 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3435 #executor_where
3436 {
3437 let _query = ::rustango::core::UpdateQuery {
3438 model: <Self as ::rustango::core::Model>::SCHEMA,
3439 set: ::std::vec![
3440 ::rustango::core::Assignment {
3441 column: #col_lit,
3442 value: ::core::convert::Into::<::rustango::core::Expr>::into(
3443 ::core::convert::Into::<::rustango::core::SqlValue>::into(
3444 ::chrono::Utc::now()
3445 )
3446 ),
3447 },
3448 ],
3449 where_clause: ::rustango::core::WhereExpr::Predicate(
3450 ::rustango::core::Filter {
3451 column: #pk_column_lit,
3452 op: ::rustango::core::Op::Eq,
3453 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3454 ::core::clone::Clone::clone(&self.#pk_ident)
3455 ),
3456 }
3457 ),
3458 };
3459 let _affected = ::rustango::sql::__macro_internals::update_on(
3460 #executor_passes_to_data_write,
3461 &_query,
3462 ).await?;
3463 #audit_softdelete_emit
3464 ::core::result::Result::Ok(_affected)
3465 }
3466
3467 pub async fn restore_on #executor_generics (
3474 &self,
3475 #executor_param,
3476 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3477 #executor_where
3478 {
3479 let _query = ::rustango::core::UpdateQuery {
3480 model: <Self as ::rustango::core::Model>::SCHEMA,
3481 set: ::std::vec![
3482 ::rustango::core::Assignment {
3483 column: #col_lit,
3484 value: ::core::convert::Into::<::rustango::core::Expr>::into(
3485 ::rustango::core::SqlValue::Null
3486 ),
3487 },
3488 ],
3489 where_clause: ::rustango::core::WhereExpr::Predicate(
3490 ::rustango::core::Filter {
3491 column: #pk_column_lit,
3492 op: ::rustango::core::Op::Eq,
3493 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3494 ::core::clone::Clone::clone(&self.#pk_ident)
3495 ),
3496 }
3497 ),
3498 };
3499 let _affected = ::rustango::sql::__macro_internals::update_on(
3500 #executor_passes_to_data_write,
3501 &_query,
3502 ).await?;
3503 #audit_restore_emit
3504 ::core::result::Result::Ok(_affected)
3505 }
3506 }
3507 } else {
3508 quote!()
3509 };
3510 quote! {
3511 #[cfg(feature = "postgres")]
3519 pub async fn delete(
3520 &self,
3521 pool: &::rustango::sql::sqlx::PgPool,
3522 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
3523 #pool_to_delete_on
3524 }
3525
3526 #[cfg(feature = "postgres")]
3533 pub async fn delete_on #executor_generics (
3534 &self,
3535 #executor_param,
3536 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3537 #executor_where
3538 {
3539 let query = ::rustango::core::DeleteQuery {
3540 model: <Self as ::rustango::core::Model>::SCHEMA,
3541 where_clause: ::rustango::core::WhereExpr::Predicate(
3542 ::rustango::core::Filter {
3543 column: #pk_column_lit,
3544 op: ::rustango::core::Op::Eq,
3545 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3546 ::core::clone::Clone::clone(&self.#pk_ident)
3547 ),
3548 }
3549 ),
3550 };
3551 let _affected = ::rustango::sql::__macro_internals::delete_on(
3552 #executor_passes_to_data_write,
3553 &query,
3554 ).await?;
3555 #audit_delete_emit
3556 ::core::result::Result::Ok(_affected)
3557 }
3558
3559 #[cfg(feature = "postgres")]
3565 pub async fn delete_on_with #executor_generics (
3566 &self,
3567 #executor_param,
3568 source: ::rustango::audit::AuditSource,
3569 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3570 #executor_where
3571 {
3572 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
3573 }
3574 #pool_delete_method
3575 #pool_insert_method
3576 #pool_save_method
3577 #tx_delete_method
3578 #tx_insert_method
3579 #tx_save_method
3580 #soft_delete_methods
3581 }
3582 });
3583
3584 let insert_method = if fields.has_auto {
3585 let pushes = &fields.insert_pushes;
3586 let returning_cols = &fields.returning_cols;
3587 let auto_assigns = &fields.auto_assigns;
3588 quote! {
3589 #[cfg(feature = "postgres")]
3598 pub async fn insert(
3599 &mut self,
3600 pool: &::rustango::sql::sqlx::PgPool,
3601 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3602 #pool_to_insert_on
3603 }
3604
3605 #[cfg(feature = "postgres")]
3611 pub async fn insert_on #executor_generics (
3612 &mut self,
3613 #executor_param,
3614 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3615 #executor_where
3616 {
3617 let mut _columns: ::std::vec::Vec<&'static str> =
3618 ::std::vec::Vec::new();
3619 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
3620 ::std::vec::Vec::new();
3621 #( #pushes )*
3622 let query = ::rustango::core::InsertQuery {
3623 model: <Self as ::rustango::core::Model>::SCHEMA,
3624 columns: _columns,
3625 values: _values,
3626 returning: ::std::vec![ #( #returning_cols ),* ],
3627 on_conflict: ::core::option::Option::None,
3628 };
3629 let _returning_row_v = ::rustango::sql::__macro_internals::insert_returning_on(
3630 #executor_passes_to_data_write,
3631 &query,
3632 ).await?;
3633 let _returning_row = &_returning_row_v;
3634 #( #auto_assigns )*
3635 #audit_insert_emit
3636 ::core::result::Result::Ok(())
3637 }
3638
3639 #[cfg(feature = "postgres")]
3645 pub async fn insert_on_with #executor_generics (
3646 &mut self,
3647 #executor_param,
3648 source: ::rustango::audit::AuditSource,
3649 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3650 #executor_where
3651 {
3652 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
3653 }
3654 }
3655 } else {
3656 let insert_columns = &fields.insert_columns;
3657 let insert_values = &fields.insert_values;
3658 quote! {
3659 #[cfg(feature = "postgres")]
3665 pub async fn insert(
3666 &self,
3667 pool: &::rustango::sql::sqlx::PgPool,
3668 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3669 self.insert_on(pool).await
3670 }
3671
3672 #[cfg(feature = "postgres")]
3678 pub async fn insert_on<'_c, _E>(
3679 &self,
3680 _executor: _E,
3681 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3682 where
3683 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
3684 {
3685 let query = ::rustango::core::InsertQuery {
3686 model: <Self as ::rustango::core::Model>::SCHEMA,
3687 columns: ::std::vec![ #( #insert_columns ),* ],
3688 values: ::std::vec![ #( #insert_values ),* ],
3689 returning: ::std::vec::Vec::new(),
3690 on_conflict: ::core::option::Option::None,
3691 };
3692 ::rustango::sql::__macro_internals::insert_on(_executor, &query).await
3693 }
3694 }
3695 };
3696
3697 let bulk_insert_method = if fields.has_auto {
3698 let cols_no_auto = &fields.bulk_columns_no_auto;
3699 let cols_all = &fields.bulk_columns_all;
3700 let pushes_no_auto = &fields.bulk_pushes_no_auto;
3701 let pushes_all = &fields.bulk_pushes_all;
3702 let returning_cols = &fields.returning_cols;
3703 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
3704 let uniformity = &fields.bulk_auto_uniformity;
3705 let first_auto_ident = fields
3706 .first_auto_ident
3707 .as_ref()
3708 .expect("has_auto implies first_auto_ident is Some");
3709 quote! {
3710 #[cfg(feature = "postgres")]
3724 pub async fn bulk_insert(
3725 rows: &mut [Self],
3726 pool: &::rustango::sql::sqlx::PgPool,
3727 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3728 #pool_to_bulk_insert_on
3729 }
3730
3731 #[cfg(feature = "postgres")]
3737 pub async fn bulk_insert_on #executor_generics (
3738 rows: &mut [Self],
3739 #executor_param,
3740 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3741 #executor_where
3742 {
3743 if rows.is_empty() {
3744 return ::core::result::Result::Ok(());
3745 }
3746 let _first_unset = matches!(
3747 rows[0].#first_auto_ident,
3748 ::rustango::sql::Auto::Unset
3749 );
3750 #( #uniformity )*
3751
3752 let mut _all_rows: ::std::vec::Vec<
3753 ::std::vec::Vec<::rustango::core::SqlValue>,
3754 > = ::std::vec::Vec::with_capacity(rows.len());
3755 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
3756 for _row in rows.iter() {
3757 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3758 ::std::vec::Vec::new();
3759 #( #pushes_no_auto )*
3760 _all_rows.push(_row_vals);
3761 }
3762 ::std::vec![ #( #cols_no_auto ),* ]
3763 } else {
3764 for _row in rows.iter() {
3765 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3766 ::std::vec::Vec::new();
3767 #( #pushes_all )*
3768 _all_rows.push(_row_vals);
3769 }
3770 ::std::vec![ #( #cols_all ),* ]
3771 };
3772
3773 let _query = ::rustango::core::BulkInsertQuery {
3774 model: <Self as ::rustango::core::Model>::SCHEMA,
3775 columns: _columns,
3776 rows: _all_rows,
3777 returning: ::std::vec![ #( #returning_cols ),* ],
3778 on_conflict: ::core::option::Option::None,
3779 };
3780 let _returned = ::rustango::sql::__macro_internals::bulk_insert_on(
3781 #executor_passes_to_data_write,
3782 &_query,
3783 ).await?;
3784 if _returned.len() != rows.len() {
3785 return ::core::result::Result::Err(
3786 ::rustango::sql::ExecError::Sql(
3787 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
3788 expected: rows.len(),
3789 actual: _returned.len(),
3790 }
3791 )
3792 );
3793 }
3794 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
3795 #auto_assigns_for_row
3796 }
3797 #audit_bulk_insert_emit
3798 ::core::result::Result::Ok(())
3799 }
3800 }
3801 } else {
3802 let cols_all = &fields.bulk_columns_all;
3803 let pushes_all = &fields.bulk_pushes_all;
3804 quote! {
3805 #[cfg(feature = "postgres")]
3815 pub async fn bulk_insert(
3816 rows: &[Self],
3817 pool: &::rustango::sql::sqlx::PgPool,
3818 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3819 Self::bulk_insert_on(rows, pool).await
3820 }
3821
3822 #[cfg(feature = "postgres")]
3828 pub async fn bulk_insert_on<'_c, _E>(
3829 rows: &[Self],
3830 _executor: _E,
3831 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3832 where
3833 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
3834 {
3835 if rows.is_empty() {
3836 return ::core::result::Result::Ok(());
3837 }
3838 let mut _all_rows: ::std::vec::Vec<
3839 ::std::vec::Vec<::rustango::core::SqlValue>,
3840 > = ::std::vec::Vec::with_capacity(rows.len());
3841 for _row in rows.iter() {
3842 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3843 ::std::vec::Vec::new();
3844 #( #pushes_all )*
3845 _all_rows.push(_row_vals);
3846 }
3847 let _query = ::rustango::core::BulkInsertQuery {
3848 model: <Self as ::rustango::core::Model>::SCHEMA,
3849 columns: ::std::vec![ #( #cols_all ),* ],
3850 rows: _all_rows,
3851 returning: ::std::vec::Vec::new(),
3852 on_conflict: ::core::option::Option::None,
3853 };
3854 let _ = ::rustango::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
3855 ::core::result::Result::Ok(())
3856 }
3857 }
3858 };
3859
3860 let bulk_upsert_pool_method = {
3868 let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
3871 (
3872 fields.bulk_columns_no_auto.clone(),
3873 fields.bulk_pushes_no_auto.clone(),
3874 )
3875 } else {
3876 (
3877 fields.bulk_columns_all.clone(),
3878 fields.bulk_pushes_all.clone(),
3879 )
3880 };
3881 quote! {
3882 pub async fn bulk_upsert_pool(
3909 rows: &[Self],
3910 target: &[&'static str],
3911 update_cols: &[&'static str],
3912 pool: &::rustango::sql::Pool,
3913 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3914 if rows.is_empty() {
3915 return ::core::result::Result::Ok(());
3916 }
3917 let mut _all_rows: ::std::vec::Vec<
3918 ::std::vec::Vec<::rustango::core::SqlValue>,
3919 > = ::std::vec::Vec::with_capacity(rows.len());
3920 for _row in rows.iter() {
3921 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3922 ::std::vec::Vec::new();
3923 #( #upsert_pushes )*
3924 _all_rows.push(_row_vals);
3925 }
3926 let _query = ::rustango::core::BulkInsertQuery {
3927 model: <Self as ::rustango::core::Model>::SCHEMA,
3928 columns: ::std::vec![ #( #upsert_cols ),* ],
3929 rows: _all_rows,
3930 returning: ::std::vec::Vec::new(),
3931 on_conflict: ::core::option::Option::Some(
3932 ::rustango::core::ConflictClause::DoUpdate {
3933 target: target.to_vec(),
3934 update_columns: update_cols.to_vec(),
3935 }
3936 ),
3937 };
3938 ::rustango::sql::bulk_insert_pool(pool, &_query).await
3939 }
3940
3941 pub async fn bulk_insert_or_ignore_pool(
3951 rows: &[Self],
3952 pool: &::rustango::sql::Pool,
3953 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3954 if rows.is_empty() {
3955 return ::core::result::Result::Ok(());
3956 }
3957 let mut _all_rows: ::std::vec::Vec<
3958 ::std::vec::Vec<::rustango::core::SqlValue>,
3959 > = ::std::vec::Vec::with_capacity(rows.len());
3960 for _row in rows.iter() {
3961 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3962 ::std::vec::Vec::new();
3963 #( #upsert_pushes )*
3964 _all_rows.push(_row_vals);
3965 }
3966 let _query = ::rustango::core::BulkInsertQuery {
3967 model: <Self as ::rustango::core::Model>::SCHEMA,
3968 columns: ::std::vec![ #( #upsert_cols ),* ],
3969 rows: _all_rows,
3970 returning: ::std::vec::Vec::new(),
3971 on_conflict: ::core::option::Option::Some(
3972 ::rustango::core::ConflictClause::DoNothing
3973 ),
3974 };
3975 ::rustango::sql::bulk_insert_pool(pool, &_query).await
3976 }
3977 }
3978 };
3979
3980 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
3981 quote! {
3982 #[doc(hidden)]
3987 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
3988 ::core::convert::Into::<::rustango::core::SqlValue>::into(
3989 ::core::clone::Clone::clone(&self.#pk_ident)
3990 )
3991 }
3992 }
3993 });
3994
3995 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
3996 quote! {
3997 impl ::rustango::sql::HasPkValue for #struct_name {
3998 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
3999 ::core::convert::Into::<::rustango::core::SqlValue>::into(
4000 ::core::clone::Clone::clone(&self.#pk_ident)
4001 )
4002 }
4003 }
4004 }
4005 });
4006
4007 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
4008
4009 let assign_auto_pk_pool_impl = {
4015 let auto_assigns = &fields.auto_assigns;
4016 let auto_assigns_sqlite: Vec<TokenStream2> = fields
4022 .auto_field_idents
4023 .iter()
4024 .map(|(ident, column)| {
4025 quote! {
4026 self.#ident = ::rustango::sql::try_get_returning_sqlite(
4027 _returning_row, #column
4028 )?;
4029 }
4030 })
4031 .collect();
4032 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
4033 let value_ty = fields
4051 .first_auto_value_ty
4052 .as_ref()
4053 .expect("first_auto_value_ty set whenever first_auto_ident is");
4054 quote! {
4055 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
4056 ::rustango_from_mysql_auto_id(_id)?;
4057 self.#first = ::rustango::sql::Auto::Set(_converted);
4058 ::core::result::Result::Ok(())
4059 }
4060 } else {
4061 quote! {
4062 let _ = _id;
4063 ::core::result::Result::Ok(())
4064 }
4065 };
4066 quote! {
4067 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
4068 fn __rustango_assign_from_pg_row(
4069 &mut self,
4070 _returning_row: &::rustango::sql::PgReturningRow,
4071 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
4072 #( #auto_assigns )*
4073 ::core::result::Result::Ok(())
4074 }
4075 fn __rustango_assign_from_mysql_id(
4076 &mut self,
4077 _id: i64,
4078 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
4079 #mysql_body
4080 }
4081 fn __rustango_assign_from_sqlite_row(
4082 &mut self,
4083 _returning_row: &::rustango::sql::SqliteReturningRow,
4084 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
4085 #( #auto_assigns_sqlite )*
4086 ::core::result::Result::Ok(())
4087 }
4088 }
4089 }
4090 };
4091
4092 let from_aliased_row_inits = &fields.from_aliased_row_inits;
4093 let aliased_row_helper = quote! {
4094 #[doc(hidden)]
4100 #[cfg(feature = "postgres")]
4101 pub fn __rustango_from_aliased_row(
4102 row: &::rustango::sql::sqlx::postgres::PgRow,
4103 prefix: &str,
4104 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
4105 ::core::result::Result::Ok(Self {
4106 #( #from_aliased_row_inits ),*
4107 })
4108 }
4109 };
4110 let aliased_row_helper_my = quote! {
4113 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
4114 #( #from_aliased_row_inits ),*
4115 });
4116 };
4117
4118 let aliased_row_helper_sqlite = quote! {
4121 ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
4122 #( #from_aliased_row_inits ),*
4123 });
4124 };
4125
4126 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
4127 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
4128 let load_related_impl_sqlite =
4129 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
4130
4131 let extra_manager_fns: Vec<TokenStream2> = manager_fns
4137 .iter()
4138 .map(|fn_ident| {
4139 let model_name_str = struct_name.to_string();
4140 let fn_name_str = fn_ident.to_string();
4141 let doc = format!(
4142 "Custom-named QuerySet accessor for [`{model_name_str}`]. \
4143 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
4144 equivalent to `Self::objects()`. Chains with any \
4145 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
4146 extension methods."
4147 );
4148 quote! {
4149 #[doc = #doc]
4150 #[must_use]
4151 pub fn #fn_ident() -> ::rustango::query::QuerySet<#struct_name> {
4152 ::rustango::query::QuerySet::new()
4153 }
4154 }
4155 })
4156 .collect();
4157
4158 quote! {
4159 impl #struct_name {
4160 #[must_use]
4162 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
4163 ::rustango::query::QuerySet::new()
4164 }
4165
4166 #( #extra_manager_fns )*
4167
4168 #insert_method
4169
4170 #bulk_insert_method
4171
4172 #bulk_upsert_pool_method
4173
4174 #save_method
4175
4176 #pk_methods
4177
4178 #pk_value_helper
4179
4180 #aliased_row_helper
4181
4182 #column_consts
4183 }
4184
4185 #aliased_row_helper_my
4186
4187 #aliased_row_helper_sqlite
4188
4189 #load_related_impl
4190
4191 #load_related_impl_my
4192
4193 #load_related_impl_sqlite
4194
4195 #has_pk_value_impl
4196
4197 #fk_pk_access_impl
4198
4199 #assign_auto_pk_pool_impl
4200 }
4201}
4202
4203fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
4207 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
4208 let col_lit = column.as_str();
4209 quote! {
4210 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
4211 _returning_row,
4212 #col_lit,
4213 )?;
4214 }
4215 });
4216 quote! { #( #lines )* }
4217}
4218
4219fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
4221 let lines = entries.iter().map(|e| {
4222 let ident = &e.ident;
4223 let col_ty = column_type_ident(ident);
4224 quote! {
4225 #[allow(non_upper_case_globals)]
4226 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
4227 }
4228 });
4229 quote! { #(#lines)* }
4230}
4231
4232fn column_module_tokens(
4235 module_ident: &syn::Ident,
4236 struct_name: &syn::Ident,
4237 entries: &[ColumnEntry],
4238) -> TokenStream2 {
4239 let items = entries.iter().map(|e| {
4240 let col_ty = column_type_ident(&e.ident);
4241 let value_ty = &e.value_ty;
4242 let name = &e.name;
4243 let column = &e.column;
4244 let field_type_tokens = &e.field_type_tokens;
4245 quote! {
4246 #[derive(::core::clone::Clone, ::core::marker::Copy)]
4247 pub struct #col_ty;
4248
4249 impl ::rustango::core::Column for #col_ty {
4250 type Model = super::#struct_name;
4251 type Value = #value_ty;
4252 const NAME: &'static str = #name;
4253 const COLUMN: &'static str = #column;
4254 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
4255 }
4256 }
4257 });
4258 quote! {
4259 #[doc(hidden)]
4260 #[allow(non_camel_case_types, non_snake_case)]
4261 pub mod #module_ident {
4262 #[allow(unused_imports)]
4267 use super::*;
4268 #(#items)*
4269 }
4270 }
4271}
4272
4273fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
4274 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
4275}
4276
4277fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
4278 syn::Ident::new(
4279 &format!("__rustango_cols_{struct_name}"),
4280 struct_name.span(),
4281 )
4282}
4283
4284fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
4285 quote! {
4296 #[cfg(feature = "postgres")]
4297 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
4298 for #struct_name
4299 {
4300 fn from_row(
4301 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
4302 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
4303 ::core::result::Result::Ok(Self {
4304 #( #from_row_inits ),*
4305 })
4306 }
4307 }
4308
4309 ::rustango::__impl_my_from_row!(#struct_name, |row| {
4310 #( #from_row_inits ),*
4311 });
4312
4313 ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
4314 #( #from_row_inits ),*
4315 });
4316 }
4317}
4318
4319struct ContainerAttrs {
4320 table: Option<String>,
4321 display: Option<(String, proc_macro2::Span)>,
4322 app: Option<String>,
4329 admin: Option<AdminAttrs>,
4334 audit: Option<AuditAttrs>,
4340 permissions: bool,
4344 m2m: Vec<M2MAttr>,
4348 indexes: Vec<IndexAttr>,
4354 checks: Vec<CheckAttr>,
4357 composite_fks: Vec<CompositeFkAttr>,
4361 generic_fks: Vec<GenericFkAttr>,
4365 scope: Option<String>,
4371 manager_ext: Option<syn::Ident>,
4378 manager_fns: Vec<syn::Ident>,
4383 default_order: Vec<(String, bool, proc_macro2::Span)>,
4389 is_view: bool,
4394 verbose_name: Option<String>,
4400 verbose_name_plural: Option<String>,
4404}
4405
4406struct IndexAttr {
4408 name: Option<String>,
4410 columns: Vec<String>,
4412 unique: bool,
4414 method: String,
4420 where_clause: Option<String>,
4424}
4425
4426struct CheckAttr {
4428 name: String,
4429 expr: String,
4430}
4431
4432struct CompositeFkAttr {
4438 name: String,
4440 to: String,
4442 from: Vec<String>,
4444 on: Vec<String>,
4446}
4447
4448struct GenericFkAttr {
4453 name: String,
4455 ct_column: String,
4457 pk_column: String,
4459}
4460
4461struct M2MAttr {
4463 name: String,
4465 to: String,
4467 through: String,
4469 src: String,
4471 dst: String,
4473 auto_create: bool,
4478}
4479
4480#[derive(Default)]
4486struct AuditAttrs {
4487 track: Option<(Vec<String>, proc_macro2::Span)>,
4491}
4492
4493#[derive(Default)]
4498struct AdminAttrs {
4499 list_display: Option<(Vec<String>, proc_macro2::Span)>,
4500 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
4501 list_per_page: Option<usize>,
4502 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
4503 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
4504 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
4505 actions: Option<(Vec<String>, proc_macro2::Span)>,
4508 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
4512 list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
4516 search_help_text: Option<String>,
4520 actions_on_top: Option<bool>,
4523 actions_on_bottom: Option<bool>,
4527 date_hierarchy: Option<String>,
4532 prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
4538 raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
4542 autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
4547 list_select_related: Option<String>,
4552 formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
4558}
4559
4560fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
4561 let mut out = ContainerAttrs {
4562 table: None,
4563 display: None,
4564 app: None,
4565 admin: None,
4566 audit: None,
4567 permissions: true,
4576 m2m: Vec::new(),
4577 indexes: Vec::new(),
4578 checks: Vec::new(),
4579 composite_fks: Vec::new(),
4580 generic_fks: Vec::new(),
4581 scope: None,
4582 manager_ext: None,
4583 manager_fns: Vec::new(),
4584 default_order: Vec::new(),
4585 is_view: false,
4586 verbose_name: None,
4587 verbose_name_plural: None,
4588 };
4589 for attr in &input.attrs {
4590 if !attr.path().is_ident("rustango") {
4591 continue;
4592 }
4593 attr.parse_nested_meta(|meta| {
4594 if meta.path.is_ident("table") {
4595 let s: LitStr = meta.value()?.parse()?;
4596 let name = s.value();
4597 validate_table_name(&name, s.span())?;
4607 out.table = Some(name);
4608 return Ok(());
4609 }
4610 if meta.path.is_ident("display") {
4611 let s: LitStr = meta.value()?.parse()?;
4612 out.display = Some((s.value(), s.span()));
4613 return Ok(());
4614 }
4615 if meta.path.is_ident("app") {
4616 let s: LitStr = meta.value()?.parse()?;
4617 out.app = Some(s.value());
4618 return Ok(());
4619 }
4620 if meta.path.is_ident("scope") {
4621 let s: LitStr = meta.value()?.parse()?;
4622 let val = s.value();
4623 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
4624 return Err(meta.error(format!(
4625 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
4626 )));
4627 }
4628 out.scope = Some(val);
4629 return Ok(());
4630 }
4631 if meta.path.is_ident("admin") {
4632 let mut admin = AdminAttrs::default();
4633 meta.parse_nested_meta(|inner| {
4634 if inner.path.is_ident("list_display") {
4635 let s: LitStr = inner.value()?.parse()?;
4636 admin.list_display =
4637 Some((split_field_list(&s.value()), s.span()));
4638 return Ok(());
4639 }
4640 if inner.path.is_ident("search_fields") {
4641 let s: LitStr = inner.value()?.parse()?;
4642 admin.search_fields =
4643 Some((split_field_list(&s.value()), s.span()));
4644 return Ok(());
4645 }
4646 if inner.path.is_ident("readonly_fields") {
4647 let s: LitStr = inner.value()?.parse()?;
4648 admin.readonly_fields =
4649 Some((split_field_list(&s.value()), s.span()));
4650 return Ok(());
4651 }
4652 if inner.path.is_ident("list_per_page") {
4653 let lit: syn::LitInt = inner.value()?.parse()?;
4654 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
4655 return Ok(());
4656 }
4657 if inner.path.is_ident("ordering") {
4658 let s: LitStr = inner.value()?.parse()?;
4659 admin.ordering = Some((
4660 parse_ordering_list(&s.value()),
4661 s.span(),
4662 ));
4663 return Ok(());
4664 }
4665 if inner.path.is_ident("list_filter") {
4666 let s: LitStr = inner.value()?.parse()?;
4667 admin.list_filter =
4668 Some((split_field_list(&s.value()), s.span()));
4669 return Ok(());
4670 }
4671 if inner.path.is_ident("actions") {
4672 let s: LitStr = inner.value()?.parse()?;
4673 admin.actions =
4674 Some((split_field_list(&s.value()), s.span()));
4675 return Ok(());
4676 }
4677 if inner.path.is_ident("fieldsets") {
4678 let s: LitStr = inner.value()?.parse()?;
4679 admin.fieldsets =
4680 Some((parse_fieldset_list(&s.value()), s.span()));
4681 return Ok(());
4682 }
4683 if inner.path.is_ident("list_display_links") {
4684 let s: LitStr = inner.value()?.parse()?;
4685 admin.list_display_links =
4686 Some((split_field_list(&s.value()), s.span()));
4687 return Ok(());
4688 }
4689 if inner.path.is_ident("search_help_text") {
4690 let s: LitStr = inner.value()?.parse()?;
4691 admin.search_help_text = Some(s.value());
4692 return Ok(());
4693 }
4694 if inner.path.is_ident("actions_on_top") {
4695 let lit: syn::LitBool = inner.value()?.parse()?;
4696 admin.actions_on_top = Some(lit.value);
4697 return Ok(());
4698 }
4699 if inner.path.is_ident("actions_on_bottom") {
4700 let lit: syn::LitBool = inner.value()?.parse()?;
4701 admin.actions_on_bottom = Some(lit.value);
4702 return Ok(());
4703 }
4704 if inner.path.is_ident("date_hierarchy") {
4705 let s: LitStr = inner.value()?.parse()?;
4706 admin.date_hierarchy = Some(s.value());
4707 return Ok(());
4708 }
4709 if inner.path.is_ident("prepopulated_fields") {
4710 let s: LitStr = inner.value()?.parse()?;
4711 admin.prepopulated_fields =
4712 Some((parse_prepopulated_list(&s.value()), s.span()));
4713 return Ok(());
4714 }
4715 if inner.path.is_ident("raw_id_fields") {
4716 let s: LitStr = inner.value()?.parse()?;
4717 admin.raw_id_fields =
4718 Some((split_field_list(&s.value()), s.span()));
4719 return Ok(());
4720 }
4721 if inner.path.is_ident("autocomplete_fields") {
4722 let s: LitStr = inner.value()?.parse()?;
4723 admin.autocomplete_fields =
4724 Some((split_field_list(&s.value()), s.span()));
4725 return Ok(());
4726 }
4727 if inner.path.is_ident("list_select_related") {
4728 let s: LitStr = inner.value()?.parse()?;
4729 admin.list_select_related = Some(s.value());
4730 return Ok(());
4731 }
4732 if inner.path.is_ident("formfield_overrides") {
4733 let s: LitStr = inner.value()?.parse()?;
4734 admin.formfield_overrides =
4735 Some((parse_formfield_overrides(&s.value()), s.span()));
4736 return Ok(());
4737 }
4738 Err(inner.error(
4739 "unknown admin attribute (supported: \
4740 `list_display`, `list_display_links`, \
4741 `search_fields`, `search_help_text`, \
4742 `readonly_fields`, \
4743 `list_filter`, `list_per_page`, `ordering`, `actions`, \
4744 `actions_on_top`, `actions_on_bottom`, \
4745 `date_hierarchy`, \
4746 `prepopulated_fields`, \
4747 `raw_id_fields`, \
4748 `autocomplete_fields`, \
4749 `list_select_related`, \
4750 `formfield_overrides`, \
4751 `fieldsets`)",
4752 ))
4753 })?;
4754 out.admin = Some(admin);
4755 return Ok(());
4756 }
4757 if meta.path.is_ident("manager") {
4758 meta.parse_nested_meta(|inner| {
4763 if inner.path.is_ident("ext") {
4764 let s: LitStr = inner.value()?.parse()?;
4765 let name = s.value();
4766 if name.is_empty() {
4767 return Err(inner.error("manager(ext = \"...\") cannot be empty"));
4768 }
4769 out.manager_ext =
4770 Some(syn::Ident::new(&name, s.span()));
4771 return Ok(());
4772 }
4773 Err(inner.error(
4774 "unknown manager attribute (supported: `ext = \"TraitName\"`)",
4775 ))
4776 })?;
4777 return Ok(());
4778 }
4779 if meta.path.is_ident("manager_fn") {
4780 let s: LitStr = meta.value()?.parse()?;
4785 let name = s.value();
4786 if name.is_empty() {
4787 return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
4788 }
4789 if name == "objects" {
4790 return Err(meta.error(
4791 "`manager_fn = \"objects\"` collides with the default \
4792 accessor — pick a different name",
4793 ));
4794 }
4795 let ident = syn::Ident::new(&name, s.span());
4796 if out.manager_fns.iter().any(|prev| *prev == ident) {
4797 return Err(meta.error(format!(
4798 "duplicate `manager_fn = \"{name}\"`"
4799 )));
4800 }
4801 out.manager_fns.push(ident);
4802 return Ok(());
4803 }
4804 if meta.path.is_ident("default_order") {
4805 let s: LitStr = meta.value()?.parse()?;
4810 let raw = s.value();
4811 let span = s.span();
4812 let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
4813 Vec::new();
4814 for entry in raw.split(',') {
4815 let trimmed = entry.trim();
4816 if trimmed.is_empty() {
4817 return Err(syn::Error::new(
4818 span,
4819 "`default_order = \"...\"` has an empty entry — \
4820 check for a stray comma",
4821 ));
4822 }
4823 let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
4824 (true, rest.trim().to_owned())
4825 } else if let Some(rest) = trimmed.strip_prefix('+') {
4826 (false, rest.trim().to_owned())
4827 } else {
4828 (false, trimmed.to_owned())
4829 };
4830 if name.is_empty() {
4831 return Err(syn::Error::new(
4832 span,
4833 "`default_order` entry has no column name after the prefix",
4834 ));
4835 }
4836 if parsed.iter().any(|(n, _, _)| *n == name) {
4837 return Err(syn::Error::new(
4838 span,
4839 format!("duplicate column `{name}` in `default_order`"),
4840 ));
4841 }
4842 parsed.push((name, desc, span));
4843 }
4844 if parsed.is_empty() {
4845 return Err(syn::Error::new(
4846 span,
4847 "`default_order = \"...\"` cannot be empty",
4848 ));
4849 }
4850 out.default_order = parsed;
4851 return Ok(());
4852 }
4853 if meta.path.is_ident("audit") {
4854 let mut audit = AuditAttrs::default();
4855 meta.parse_nested_meta(|inner| {
4856 if inner.path.is_ident("track") {
4857 let s: LitStr = inner.value()?.parse()?;
4858 audit.track =
4859 Some((split_field_list(&s.value()), s.span()));
4860 return Ok(());
4861 }
4862 Err(inner.error(
4863 "unknown audit attribute (supported: `track`)",
4864 ))
4865 })?;
4866 out.audit = Some(audit);
4867 return Ok(());
4868 }
4869 if meta.path.is_ident("permissions") {
4870 if let Ok(v) = meta.value() {
4875 let lit: syn::LitBool = v.parse()?;
4876 out.permissions = lit.value;
4877 } else {
4878 out.permissions = true;
4879 }
4880 return Ok(());
4881 }
4882 if meta.path.is_ident("view") {
4883 if let Ok(v) = meta.value() {
4889 let lit: syn::LitBool = v.parse()?;
4890 out.is_view = lit.value;
4891 } else {
4892 out.is_view = true;
4893 }
4894 return Ok(());
4895 }
4896 if meta.path.is_ident("verbose_name") {
4897 let s: LitStr = meta.value()?.parse()?;
4898 out.verbose_name = Some(s.value());
4899 return Ok(());
4900 }
4901 if meta.path.is_ident("verbose_name_plural") {
4902 let s: LitStr = meta.value()?.parse()?;
4903 out.verbose_name_plural = Some(s.value());
4904 return Ok(());
4905 }
4906 if meta.path.is_ident("unique_together") {
4907 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
4916 out.indexes.push(IndexAttr {
4917 name,
4918 columns,
4919 unique: true,
4920 method: "btree".to_owned(),
4921 where_clause: None,
4922 });
4923 return Ok(());
4924 }
4925 if meta.path.is_ident("index_together") {
4926 let (columns, name) = parse_together_attr(&meta, "index_together")?;
4932 out.indexes.push(IndexAttr {
4933 name,
4934 columns,
4935 unique: false,
4936 method: "btree".to_owned(),
4937 where_clause: None,
4938 });
4939 return Ok(());
4940 }
4941 if meta.path.is_ident("unique_when") {
4942 let mut columns: Option<Vec<String>> = None;
4957 let mut condition: Option<String> = None;
4958 let mut name: Option<String> = None;
4959 meta.parse_nested_meta(|inner| {
4960 if inner.path.is_ident("columns") {
4961 let s: LitStr = inner.value()?.parse()?;
4962 columns = Some(split_field_list(&s.value()));
4963 return Ok(());
4964 }
4965 if inner.path.is_ident("condition") {
4966 let s: LitStr = inner.value()?.parse()?;
4967 condition = Some(s.value());
4968 return Ok(());
4969 }
4970 if inner.path.is_ident("name") {
4971 let s: LitStr = inner.value()?.parse()?;
4972 name = Some(s.value());
4973 return Ok(());
4974 }
4975 Err(inner.error(
4976 "unknown unique_when attribute (supported: \
4977 `columns = \"...\"`, `condition = \"...\"`, \
4978 `name = \"...\"`)",
4979 ))
4980 })?;
4981 let columns = columns.ok_or_else(|| {
4982 meta.error("`unique_when(...)` requires `columns = \"...\"`")
4983 })?;
4984 let condition = condition.ok_or_else(|| {
4985 meta.error("`unique_when(...)` requires `condition = \"...\"`")
4986 })?;
4987 if columns.is_empty() {
4988 return Err(meta.error("`unique_when(columns = \"\")` is empty"));
4989 }
4990 out.indexes.push(IndexAttr {
4991 name,
4992 columns,
4993 unique: true,
4994 method: "btree".to_owned(),
4995 where_clause: Some(condition),
4996 });
4997 return Ok(());
4998 }
4999 if meta.path.is_ident("index") {
5000 let cols_lit: LitStr = meta.value()?.parse()?;
5008 let columns = split_field_list(&cols_lit.value());
5009 out.indexes.push(IndexAttr {
5010 name: None,
5011 columns,
5012 unique: false,
5013 method: "btree".to_owned(),
5014 where_clause: None,
5015 });
5016 return Ok(());
5017 }
5018 if meta.path.is_ident("check") {
5019 let mut name: Option<String> = None;
5021 let mut expr: Option<String> = None;
5022 meta.parse_nested_meta(|inner| {
5023 if inner.path.is_ident("name") {
5024 let s: LitStr = inner.value()?.parse()?;
5025 name = Some(s.value());
5026 return Ok(());
5027 }
5028 if inner.path.is_ident("expr") {
5029 let s: LitStr = inner.value()?.parse()?;
5030 expr = Some(s.value());
5031 return Ok(());
5032 }
5033 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
5034 })?;
5035 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
5036 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
5037 out.checks.push(CheckAttr { name, expr });
5038 return Ok(());
5039 }
5040 if meta.path.is_ident("generic_fk") {
5041 let mut gfk = GenericFkAttr {
5042 name: String::new(),
5043 ct_column: String::new(),
5044 pk_column: String::new(),
5045 };
5046 meta.parse_nested_meta(|inner| {
5047 if inner.path.is_ident("name") {
5048 let s: LitStr = inner.value()?.parse()?;
5049 gfk.name = s.value();
5050 return Ok(());
5051 }
5052 if inner.path.is_ident("ct_column") {
5053 let s: LitStr = inner.value()?.parse()?;
5054 gfk.ct_column = s.value();
5055 return Ok(());
5056 }
5057 if inner.path.is_ident("pk_column") {
5058 let s: LitStr = inner.value()?.parse()?;
5059 gfk.pk_column = s.value();
5060 return Ok(());
5061 }
5062 Err(inner.error(
5063 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
5064 ))
5065 })?;
5066 if gfk.name.is_empty() {
5067 return Err(meta.error("generic_fk requires `name = \"...\"`"));
5068 }
5069 if gfk.ct_column.is_empty() {
5070 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
5071 }
5072 if gfk.pk_column.is_empty() {
5073 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
5074 }
5075 out.generic_fks.push(gfk);
5076 return Ok(());
5077 }
5078 if meta.path.is_ident("fk_composite") {
5079 let mut fk = CompositeFkAttr {
5080 name: String::new(),
5081 to: String::new(),
5082 from: Vec::new(),
5083 on: Vec::new(),
5084 };
5085 meta.parse_nested_meta(|inner| {
5086 if inner.path.is_ident("name") {
5087 let s: LitStr = inner.value()?.parse()?;
5088 fk.name = s.value();
5089 return Ok(());
5090 }
5091 if inner.path.is_ident("to") {
5092 let s: LitStr = inner.value()?.parse()?;
5093 fk.to = s.value();
5094 return Ok(());
5095 }
5096 if inner.path.is_ident("on") || inner.path.is_ident("from") {
5099 let value = inner.value()?;
5100 let content;
5101 syn::parenthesized!(content in value);
5102 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
5103 content.parse_terminated(
5104 |p| p.parse::<syn::LitStr>(),
5105 syn::Token![,],
5106 )?;
5107 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
5108 if inner.path.is_ident("on") {
5109 fk.on = cols;
5110 } else {
5111 fk.from = cols;
5112 }
5113 return Ok(());
5114 }
5115 Err(inner.error(
5116 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
5117 ))
5118 })?;
5119 if fk.name.is_empty() {
5120 return Err(meta.error("fk_composite requires `name = \"...\"`"));
5121 }
5122 if fk.to.is_empty() {
5123 return Err(meta.error("fk_composite requires `to = \"...\"`"));
5124 }
5125 if fk.from.is_empty() || fk.on.is_empty() {
5126 return Err(meta.error(
5127 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
5128 ));
5129 }
5130 if fk.from.len() != fk.on.len() {
5131 return Err(meta.error(format!(
5132 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
5133 fk.from.len(),
5134 fk.on.len(),
5135 )));
5136 }
5137 out.composite_fks.push(fk);
5138 return Ok(());
5139 }
5140 if meta.path.is_ident("m2m") {
5141 let mut m2m = M2MAttr {
5142 name: String::new(),
5143 to: String::new(),
5144 through: String::new(),
5145 src: String::new(),
5146 dst: String::new(),
5147 auto_create: true,
5148 };
5149 meta.parse_nested_meta(|inner| {
5150 if inner.path.is_ident("name") {
5151 let s: LitStr = inner.value()?.parse()?;
5152 m2m.name = s.value();
5153 return Ok(());
5154 }
5155 if inner.path.is_ident("to") {
5156 let s: LitStr = inner.value()?.parse()?;
5157 m2m.to = s.value();
5158 return Ok(());
5159 }
5160 if inner.path.is_ident("through") {
5161 let s: LitStr = inner.value()?.parse()?;
5162 m2m.through = s.value();
5163 return Ok(());
5164 }
5165 if inner.path.is_ident("src") {
5166 let s: LitStr = inner.value()?.parse()?;
5167 m2m.src = s.value();
5168 return Ok(());
5169 }
5170 if inner.path.is_ident("dst") {
5171 let s: LitStr = inner.value()?.parse()?;
5172 m2m.dst = s.value();
5173 return Ok(());
5174 }
5175 if inner.path.is_ident("auto_create") {
5176 let lit: syn::LitBool = inner.value()?.parse()?;
5177 m2m.auto_create = lit.value;
5178 return Ok(());
5179 }
5180 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
5181 })?;
5182 if m2m.name.is_empty() {
5183 return Err(meta.error("m2m requires `name = \"...\"`"));
5184 }
5185 if m2m.to.is_empty() {
5186 return Err(meta.error("m2m requires `to = \"...\"`"));
5187 }
5188 if m2m.through.is_empty() {
5189 return Err(meta.error("m2m requires `through = \"...\"`"));
5190 }
5191 if m2m.src.is_empty() {
5192 return Err(meta.error("m2m requires `src = \"...\"`"));
5193 }
5194 if m2m.dst.is_empty() {
5195 return Err(meta.error("m2m requires `dst = \"...\"`"));
5196 }
5197 out.m2m.push(m2m);
5198 return Ok(());
5199 }
5200 Err(meta.error("unknown rustango container attribute"))
5201 })?;
5202 }
5203 Ok(out)
5204}
5205
5206fn split_field_list(raw: &str) -> Vec<String> {
5210 raw.split(',')
5211 .map(str::trim)
5212 .filter(|s| !s.is_empty())
5213 .map(str::to_owned)
5214 .collect()
5215}
5216
5217fn parse_together_attr(
5225 meta: &syn::meta::ParseNestedMeta<'_>,
5226 attr: &str,
5227) -> syn::Result<(Vec<String>, Option<String>)> {
5228 if meta.input.peek(syn::Token![=]) {
5231 let cols_lit: LitStr = meta.value()?.parse()?;
5232 let columns = split_field_list(&cols_lit.value());
5233 check_together_columns(meta, attr, &columns)?;
5234 return Ok((columns, None));
5235 }
5236 let mut columns: Option<Vec<String>> = None;
5237 let mut name: Option<String> = None;
5238 meta.parse_nested_meta(|inner| {
5239 if inner.path.is_ident("columns") {
5240 let s: LitStr = inner.value()?.parse()?;
5241 columns = Some(split_field_list(&s.value()));
5242 return Ok(());
5243 }
5244 if inner.path.is_ident("name") {
5245 let s: LitStr = inner.value()?.parse()?;
5246 name = Some(s.value());
5247 return Ok(());
5248 }
5249 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
5250 })?;
5251 let columns = columns.ok_or_else(|| {
5252 meta.error(format!(
5253 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
5254 ))
5255 })?;
5256 check_together_columns(meta, attr, &columns)?;
5257 Ok((columns, name))
5258}
5259
5260fn check_together_columns(
5261 meta: &syn::meta::ParseNestedMeta<'_>,
5262 attr: &str,
5263 columns: &[String],
5264) -> syn::Result<()> {
5265 if columns.len() < 2 {
5266 let single = if attr == "unique_together" {
5267 "#[rustango(unique)] on the field"
5268 } else {
5269 "#[rustango(index)] on the field"
5270 };
5271 return Err(meta.error(format!(
5272 "{attr} expects two or more columns; for a single-column equivalent use {single}",
5273 )));
5274 }
5275 Ok(())
5276}
5277
5278fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
5287 raw.split('|')
5288 .map(str::trim)
5289 .filter(|s| !s.is_empty())
5290 .map(|section| {
5291 let (title, rest) = match section.split_once(':') {
5293 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
5294 _ => (String::new(), section),
5295 };
5296 let fields = split_field_list(rest);
5297 (title, fields)
5298 })
5299 .collect()
5300}
5301
5302fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
5307 raw.split(',')
5308 .map(str::trim)
5309 .filter(|s| !s.is_empty())
5310 .filter_map(|entry| {
5311 let (target, sources_raw) = entry.split_once(':')?;
5312 let target = target.trim().to_owned();
5313 if target.is_empty() {
5314 return None;
5315 }
5316 let sources: Vec<String> = sources_raw
5317 .split('+')
5318 .map(|s| s.trim().to_owned())
5319 .filter(|s| !s.is_empty())
5320 .collect();
5321 if sources.is_empty() {
5322 return None;
5323 }
5324 Some((target, sources))
5325 })
5326 .collect()
5327}
5328
5329fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
5335 raw.split(',')
5336 .map(str::trim)
5337 .filter(|s| !s.is_empty())
5338 .filter_map(|entry| {
5339 let (field, widget) = entry.split_once(':')?;
5340 let field = field.trim().to_owned();
5341 let widget = widget.trim().to_owned();
5342 if field.is_empty() || widget.is_empty() {
5343 return None;
5344 }
5345 Some((field, widget))
5346 })
5347 .collect()
5348}
5349
5350fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
5353 raw.split(',')
5354 .map(str::trim)
5355 .filter(|s| !s.is_empty())
5356 .map(|spec| {
5357 spec.strip_prefix('-')
5358 .map_or((spec.to_owned(), false), |rest| {
5359 (rest.trim().to_owned(), true)
5360 })
5361 })
5362 .collect()
5363}
5364
5365struct FieldAttrs {
5366 column: Option<String>,
5367 primary_key: bool,
5368 fk: Option<String>,
5369 o2o: Option<String>,
5370 on: Option<String>,
5371 max_length: Option<u32>,
5372 min: Option<i64>,
5373 max: Option<i64>,
5374 default: Option<String>,
5375 auto_uuid: bool,
5381 auto_now_add: bool,
5386 auto_now: bool,
5392 soft_delete: bool,
5397 unique: bool,
5400 index: bool,
5404 index_unique: bool,
5405 index_name: Option<String>,
5406 index_method: String,
5409 generated_as: Option<String>,
5415 help_text: Option<String>,
5420 choices: Option<Vec<(String, String)>>,
5428 db_comment: Option<String>,
5434 verbose_name: Option<String>,
5440 editable: bool,
5447 blank: bool,
5451 validators: Vec<String>,
5456}
5457
5458fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
5459 let mut out = FieldAttrs {
5460 column: None,
5461 primary_key: false,
5462 fk: None,
5463 o2o: None,
5464 on: None,
5465 max_length: None,
5466 min: None,
5467 max: None,
5468 default: None,
5469 auto_uuid: false,
5470 auto_now_add: false,
5471 auto_now: false,
5472 soft_delete: false,
5473 unique: false,
5474 index: false,
5475 index_unique: false,
5476 index_name: None,
5477 index_method: "btree".to_owned(),
5478 generated_as: None,
5479 help_text: None,
5480 choices: None,
5481 db_comment: None,
5482 verbose_name: None,
5483 editable: true,
5484 blank: false,
5485 validators: Vec::new(),
5486 };
5487 for attr in &field.attrs {
5488 if !attr.path().is_ident("rustango") {
5489 continue;
5490 }
5491 attr.parse_nested_meta(|meta| {
5492 if meta.path.is_ident("column") {
5493 let s: LitStr = meta.value()?.parse()?;
5494 let name = s.value();
5495 validate_sql_identifier(&name, "column", s.span())?;
5496 out.column = Some(name);
5497 return Ok(());
5498 }
5499 if meta.path.is_ident("primary_key") {
5500 out.primary_key = true;
5501 return Ok(());
5502 }
5503 if meta.path.is_ident("fk") {
5504 let s: LitStr = meta.value()?.parse()?;
5505 out.fk = Some(s.value());
5506 return Ok(());
5507 }
5508 if meta.path.is_ident("o2o") {
5509 let s: LitStr = meta.value()?.parse()?;
5510 out.o2o = Some(s.value());
5511 return Ok(());
5512 }
5513 if meta.path.is_ident("on") {
5514 let s: LitStr = meta.value()?.parse()?;
5515 out.on = Some(s.value());
5516 return Ok(());
5517 }
5518 if meta.path.is_ident("max_length") {
5519 let lit: syn::LitInt = meta.value()?.parse()?;
5520 out.max_length = Some(lit.base10_parse::<u32>()?);
5521 return Ok(());
5522 }
5523 if meta.path.is_ident("min") {
5524 out.min = Some(parse_signed_i64(&meta)?);
5525 return Ok(());
5526 }
5527 if meta.path.is_ident("max") {
5528 out.max = Some(parse_signed_i64(&meta)?);
5529 return Ok(());
5530 }
5531 if meta.path.is_ident("default") {
5532 let s: LitStr = meta.value()?.parse()?;
5533 out.default = Some(s.value());
5534 return Ok(());
5535 }
5536 if meta.path.is_ident("generated_as") {
5537 let s: LitStr = meta.value()?.parse()?;
5538 out.generated_as = Some(s.value());
5539 return Ok(());
5540 }
5541 if meta.path.is_ident("help_text") {
5542 let s: LitStr = meta.value()?.parse()?;
5543 out.help_text = Some(s.value());
5544 return Ok(());
5545 }
5546 if meta.path.is_ident("choices") {
5547 let s: LitStr = meta.value()?.parse()?;
5548 let raw = s.value();
5549 let mut pairs: Vec<(String, String)> = Vec::new();
5550 for chunk in raw.split(',') {
5551 let chunk = chunk.trim();
5552 if chunk.is_empty() {
5553 continue;
5554 }
5555 let (value, label) = match chunk.split_once(':') {
5556 Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
5557 None => (chunk.to_owned(), chunk.to_owned()),
5558 };
5559 if value.is_empty() {
5560 return Err(syn::Error::new(
5561 s.span(),
5562 "`choices` entry has empty value before `:`",
5563 ));
5564 }
5565 pairs.push((value, label));
5566 }
5567 if pairs.is_empty() {
5568 return Err(syn::Error::new(
5569 s.span(),
5570 "`choices = \"…\"` must contain at least one value",
5571 ));
5572 }
5573 out.choices = Some(pairs);
5574 return Ok(());
5575 }
5576 if meta.path.is_ident("db_comment") {
5577 let s: LitStr = meta.value()?.parse()?;
5578 out.db_comment = Some(s.value());
5579 return Ok(());
5580 }
5581 if meta.path.is_ident("verbose_name") {
5582 let s: LitStr = meta.value()?.parse()?;
5583 out.verbose_name = Some(s.value());
5584 return Ok(());
5585 }
5586 if meta.path.is_ident("editable") {
5587 if let Ok(v) = meta.value() {
5592 let lit: syn::LitBool = v.parse()?;
5593 out.editable = lit.value;
5594 } else {
5595 out.editable = true;
5596 }
5597 return Ok(());
5598 }
5599 if meta.path.is_ident("blank") {
5600 if let Ok(v) = meta.value() {
5604 let lit: syn::LitBool = v.parse()?;
5605 out.blank = lit.value;
5606 } else {
5607 out.blank = true;
5608 }
5609 return Ok(());
5610 }
5611 if meta.path.is_ident("validators") {
5612 let s: LitStr = meta.value()?.parse()?;
5613 let raw = s.value();
5614 out.validators = raw
5615 .split(',')
5616 .map(str::trim)
5617 .filter(|s| !s.is_empty())
5618 .map(str::to_owned)
5619 .collect();
5620 if out.validators.is_empty() {
5621 return Err(syn::Error::new(
5622 s.span(),
5623 "`validators = \"…\"` must list at least one name",
5624 ));
5625 }
5626 return Ok(());
5627 }
5628 if meta.path.is_ident("auto_uuid") {
5629 out.auto_uuid = true;
5630 out.primary_key = true;
5634 if out.default.is_none() {
5635 out.default = Some("gen_random_uuid()".into());
5636 }
5637 return Ok(());
5638 }
5639 if meta.path.is_ident("auto_now_add") {
5640 out.auto_now_add = true;
5641 if out.default.is_none() {
5642 out.default = Some("now()".into());
5643 }
5644 return Ok(());
5645 }
5646 if meta.path.is_ident("auto_now") {
5647 out.auto_now = true;
5648 if out.default.is_none() {
5649 out.default = Some("now()".into());
5650 }
5651 return Ok(());
5652 }
5653 if meta.path.is_ident("soft_delete") {
5654 out.soft_delete = true;
5655 return Ok(());
5656 }
5657 if meta.path.is_ident("unique") {
5658 out.unique = true;
5659 return Ok(());
5660 }
5661 if meta.path.is_ident("index") {
5662 out.index = true;
5663 if meta.input.peek(syn::token::Paren) {
5665 meta.parse_nested_meta(|inner| {
5666 if inner.path.is_ident("unique") {
5667 out.index_unique = true;
5668 return Ok(());
5669 }
5670 if inner.path.is_ident("name") {
5671 let s: LitStr = inner.value()?.parse()?;
5672 out.index_name = Some(s.value());
5673 return Ok(());
5674 }
5675 if inner.path.is_ident("method") {
5676 let s: LitStr = inner.value()?.parse()?;
5677 let v = s.value();
5678 match v.as_str() {
5679 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
5680 out.index_method = v;
5681 }
5682 other => {
5683 return Err(inner.error(format!(
5684 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
5685 )));
5686 }
5687 }
5688 return Ok(());
5689 }
5690 Err(inner.error(
5691 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
5692 ))
5693 })?;
5694 }
5695 return Ok(());
5696 }
5697 Err(meta.error("unknown rustango field attribute"))
5698 })?;
5699 }
5700 Ok(out)
5701}
5702
5703fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
5705 let expr: syn::Expr = meta.value()?.parse()?;
5706 match expr {
5707 syn::Expr::Lit(syn::ExprLit {
5708 lit: syn::Lit::Int(lit),
5709 ..
5710 }) => lit.base10_parse::<i64>(),
5711 syn::Expr::Unary(syn::ExprUnary {
5712 op: syn::UnOp::Neg(_),
5713 expr,
5714 ..
5715 }) => {
5716 if let syn::Expr::Lit(syn::ExprLit {
5717 lit: syn::Lit::Int(lit),
5718 ..
5719 }) = *expr
5720 {
5721 let v: i64 = lit.base10_parse()?;
5722 Ok(-v)
5723 } else {
5724 Err(syn::Error::new_spanned(expr, "expected integer literal"))
5725 }
5726 }
5727 other => Err(syn::Error::new_spanned(
5728 other,
5729 "expected integer literal (signed)",
5730 )),
5731 }
5732}
5733
5734struct FieldInfo<'a> {
5735 ident: &'a syn::Ident,
5736 column: String,
5737 primary_key: bool,
5738 auto: bool,
5742 value_ty: &'a Type,
5745 field_type_tokens: TokenStream2,
5747 schema: TokenStream2,
5748 from_row_init: TokenStream2,
5749 from_aliased_row_init: TokenStream2,
5755 fk_inner: Option<Type>,
5759 fk_pk_kind: DetectedKind,
5765 nullable: bool,
5773 auto_now: bool,
5779 auto_now_add: bool,
5785 soft_delete: bool,
5790 generated_as: Option<String>,
5795}
5796
5797fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
5811 validate_sql_identifier(name, "table", span)
5812}
5813
5814fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
5819 if name.is_empty() {
5820 return Err(syn::Error::new(
5821 span,
5822 format!("`{kind} = \"\"` is not a valid SQL identifier"),
5823 ));
5824 }
5825 let mut chars = name.chars();
5826 let first = chars.next().unwrap();
5827 if !(first.is_ascii_alphabetic() || first == '_') {
5828 return Err(syn::Error::new(
5829 span,
5830 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
5831 ));
5832 }
5833 for c in chars {
5834 if !(c.is_ascii_alphanumeric() || c == '_') {
5835 return Err(syn::Error::new(
5836 span,
5837 format!(
5838 "{kind} name `{name}` contains invalid character {c:?} — \
5839 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
5840 Hyphens in particular break FK / index name derivation \
5841 downstream; use underscores instead (e.g. `{}`)",
5842 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
5843 ),
5844 ));
5845 }
5846 }
5847 Ok(())
5848}
5849
5850fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
5851 let attrs = parse_field_attrs(field)?;
5852 let ident = field
5853 .ident
5854 .as_ref()
5855 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
5856 let name = ident.to_string();
5857 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
5858 let primary_key = attrs.primary_key;
5859 let DetectedType {
5860 kind,
5861 nullable,
5862 auto: detected_auto,
5863 fk_inner,
5864 } = detect_type(&field.ty)?;
5865 check_bound_compatibility(field, &attrs, kind)?;
5866 let auto = detected_auto;
5867 if attrs.auto_uuid {
5873 if kind != DetectedKind::Uuid {
5874 return Err(syn::Error::new_spanned(
5875 field,
5876 "`#[rustango(auto_uuid)]` requires the field type to be \
5877 `Auto<uuid::Uuid>`",
5878 ));
5879 }
5880 if !detected_auto {
5881 return Err(syn::Error::new_spanned(
5882 field,
5883 "`#[rustango(auto_uuid)]` requires the field type to be \
5884 wrapped in `Auto<...>` so the macro skips the column on \
5885 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
5886 ));
5887 }
5888 }
5889 if attrs.auto_now_add || attrs.auto_now {
5890 if kind != DetectedKind::DateTime {
5891 return Err(syn::Error::new_spanned(
5892 field,
5893 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
5894 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
5895 ));
5896 }
5897 if !detected_auto {
5898 return Err(syn::Error::new_spanned(
5899 field,
5900 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
5901 the field type to be wrapped in `Auto<...>` so the macro skips \
5902 the column on INSERT and the DB DEFAULT (`now()`) fires",
5903 ));
5904 }
5905 }
5906 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
5907 return Err(syn::Error::new_spanned(
5908 field,
5909 "`#[rustango(soft_delete)]` requires the field type to be \
5910 `Option<chrono::DateTime<chrono::Utc>>`",
5911 ));
5912 }
5913 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
5914 if detected_auto && !primary_key && !is_mixin_auto {
5915 return Err(syn::Error::new_spanned(
5916 field,
5917 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
5918 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
5919 `auto_now`",
5920 ));
5921 }
5922 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
5923 return Err(syn::Error::new_spanned(
5924 field,
5925 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
5926 SERIAL / BIGSERIAL already supplies a default sequence.",
5927 ));
5928 }
5929 if fk_inner.is_some() && primary_key {
5930 return Err(syn::Error::new_spanned(
5931 field,
5932 "`ForeignKey<T>` is not allowed on a primary-key field — \
5933 a row's PK is its own identity, not a reference to a parent.",
5934 ));
5935 }
5936 if attrs.generated_as.is_some() {
5937 if primary_key {
5938 return Err(syn::Error::new_spanned(
5939 field,
5940 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
5941 primary-key field — a PK must be writable so the row \
5942 has an identity at INSERT time.",
5943 ));
5944 }
5945 if attrs.default.is_some() {
5946 return Err(syn::Error::new_spanned(
5947 field,
5948 "`#[rustango(generated_as = \"…\")]` cannot combine with \
5949 `default = \"…\"` — Postgres rejects DEFAULT on \
5950 generated columns. The expression IS the default.",
5951 ));
5952 }
5953 if detected_auto {
5954 return Err(syn::Error::new_spanned(
5955 field,
5956 "`#[rustango(generated_as = \"…\")]` is not allowed on \
5957 an `Auto<T>` field — generated columns are computed \
5958 by the DB, not server-assigned via a sequence. Use a \
5959 plain Rust type (e.g. `f64`).",
5960 ));
5961 }
5962 if fk_inner.is_some() {
5963 return Err(syn::Error::new_spanned(
5964 field,
5965 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
5966 ForeignKey field.",
5967 ));
5968 }
5969 }
5970 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
5971 let column_lit = column.as_str();
5972 let field_type_tokens = kind.variant_tokens();
5973 let max_length = optional_u32(attrs.max_length);
5974 let min = optional_i64(attrs.min);
5975 let max = optional_i64(attrs.max);
5976 let default = optional_str(attrs.default.as_deref());
5977
5978 let unique = attrs.unique;
5979 let generated_as = optional_str(attrs.generated_as.as_deref());
5980 let help_text = optional_str(attrs.help_text.as_deref());
5981 let choices = optional_choices(attrs.choices.as_deref());
5982 let db_comment = optional_str(attrs.db_comment.as_deref());
5983 let verbose_name = optional_str(attrs.verbose_name.as_deref());
5984 let editable = attrs.editable;
5985 let blank = attrs.blank;
5986 let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
5987 let schema = quote! {
5988 ::rustango::core::FieldSchema {
5989 name: #name,
5990 column: #column_lit,
5991 ty: #field_type_tokens,
5992 nullable: #nullable,
5993 primary_key: #primary_key,
5994 relation: #relation,
5995 max_length: #max_length,
5996 min: #min,
5997 max: #max,
5998 default: #default,
5999 auto: #auto,
6000 unique: #unique,
6001 generated_as: #generated_as,
6002 help_text: #help_text,
6003 choices: #choices,
6004 db_comment: #db_comment,
6005 verbose_name: #verbose_name,
6006 editable: #editable,
6007 blank: #blank,
6008 validators: &[ #(#validators_lits),* ],
6009 }
6010 };
6011
6012 let from_row_init = quote! {
6013 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
6014 };
6015 let from_aliased_row_init = quote! {
6016 #ident: ::rustango::sql::sqlx::Row::try_get(
6017 row,
6018 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
6019 )?
6020 };
6021
6022 Ok(FieldInfo {
6023 ident,
6024 column,
6025 primary_key,
6026 auto,
6027 value_ty: &field.ty,
6028 field_type_tokens,
6029 schema,
6030 from_row_init,
6031 from_aliased_row_init,
6032 fk_inner: fk_inner.cloned(),
6033 fk_pk_kind: kind,
6034 nullable,
6035 auto_now: attrs.auto_now,
6036 auto_now_add: attrs.auto_now_add,
6037 soft_delete: attrs.soft_delete,
6038 generated_as: attrs.generated_as.clone(),
6039 })
6040}
6041
6042fn check_bound_compatibility(
6043 field: &syn::Field,
6044 attrs: &FieldAttrs,
6045 kind: DetectedKind,
6046) -> syn::Result<()> {
6047 if attrs.max_length.is_some() && kind != DetectedKind::String {
6048 return Err(syn::Error::new_spanned(
6049 field,
6050 "`max_length` is only valid on `String` fields (or `Option<String>`)",
6051 ));
6052 }
6053 if attrs.choices.is_some() && kind != DetectedKind::String {
6054 return Err(syn::Error::new_spanned(
6055 field,
6056 "`choices` is only valid on `String` fields (or `Option<String>`) — \
6057 integer-valued enumerations should be modeled with a Rust enum and \
6058 custom (de)serializer for now",
6059 ));
6060 }
6061 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
6062 return Err(syn::Error::new_spanned(
6063 field,
6064 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
6065 ));
6066 }
6067 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
6068 if min > max {
6069 return Err(syn::Error::new_spanned(
6070 field,
6071 format!("`min` ({min}) is greater than `max` ({max})"),
6072 ));
6073 }
6074 }
6075 Ok(())
6076}
6077
6078fn optional_u32(value: Option<u32>) -> TokenStream2 {
6079 if let Some(v) = value {
6080 quote!(::core::option::Option::Some(#v))
6081 } else {
6082 quote!(::core::option::Option::None)
6083 }
6084}
6085
6086fn optional_i64(value: Option<i64>) -> TokenStream2 {
6087 if let Some(v) = value {
6088 quote!(::core::option::Option::Some(#v))
6089 } else {
6090 quote!(::core::option::Option::None)
6091 }
6092}
6093
6094fn optional_str(value: Option<&str>) -> TokenStream2 {
6095 if let Some(v) = value {
6096 quote!(::core::option::Option::Some(#v))
6097 } else {
6098 quote!(::core::option::Option::None)
6099 }
6100}
6101
6102fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
6103 let Some(pairs) = pairs else {
6104 return quote!(::core::option::Option::None);
6105 };
6106 let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
6107 quote!(::core::option::Option::Some(&[#(#entries),*]))
6108}
6109
6110fn relation_tokens(
6111 field: &syn::Field,
6112 attrs: &FieldAttrs,
6113 fk_inner: Option<&syn::Type>,
6114 table: &str,
6115) -> syn::Result<TokenStream2> {
6116 if let Some(inner) = fk_inner {
6117 if attrs.fk.is_some() || attrs.o2o.is_some() {
6118 return Err(syn::Error::new_spanned(
6119 field,
6120 "`ForeignKey<T>` already declares the FK target via the type parameter — \
6121 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
6122 ));
6123 }
6124 let on = attrs.on.as_deref().unwrap_or("id");
6125 return Ok(quote! {
6126 ::core::option::Option::Some(::rustango::core::Relation::Fk {
6127 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
6128 on: #on,
6129 })
6130 });
6131 }
6132 match (&attrs.fk, &attrs.o2o) {
6133 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
6134 field,
6135 "`fk` and `o2o` are mutually exclusive",
6136 )),
6137 (Some(to), None) => {
6138 let on = attrs.on.as_deref().unwrap_or("id");
6139 let resolved = if to == "self" { table } else { to };
6145 Ok(quote! {
6146 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
6147 })
6148 }
6149 (None, Some(to)) => {
6150 let on = attrs.on.as_deref().unwrap_or("id");
6151 let resolved = if to == "self" { table } else { to };
6152 Ok(quote! {
6153 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
6154 })
6155 }
6156 (None, None) => {
6157 if attrs.on.is_some() {
6158 return Err(syn::Error::new_spanned(
6159 field,
6160 "`on` requires `fk` or `o2o`",
6161 ));
6162 }
6163 Ok(quote!(::core::option::Option::None))
6164 }
6165 }
6166}
6167
6168#[derive(Clone, Copy, PartialEq, Eq)]
6172enum DetectedKind {
6173 I16,
6174 I32,
6175 I64,
6176 F32,
6177 F64,
6178 Bool,
6179 String,
6180 DateTime,
6181 Date,
6182 Time,
6183 Uuid,
6184 Json,
6185 Decimal,
6186 Binary,
6187}
6188
6189impl DetectedKind {
6190 fn variant_tokens(self) -> TokenStream2 {
6191 match self {
6192 Self::I16 => quote!(::rustango::core::FieldType::I16),
6193 Self::I32 => quote!(::rustango::core::FieldType::I32),
6194 Self::I64 => quote!(::rustango::core::FieldType::I64),
6195 Self::F32 => quote!(::rustango::core::FieldType::F32),
6196 Self::F64 => quote!(::rustango::core::FieldType::F64),
6197 Self::Bool => quote!(::rustango::core::FieldType::Bool),
6198 Self::String => quote!(::rustango::core::FieldType::String),
6199 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
6200 Self::Date => quote!(::rustango::core::FieldType::Date),
6201 Self::Time => quote!(::rustango::core::FieldType::Time),
6202 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
6203 Self::Json => quote!(::rustango::core::FieldType::Json),
6204 Self::Decimal => quote!(::rustango::core::FieldType::Decimal),
6205 Self::Binary => quote!(::rustango::core::FieldType::Binary),
6206 }
6207 }
6208
6209 fn is_integer(self) -> bool {
6210 matches!(self, Self::I16 | Self::I32 | Self::I64)
6211 }
6212
6213 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
6221 match self {
6222 Self::I16 => (quote!(I16), quote!(0i16)),
6223 Self::I32 => (quote!(I32), quote!(0i32)),
6224 Self::I64 => (quote!(I64), quote!(0i64)),
6225 Self::F32 => (quote!(F32), quote!(0f32)),
6226 Self::F64 => (quote!(F64), quote!(0f64)),
6227 Self::Bool => (quote!(Bool), quote!(false)),
6228 Self::String => (quote!(String), quote!(::std::string::String::new())),
6229 Self::DateTime => (
6230 quote!(DateTime),
6231 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
6232 ),
6233 Self::Date => (
6234 quote!(Date),
6235 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
6236 ),
6237 Self::Time => (
6238 quote!(Time),
6239 quote!(<::chrono::NaiveTime as ::std::default::Default>::default()),
6240 ),
6241 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
6242 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
6243 Self::Decimal => (
6244 quote!(Decimal),
6245 quote!(<::rust_decimal::Decimal as ::std::default::Default>::default()),
6246 ),
6247 Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
6248 }
6249 }
6250}
6251
6252#[derive(Clone, Copy)]
6258struct DetectedType<'a> {
6259 kind: DetectedKind,
6260 nullable: bool,
6261 auto: bool,
6262 fk_inner: Option<&'a syn::Type>,
6263}
6264
6265fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
6270 let Type::Path(TypePath { path, qself: None }) = ty else {
6271 return None;
6272 };
6273 let last = path.segments.last()?;
6274 if last.ident != "Auto" {
6275 return None;
6276 }
6277 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
6278 return None;
6279 };
6280 args.args.iter().find_map(|a| match a {
6281 syn::GenericArgument::Type(t) => Some(t),
6282 _ => None,
6283 })
6284}
6285
6286fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
6287 let Type::Path(TypePath { path, qself: None }) = ty else {
6288 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
6289 };
6290 let last = path
6291 .segments
6292 .last()
6293 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
6294
6295 if last.ident == "Option" {
6296 let inner = generic_inner(ty, &last.arguments, "Option")?;
6297 let inner_det = detect_type(inner)?;
6298 if inner_det.nullable {
6299 return Err(syn::Error::new_spanned(
6300 ty,
6301 "nested Option is not supported",
6302 ));
6303 }
6304 if inner_det.auto {
6305 return Err(syn::Error::new_spanned(
6306 ty,
6307 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
6308 ));
6309 }
6310 return Ok(DetectedType {
6311 nullable: true,
6312 ..inner_det
6313 });
6314 }
6315
6316 if last.ident == "Auto" {
6317 let inner = generic_inner(ty, &last.arguments, "Auto")?;
6318 let inner_det = detect_type(inner)?;
6319 if inner_det.auto {
6320 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
6321 }
6322 if inner_det.nullable {
6323 return Err(syn::Error::new_spanned(
6324 ty,
6325 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
6326 ));
6327 }
6328 if inner_det.fk_inner.is_some() {
6329 return Err(syn::Error::new_spanned(
6330 ty,
6331 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
6332 ));
6333 }
6334 if !matches!(
6335 inner_det.kind,
6336 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
6337 ) {
6338 return Err(syn::Error::new_spanned(
6339 ty,
6340 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
6341 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
6342 (DEFAULT now())",
6343 ));
6344 }
6345 return Ok(DetectedType {
6346 auto: true,
6347 ..inner_det
6348 });
6349 }
6350
6351 if last.ident == "ForeignKey" {
6352 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
6353 let kind = match key_ty {
6361 Some(k) => detect_type(k)?.kind,
6362 None => DetectedKind::I64,
6363 };
6364 return Ok(DetectedType {
6365 kind,
6366 nullable: false,
6367 auto: false,
6368 fk_inner: Some(inner),
6369 });
6370 }
6371
6372 let kind = match last.ident.to_string().as_str() {
6373 "i16" => DetectedKind::I16,
6374 "i32" => DetectedKind::I32,
6375 "i64" => DetectedKind::I64,
6376 "f32" => DetectedKind::F32,
6377 "f64" => DetectedKind::F64,
6378 "bool" => DetectedKind::Bool,
6379 "String" => DetectedKind::String,
6380 "DateTime" => DetectedKind::DateTime,
6381 "NaiveDate" => DetectedKind::Date,
6382 "NaiveTime" => DetectedKind::Time,
6383 "Uuid" => DetectedKind::Uuid,
6384 "Value" => DetectedKind::Json,
6385 "Decimal" => DetectedKind::Decimal,
6386 "Vec" => {
6390 let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
6391 if let Type::Path(TypePath { path, qself: None }) = inner {
6392 if let Some(seg) = path.segments.last() {
6393 if seg.ident == "u8" && seg.arguments.is_empty() {
6394 return Ok(DetectedType {
6395 kind: DetectedKind::Binary,
6396 nullable: false,
6397 auto: false,
6398 fk_inner: None,
6399 });
6400 }
6401 }
6402 }
6403 return Err(syn::Error::new_spanned(
6404 ty,
6405 "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported",
6406 ));
6407 }
6408 other => {
6409 return Err(syn::Error::new_spanned(
6410 ty,
6411 format!("unsupported field type `{other}`; supports i16/i32/i64/f32/f64/bool/String/DateTime/NaiveDate/NaiveTime/Uuid/serde_json::Value/Decimal/Vec<u8>, optionally wrapped in Option or Auto (Auto only on integers/Uuid/DateTime)"),
6412 ));
6413 }
6414 };
6415 Ok(DetectedType {
6416 kind,
6417 nullable: false,
6418 auto: false,
6419 fk_inner: None,
6420 })
6421}
6422
6423fn generic_inner<'a>(
6424 ty: &'a Type,
6425 arguments: &'a PathArguments,
6426 wrapper: &str,
6427) -> syn::Result<&'a Type> {
6428 let PathArguments::AngleBracketed(args) = arguments else {
6429 return Err(syn::Error::new_spanned(
6430 ty,
6431 format!("{wrapper} requires a generic argument"),
6432 ));
6433 };
6434 args.args
6435 .iter()
6436 .find_map(|a| match a {
6437 GenericArgument::Type(t) => Some(t),
6438 _ => None,
6439 })
6440 .ok_or_else(|| {
6441 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
6442 })
6443}
6444
6445fn generic_pair<'a>(
6449 ty: &'a Type,
6450 arguments: &'a PathArguments,
6451 wrapper: &str,
6452) -> syn::Result<(&'a Type, Option<&'a Type>)> {
6453 let PathArguments::AngleBracketed(args) = arguments else {
6454 return Err(syn::Error::new_spanned(
6455 ty,
6456 format!("{wrapper} requires a generic argument"),
6457 ));
6458 };
6459 let mut types = args.args.iter().filter_map(|a| match a {
6460 GenericArgument::Type(t) => Some(t),
6461 _ => None,
6462 });
6463 let first = types.next().ok_or_else(|| {
6464 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
6465 })?;
6466 let second = types.next();
6467 Ok((first, second))
6468}
6469
6470fn to_snake_case(s: &str) -> String {
6471 let mut out = String::with_capacity(s.len() + 4);
6472 for (i, ch) in s.chars().enumerate() {
6473 if ch.is_ascii_uppercase() {
6474 if i > 0 {
6475 out.push('_');
6476 }
6477 out.push(ch.to_ascii_lowercase());
6478 } else {
6479 out.push(ch);
6480 }
6481 }
6482 out
6483}
6484
6485#[derive(Default)]
6491struct FormFieldAttrs {
6492 min: Option<i64>,
6493 max: Option<i64>,
6494 min_length: Option<u32>,
6495 max_length: Option<u32>,
6496 clean: Option<syn::Ident>,
6503}
6504
6505#[derive(Default)]
6508struct FormContainerAttrs {
6509 validate: Option<syn::Ident>,
6515}
6516
6517#[derive(Clone, Copy)]
6519enum FormFieldKind {
6520 String,
6521 I16,
6522 I32,
6523 I64,
6524 F32,
6525 F64,
6526 Bool,
6527}
6528
6529impl FormFieldKind {
6530 fn parse_method(self) -> &'static str {
6531 match self {
6532 Self::I16 => "i16",
6533 Self::I32 => "i32",
6534 Self::I64 => "i64",
6535 Self::F32 => "f32",
6536 Self::F64 => "f64",
6537 Self::String | Self::Bool => "",
6540 }
6541 }
6542}
6543
6544fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
6545 let struct_name = &input.ident;
6546
6547 let Data::Struct(data) = &input.data else {
6548 return Err(syn::Error::new_spanned(
6549 struct_name,
6550 "Form can only be derived on structs",
6551 ));
6552 };
6553 let Fields::Named(named) = &data.fields else {
6554 return Err(syn::Error::new_spanned(
6555 struct_name,
6556 "Form requires a struct with named fields",
6557 ));
6558 };
6559
6560 let container = parse_form_container_attrs(input)?;
6562 let post_field_clean: Vec<TokenStream2> = Vec::new();
6563 let _ = post_field_clean;
6564
6565 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
6566 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
6567
6568 for field in &named.named {
6569 let ident = field
6570 .ident
6571 .as_ref()
6572 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
6573 let attrs = parse_form_field_attrs(field)?;
6574 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
6575
6576 let name_lit = ident.to_string();
6577 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
6578 let clean_block = if let Some(clean_fn) = &attrs.clean {
6584 quote! {
6585 if __errors.fields().get(#name_lit).is_none() {
6586 match Self::#clean_fn(&#ident) {
6587 ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
6588 ::core::result::Result::Err(__msg) => {
6589 __errors.add(#name_lit, __msg);
6590 }
6591 }
6592 }
6593 }
6594 } else {
6595 quote! {}
6596 };
6597 field_blocks.push(quote! {
6598 #parse_block
6599 #clean_block
6600 });
6601 field_idents.push(ident);
6602 }
6603
6604 let cross_field_call = if let Some(validate_fn) = &container.validate {
6609 quote! {
6610 if __errors.is_empty() {
6611 let __candidate = Self { #( #field_idents ),* };
6612 if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
6613 __errors.merge(__other);
6614 }
6615 if !__errors.is_empty() {
6616 return ::core::result::Result::Err(__errors);
6617 }
6618 return ::core::result::Result::Ok(__candidate);
6619 }
6620 }
6621 } else {
6622 quote! {}
6623 };
6624
6625 Ok(quote! {
6626 impl ::rustango::forms::Form for #struct_name {
6627 fn parse(
6628 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
6629 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
6630 let mut __errors = ::rustango::forms::FormErrors::default();
6631 #( #field_blocks )*
6632 #cross_field_call
6633 if !__errors.is_empty() {
6634 return ::core::result::Result::Err(__errors);
6635 }
6636 ::core::result::Result::Ok(Self {
6637 #( #field_idents ),*
6638 })
6639 }
6640 }
6641 })
6642}
6643
6644fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
6645 let mut out = FormContainerAttrs::default();
6646 for attr in &input.attrs {
6647 if !attr.path().is_ident("form") {
6648 continue;
6649 }
6650 attr.parse_nested_meta(|meta| {
6651 if meta.path.is_ident("validate") {
6652 let s: LitStr = meta.value()?.parse()?;
6653 out.validate = Some(syn::Ident::new(&s.value(), s.span()));
6654 return Ok(());
6655 }
6656 Err(meta.error("unknown form container attribute (supported: `validate`)"))
6657 })?;
6658 }
6659 Ok(out)
6660}
6661
6662fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
6663 let mut out = FormFieldAttrs::default();
6664 for attr in &field.attrs {
6665 if !attr.path().is_ident("form") {
6666 continue;
6667 }
6668 attr.parse_nested_meta(|meta| {
6669 if meta.path.is_ident("min") {
6670 let lit: syn::LitInt = meta.value()?.parse()?;
6671 out.min = Some(lit.base10_parse::<i64>()?);
6672 return Ok(());
6673 }
6674 if meta.path.is_ident("max") {
6675 let lit: syn::LitInt = meta.value()?.parse()?;
6676 out.max = Some(lit.base10_parse::<i64>()?);
6677 return Ok(());
6678 }
6679 if meta.path.is_ident("min_length") {
6680 let lit: syn::LitInt = meta.value()?.parse()?;
6681 out.min_length = Some(lit.base10_parse::<u32>()?);
6682 return Ok(());
6683 }
6684 if meta.path.is_ident("max_length") {
6685 let lit: syn::LitInt = meta.value()?.parse()?;
6686 out.max_length = Some(lit.base10_parse::<u32>()?);
6687 return Ok(());
6688 }
6689 if meta.path.is_ident("clean") {
6690 let s: LitStr = meta.value()?.parse()?;
6691 out.clean = Some(syn::Ident::new(&s.value(), s.span()));
6692 return Ok(());
6693 }
6694 Err(meta.error(
6695 "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
6696 ))
6697 })?;
6698 }
6699 Ok(out)
6700}
6701
6702fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
6703 let Type::Path(TypePath { path, qself: None }) = ty else {
6704 return Err(syn::Error::new(
6705 span,
6706 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
6707 ));
6708 };
6709 let last = path
6710 .segments
6711 .last()
6712 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
6713
6714 if last.ident == "Option" {
6715 let inner = generic_inner(ty, &last.arguments, "Option")?;
6716 let (kind, nested) = detect_form_field(inner, span)?;
6717 if nested {
6718 return Err(syn::Error::new(
6719 span,
6720 "nested Option in Form fields is not supported",
6721 ));
6722 }
6723 return Ok((kind, true));
6724 }
6725
6726 let kind = match last.ident.to_string().as_str() {
6727 "String" => FormFieldKind::String,
6728 "i16" => FormFieldKind::I16,
6729 "i32" => FormFieldKind::I32,
6730 "i64" => FormFieldKind::I64,
6731 "f32" => FormFieldKind::F32,
6732 "f64" => FormFieldKind::F64,
6733 "bool" => FormFieldKind::Bool,
6734 other => {
6735 return Err(syn::Error::new(
6736 span,
6737 format!(
6738 "Form field type `{other}` is not supported in v0.8 — use String / \
6739 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
6740 ),
6741 ));
6742 }
6743 };
6744 Ok((kind, false))
6745}
6746
6747#[allow(clippy::too_many_lines)]
6748fn render_form_field_parse(
6749 ident: &syn::Ident,
6750 name_lit: &str,
6751 kind: FormFieldKind,
6752 nullable: bool,
6753 attrs: &FormFieldAttrs,
6754) -> TokenStream2 {
6755 let lookup = quote! {
6758 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
6759 };
6760
6761 let parsed_value = match kind {
6762 FormFieldKind::Bool => quote! {
6763 let __v: bool = match __raw {
6764 ::core::option::Option::None => false,
6765 ::core::option::Option::Some(__s) => !matches!(
6766 __s.to_ascii_lowercase().as_str(),
6767 "" | "false" | "0" | "off" | "no"
6768 ),
6769 };
6770 },
6771 FormFieldKind::String => {
6772 if nullable {
6773 quote! {
6774 let __v: ::core::option::Option<::std::string::String> = match __raw {
6775 ::core::option::Option::None => ::core::option::Option::None,
6776 ::core::option::Option::Some(__s) if __s.is_empty() => {
6777 ::core::option::Option::None
6778 }
6779 ::core::option::Option::Some(__s) => {
6780 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
6781 }
6782 };
6783 }
6784 } else {
6785 quote! {
6786 let __v: ::std::string::String = match __raw {
6787 ::core::option::Option::Some(__s) if !__s.is_empty() => {
6788 ::core::clone::Clone::clone(__s)
6789 }
6790 _ => {
6791 __errors.add(#name_lit, "This field is required.");
6792 ::std::string::String::new()
6793 }
6794 };
6795 }
6796 }
6797 }
6798 FormFieldKind::I16
6799 | FormFieldKind::I32
6800 | FormFieldKind::I64
6801 | FormFieldKind::F32
6802 | FormFieldKind::F64 => {
6803 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
6804 let ty_lit = kind.parse_method();
6805 let default_val = match kind {
6806 FormFieldKind::I16 => quote! { 0i16 },
6807 FormFieldKind::I32 => quote! { 0i32 },
6808 FormFieldKind::I64 => quote! { 0i64 },
6809 FormFieldKind::F32 => quote! { 0f32 },
6810 FormFieldKind::F64 => quote! { 0f64 },
6811 _ => quote! { Default::default() },
6812 };
6813 if nullable {
6814 quote! {
6815 let __v: ::core::option::Option<#parse_ty> = match __raw {
6816 ::core::option::Option::None => ::core::option::Option::None,
6817 ::core::option::Option::Some(__s) if __s.is_empty() => {
6818 ::core::option::Option::None
6819 }
6820 ::core::option::Option::Some(__s) => {
6821 match __s.parse::<#parse_ty>() {
6822 ::core::result::Result::Ok(__n) => {
6823 ::core::option::Option::Some(__n)
6824 }
6825 ::core::result::Result::Err(__e) => {
6826 __errors.add(
6827 #name_lit,
6828 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
6829 );
6830 ::core::option::Option::None
6831 }
6832 }
6833 }
6834 };
6835 }
6836 } else {
6837 quote! {
6838 let __v: #parse_ty = match __raw {
6839 ::core::option::Option::Some(__s) if !__s.is_empty() => {
6840 match __s.parse::<#parse_ty>() {
6841 ::core::result::Result::Ok(__n) => __n,
6842 ::core::result::Result::Err(__e) => {
6843 __errors.add(
6844 #name_lit,
6845 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
6846 );
6847 #default_val
6848 }
6849 }
6850 }
6851 _ => {
6852 __errors.add(#name_lit, "This field is required.");
6853 #default_val
6854 }
6855 };
6856 }
6857 }
6858 }
6859 };
6860
6861 let validators = render_form_validators(name_lit, kind, nullable, attrs);
6862
6863 quote! {
6864 let mut #ident = {
6868 #lookup
6869 #parsed_value
6870 #validators
6871 __v
6872 };
6873 }
6874}
6875
6876fn render_form_validators(
6877 name_lit: &str,
6878 kind: FormFieldKind,
6879 nullable: bool,
6880 attrs: &FormFieldAttrs,
6881) -> TokenStream2 {
6882 let mut checks: Vec<TokenStream2> = Vec::new();
6883
6884 let val_ref = if nullable {
6885 quote! { __v.as_ref() }
6886 } else {
6887 quote! { ::core::option::Option::Some(&__v) }
6888 };
6889
6890 let is_string = matches!(kind, FormFieldKind::String);
6891 let is_numeric = matches!(
6892 kind,
6893 FormFieldKind::I16
6894 | FormFieldKind::I32
6895 | FormFieldKind::I64
6896 | FormFieldKind::F32
6897 | FormFieldKind::F64
6898 );
6899
6900 if is_string {
6901 if let Some(min_len) = attrs.min_length {
6902 let min_len_usize = min_len as usize;
6903 checks.push(quote! {
6904 if let ::core::option::Option::Some(__s) = #val_ref {
6905 if __s.len() < #min_len_usize {
6906 __errors.add(
6907 #name_lit,
6908 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
6909 );
6910 }
6911 }
6912 });
6913 }
6914 if let Some(max_len) = attrs.max_length {
6915 let max_len_usize = max_len as usize;
6916 checks.push(quote! {
6917 if let ::core::option::Option::Some(__s) = #val_ref {
6918 if __s.len() > #max_len_usize {
6919 __errors.add(
6920 #name_lit,
6921 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
6922 );
6923 }
6924 }
6925 });
6926 }
6927 }
6928
6929 if is_numeric {
6930 if let Some(min) = attrs.min {
6931 checks.push(quote! {
6932 if let ::core::option::Option::Some(__n) = #val_ref {
6933 if (*__n as f64) < (#min as f64) {
6934 __errors.add(
6935 #name_lit,
6936 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
6937 );
6938 }
6939 }
6940 });
6941 }
6942 if let Some(max) = attrs.max {
6943 checks.push(quote! {
6944 if let ::core::option::Option::Some(__n) = #val_ref {
6945 if (*__n as f64) > (#max as f64) {
6946 __errors.add(
6947 #name_lit,
6948 ::std::format!("Ensure this value is less than or equal to {}.", #max),
6949 );
6950 }
6951 }
6952 });
6953 }
6954 }
6955
6956 quote! { #( #checks )* }
6957}
6958
6959struct ViewSetAttrs {
6964 model: syn::Path,
6965 fields: Option<Vec<String>>,
6966 filter_fields: Vec<String>,
6967 search_fields: Vec<String>,
6968 ordering: Vec<(String, bool)>,
6970 page_size: Option<usize>,
6971 read_only: bool,
6972 perms: ViewSetPermsAttrs,
6973}
6974
6975#[derive(Default)]
6976struct ViewSetPermsAttrs {
6977 list: Vec<String>,
6978 retrieve: Vec<String>,
6979 create: Vec<String>,
6980 update: Vec<String>,
6981 destroy: Vec<String>,
6982}
6983
6984fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
6985 let struct_name = &input.ident;
6986
6987 match &input.data {
6989 Data::Struct(s) => match &s.fields {
6990 Fields::Unit | Fields::Named(_) => {}
6991 Fields::Unnamed(_) => {
6992 return Err(syn::Error::new_spanned(
6993 struct_name,
6994 "ViewSet can only be derived on a unit struct or an empty named struct",
6995 ));
6996 }
6997 },
6998 _ => {
6999 return Err(syn::Error::new_spanned(
7000 struct_name,
7001 "ViewSet can only be derived on a struct",
7002 ));
7003 }
7004 }
7005
7006 let attrs = parse_viewset_attrs(input)?;
7007 let model_path = &attrs.model;
7008
7009 let fields_call = if let Some(ref fields) = attrs.fields {
7011 let lits = fields.iter().map(|f| f.as_str());
7012 quote!(.fields(&[ #(#lits),* ]))
7013 } else {
7014 quote!()
7015 };
7016
7017 let filter_fields_call = if attrs.filter_fields.is_empty() {
7018 quote!()
7019 } else {
7020 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
7021 quote!(.filter_fields(&[ #(#lits),* ]))
7022 };
7023
7024 let search_fields_call = if attrs.search_fields.is_empty() {
7025 quote!()
7026 } else {
7027 let lits = attrs.search_fields.iter().map(|f| f.as_str());
7028 quote!(.search_fields(&[ #(#lits),* ]))
7029 };
7030
7031 let ordering_call = if attrs.ordering.is_empty() {
7032 quote!()
7033 } else {
7034 let pairs = attrs.ordering.iter().map(|(f, desc)| {
7035 let f = f.as_str();
7036 quote!((#f, #desc))
7037 });
7038 quote!(.ordering(&[ #(#pairs),* ]))
7039 };
7040
7041 let page_size_call = if let Some(n) = attrs.page_size {
7042 quote!(.page_size(#n))
7043 } else {
7044 quote!()
7045 };
7046
7047 let read_only_call = if attrs.read_only {
7048 quote!(.read_only())
7049 } else {
7050 quote!()
7051 };
7052
7053 let perms = &attrs.perms;
7054 let perms_call = if perms.list.is_empty()
7055 && perms.retrieve.is_empty()
7056 && perms.create.is_empty()
7057 && perms.update.is_empty()
7058 && perms.destroy.is_empty()
7059 {
7060 quote!()
7061 } else {
7062 let list_lits = perms.list.iter().map(|s| s.as_str());
7063 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
7064 let create_lits = perms.create.iter().map(|s| s.as_str());
7065 let update_lits = perms.update.iter().map(|s| s.as_str());
7066 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
7067 quote! {
7068 .permissions(::rustango::viewset::ViewSetPerms {
7069 list: ::std::vec![ #(#list_lits.to_owned()),* ],
7070 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
7071 create: ::std::vec![ #(#create_lits.to_owned()),* ],
7072 update: ::std::vec![ #(#update_lits.to_owned()),* ],
7073 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
7074 })
7075 }
7076 };
7077
7078 Ok(quote! {
7079 impl #struct_name {
7080 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
7083 ::rustango::viewset::ViewSet::for_model(
7084 <#model_path as ::rustango::core::Model>::SCHEMA
7085 )
7086 #fields_call
7087 #filter_fields_call
7088 #search_fields_call
7089 #ordering_call
7090 #page_size_call
7091 #perms_call
7092 #read_only_call
7093 .router(prefix, pool)
7094 }
7095 }
7096 })
7097}
7098
7099fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
7100 let mut model: Option<syn::Path> = None;
7101 let mut fields: Option<Vec<String>> = None;
7102 let mut filter_fields: Vec<String> = Vec::new();
7103 let mut search_fields: Vec<String> = Vec::new();
7104 let mut ordering: Vec<(String, bool)> = Vec::new();
7105 let mut page_size: Option<usize> = None;
7106 let mut read_only = false;
7107 let mut perms = ViewSetPermsAttrs::default();
7108
7109 for attr in &input.attrs {
7110 if !attr.path().is_ident("viewset") {
7111 continue;
7112 }
7113 attr.parse_nested_meta(|meta| {
7114 if meta.path.is_ident("model") {
7115 let path: syn::Path = meta.value()?.parse()?;
7116 model = Some(path);
7117 return Ok(());
7118 }
7119 if meta.path.is_ident("fields") {
7120 let s: LitStr = meta.value()?.parse()?;
7121 fields = Some(split_field_list(&s.value()));
7122 return Ok(());
7123 }
7124 if meta.path.is_ident("filter_fields") {
7125 let s: LitStr = meta.value()?.parse()?;
7126 filter_fields = split_field_list(&s.value());
7127 return Ok(());
7128 }
7129 if meta.path.is_ident("search_fields") {
7130 let s: LitStr = meta.value()?.parse()?;
7131 search_fields = split_field_list(&s.value());
7132 return Ok(());
7133 }
7134 if meta.path.is_ident("ordering") {
7135 let s: LitStr = meta.value()?.parse()?;
7136 ordering = parse_ordering_list(&s.value());
7137 return Ok(());
7138 }
7139 if meta.path.is_ident("page_size") {
7140 let lit: syn::LitInt = meta.value()?.parse()?;
7141 page_size = Some(lit.base10_parse::<usize>()?);
7142 return Ok(());
7143 }
7144 if meta.path.is_ident("read_only") {
7145 read_only = true;
7146 return Ok(());
7147 }
7148 if meta.path.is_ident("permissions") {
7149 meta.parse_nested_meta(|inner| {
7150 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
7151 let s: LitStr = inner.value()?.parse()?;
7152 Ok(split_field_list(&s.value()))
7153 };
7154 if inner.path.is_ident("list") {
7155 perms.list = parse_codenames(&inner)?;
7156 } else if inner.path.is_ident("retrieve") {
7157 perms.retrieve = parse_codenames(&inner)?;
7158 } else if inner.path.is_ident("create") {
7159 perms.create = parse_codenames(&inner)?;
7160 } else if inner.path.is_ident("update") {
7161 perms.update = parse_codenames(&inner)?;
7162 } else if inner.path.is_ident("destroy") {
7163 perms.destroy = parse_codenames(&inner)?;
7164 } else {
7165 return Err(inner.error(
7166 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
7167 ));
7168 }
7169 Ok(())
7170 })?;
7171 return Ok(());
7172 }
7173 Err(meta.error(
7174 "unknown viewset attribute (supported: model, fields, filter_fields, \
7175 search_fields, ordering, page_size, read_only, permissions(...))",
7176 ))
7177 })?;
7178 }
7179
7180 let model = model.ok_or_else(|| {
7181 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
7182 })?;
7183
7184 Ok(ViewSetAttrs {
7185 model,
7186 fields,
7187 filter_fields,
7188 search_fields,
7189 ordering,
7190 page_size,
7191 read_only,
7192 perms,
7193 })
7194}
7195
7196struct SerializerContainerAttrs {
7199 model: syn::Path,
7200 cross_validate: Option<syn::Ident>,
7208}
7209
7210#[derive(Default)]
7211struct SerializerFieldAttrs {
7212 read_only: bool,
7213 write_only: bool,
7214 source: Option<String>,
7215 skip: bool,
7216 method: Option<String>,
7220 validate: Option<String>,
7225 nested: bool,
7235 nested_strict: bool,
7240 many: Option<syn::Type>,
7249 slug: Option<String>,
7259}
7260
7261fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
7262 let mut model: Option<syn::Path> = None;
7263 let mut cross_validate: Option<syn::Ident> = None;
7264 for attr in &input.attrs {
7265 if !attr.path().is_ident("serializer") {
7266 continue;
7267 }
7268 attr.parse_nested_meta(|meta| {
7269 if meta.path.is_ident("model") {
7270 let _eq: syn::Token![=] = meta.input.parse()?;
7271 model = Some(meta.input.parse()?);
7272 return Ok(());
7273 }
7274 if meta.path.is_ident("validate") {
7275 let s: LitStr = meta.value()?.parse()?;
7280 cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
7281 return Ok(());
7282 }
7283 Err(meta.error(
7284 "unknown serializer container attribute \
7285 (supported: `model`, `validate`)",
7286 ))
7287 })?;
7288 }
7289 let model = model.ok_or_else(|| {
7290 syn::Error::new_spanned(
7291 &input.ident,
7292 "`#[serializer(model = SomeModel)]` is required",
7293 )
7294 })?;
7295 Ok(SerializerContainerAttrs {
7296 model,
7297 cross_validate,
7298 })
7299}
7300
7301fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
7302 let mut out = SerializerFieldAttrs::default();
7303 for attr in &field.attrs {
7304 if !attr.path().is_ident("serializer") {
7305 continue;
7306 }
7307 attr.parse_nested_meta(|meta| {
7308 if meta.path.is_ident("read_only") {
7309 out.read_only = true;
7310 return Ok(());
7311 }
7312 if meta.path.is_ident("write_only") {
7313 out.write_only = true;
7314 return Ok(());
7315 }
7316 if meta.path.is_ident("skip") {
7317 out.skip = true;
7318 return Ok(());
7319 }
7320 if meta.path.is_ident("source") {
7321 let s: LitStr = meta.value()?.parse()?;
7322 out.source = Some(s.value());
7323 return Ok(());
7324 }
7325 if meta.path.is_ident("method") {
7326 let s: LitStr = meta.value()?.parse()?;
7327 out.method = Some(s.value());
7328 return Ok(());
7329 }
7330 if meta.path.is_ident("validate") {
7331 let s: LitStr = meta.value()?.parse()?;
7332 out.validate = Some(s.value());
7333 return Ok(());
7334 }
7335 if meta.path.is_ident("many") {
7336 let _eq: syn::Token![=] = meta.input.parse()?;
7337 out.many = Some(meta.input.parse()?);
7338 return Ok(());
7339 }
7340 if meta.path.is_ident("nested") {
7341 out.nested = true;
7342 if meta.input.peek(syn::token::Paren) {
7345 meta.parse_nested_meta(|inner| {
7346 if inner.path.is_ident("strict") {
7347 out.nested_strict = true;
7348 return Ok(());
7349 }
7350 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
7351 })?;
7352 }
7353 return Ok(());
7354 }
7355 if meta.path.is_ident("slug") {
7356 let s: LitStr = meta.value()?.parse()?;
7357 out.slug = Some(s.value());
7358 return Ok(());
7359 }
7360 Err(meta.error(
7361 "unknown serializer field attribute (supported: \
7362 `read_only`, `write_only`, `source`, `skip`, `method`, \
7363 `validate`, `nested`, `many`, `slug`)",
7364 ))
7365 })?;
7366 }
7367 if out.read_only && out.write_only {
7369 return Err(syn::Error::new_spanned(
7370 field,
7371 "a field cannot be both `read_only` and `write_only`",
7372 ));
7373 }
7374 if out.method.is_some() && out.source.is_some() {
7375 return Err(syn::Error::new_spanned(
7376 field,
7377 "`method` and `source` are mutually exclusive — `method` computes \
7378 the value from a method, `source` reads it from a different model field",
7379 ));
7380 }
7381 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
7382 return Err(syn::Error::new_spanned(
7383 field,
7384 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
7385 — pick one strategy for populating the field",
7386 ));
7387 }
7388 Ok(out)
7389}
7390
7391fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
7392 let struct_name = &input.ident;
7393 let struct_name_lit = struct_name.to_string();
7394
7395 let Data::Struct(data) = &input.data else {
7396 return Err(syn::Error::new_spanned(
7397 struct_name,
7398 "Serializer can only be derived on structs",
7399 ));
7400 };
7401 let Fields::Named(named) = &data.fields else {
7402 return Err(syn::Error::new_spanned(
7403 struct_name,
7404 "Serializer requires a struct with named fields",
7405 ));
7406 };
7407
7408 let container = parse_serializer_container_attrs(input)?;
7409 let model_path = &container.model;
7410
7411 #[allow(dead_code)]
7415 struct FieldInfo {
7416 ident: syn::Ident,
7417 ty: syn::Type,
7418 attrs: SerializerFieldAttrs,
7419 }
7420 let mut fields_info: Vec<FieldInfo> = Vec::new();
7421 for field in &named.named {
7422 let ident = field.ident.clone().expect("named field has ident");
7423 let attrs = parse_serializer_field_attrs(field)?;
7424 fields_info.push(FieldInfo {
7425 ident,
7426 ty: field.ty.clone(),
7427 attrs,
7428 });
7429 }
7430
7431 let from_model_fields = fields_info.iter().map(|fi| {
7433 let ident = &fi.ident;
7434 let ty = &fi.ty;
7435 if let Some(_inner) = &fi.attrs.many {
7436 quote! { #ident: ::std::vec::Vec::new() }
7440 } else if let Some(method) = &fi.attrs.method {
7441 let method_ident = syn::Ident::new(method, ident.span());
7445 quote! { #ident: Self::#method_ident(model) }
7446 } else if let Some(slug_field) = &fi.attrs.slug {
7447 let src_name = fi
7455 .attrs
7456 .source
7457 .as_deref()
7458 .unwrap_or(&fi.ident.to_string())
7459 .to_owned();
7460 let src_ident = syn::Ident::new(&src_name, ident.span());
7461 let slug_ident = syn::Ident::new(slug_field, ident.span());
7462 quote! {
7463 #ident: match model.#src_ident.value() {
7464 ::core::option::Option::Some(__loaded) =>
7465 ::core::clone::Clone::clone(&__loaded.#slug_ident),
7466 ::core::option::Option::None =>
7467 ::core::default::Default::default(),
7468 }
7469 }
7470 } else if fi.attrs.nested {
7471 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
7487 let src_ident = syn::Ident::new(&src_name, ident.span());
7488 if fi.attrs.nested_strict {
7489 let panic_msg = format!(
7490 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
7491 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
7492 );
7493 quote! {
7494 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
7495 model.#src_ident.value().expect(#panic_msg),
7496 )
7497 }
7498 } else {
7499 quote! {
7500 #ident: match model.#src_ident.value() {
7501 ::core::option::Option::Some(__loaded) =>
7502 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
7503 ::core::option::Option::None =>
7504 ::core::default::Default::default(),
7505 }
7506 }
7507 }
7508 } else if fi.attrs.write_only || fi.attrs.skip {
7509 quote! { #ident: ::core::default::Default::default() }
7511 } else if let Some(src) = &fi.attrs.source {
7512 let src_ident = syn::Ident::new(src, ident.span());
7513 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
7514 } else {
7515 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
7516 }
7517 });
7518
7519 let validator_calls: Vec<_> = fields_info
7523 .iter()
7524 .filter_map(|fi| {
7525 let ident = &fi.ident;
7526 let name_lit = ident.to_string();
7527 let method = fi.attrs.validate.as_ref()?;
7528 let method_ident = syn::Ident::new(method, ident.span());
7529 Some(quote! {
7530 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
7531 __errors.add(#name_lit.to_owned(), __e);
7532 }
7533 })
7534 })
7535 .collect();
7536 let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
7543 quote! {
7544 if let ::core::result::Result::Err(__cross) = self.#method_ident() {
7547 __errors.merge(__cross);
7548 }
7549 }
7550 });
7551 let validate_method = if validator_calls.is_empty() && container.cross_validate.is_none() {
7552 quote! {}
7553 } else {
7554 quote! {
7555 impl #struct_name {
7556 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
7563 let mut __errors = ::rustango::forms::FormErrors::default();
7564 #( #validator_calls )*
7565 #cross_validate_call
7566 if __errors.is_empty() {
7567 ::core::result::Result::Ok(())
7568 } else {
7569 ::core::result::Result::Err(__errors)
7570 }
7571 }
7572 }
7573 }
7574 };
7575
7576 let many_setters: Vec<_> = fields_info
7580 .iter()
7581 .filter_map(|fi| {
7582 let many_ty = fi.attrs.many.as_ref()?;
7583 let ident = &fi.ident;
7584 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
7585 Some(quote! {
7586 pub fn #setter(
7591 &mut self,
7592 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
7593 ) -> &mut Self {
7594 self.#ident = models.iter()
7595 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
7596 .collect();
7597 self
7598 }
7599 })
7600 })
7601 .collect();
7602 let many_setters_impl = if many_setters.is_empty() {
7603 quote! {}
7604 } else {
7605 quote! {
7606 impl #struct_name {
7607 #( #many_setters )*
7608 }
7609 }
7610 };
7611
7612 let output_fields: Vec<_> = fields_info
7614 .iter()
7615 .filter(|fi| !fi.attrs.write_only)
7616 .collect();
7617 let output_field_count = output_fields.len();
7618 let serialize_fields = output_fields.iter().map(|fi| {
7619 let ident = &fi.ident;
7620 let name_lit = ident.to_string();
7621 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
7622 });
7623
7624 let writable_lits: Vec<_> = fields_info
7638 .iter()
7639 .filter(|fi| {
7640 !fi.attrs.read_only
7641 && !fi.attrs.skip
7642 && fi.attrs.method.is_none()
7643 && !fi.attrs.nested
7644 && fi.attrs.many.is_none()
7645 && fi.attrs.slug.is_none()
7646 })
7647 .map(|fi| fi.ident.to_string())
7648 .collect();
7649
7650 let openapi_impl = {
7654 #[cfg(feature = "openapi")]
7655 {
7656 let property_calls = output_fields.iter().map(|fi| {
7657 let ident = &fi.ident;
7658 let name_lit = ident.to_string();
7659 let ty = &fi.ty;
7660 let nullable_call = if is_option(ty) {
7661 quote! { .nullable() }
7662 } else {
7663 quote! {}
7664 };
7665 quote! {
7666 .property(
7667 #name_lit,
7668 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
7669 #nullable_call,
7670 )
7671 }
7672 });
7673 let required_lits: Vec<_> = output_fields
7674 .iter()
7675 .filter(|fi| !is_option(&fi.ty))
7676 .map(|fi| fi.ident.to_string())
7677 .collect();
7678 quote! {
7679 impl ::rustango::openapi::OpenApiSchema for #struct_name {
7680 fn openapi_schema() -> ::rustango::openapi::Schema {
7681 ::rustango::openapi::Schema::object()
7682 #( #property_calls )*
7683 .required([ #( #required_lits ),* ])
7684 }
7685 }
7686 }
7687 }
7688 #[cfg(not(feature = "openapi"))]
7689 {
7690 quote! {}
7691 }
7692 };
7693
7694 Ok(quote! {
7695 impl ::rustango::serializer::ModelSerializer for #struct_name {
7696 type Model = #model_path;
7697
7698 fn from_model(model: &Self::Model) -> Self {
7699 Self {
7700 #( #from_model_fields ),*
7701 }
7702 }
7703
7704 fn writable_fields() -> &'static [&'static str] {
7705 &[ #( #writable_lits ),* ]
7706 }
7707 }
7708
7709 impl ::serde::Serialize for #struct_name {
7710 fn serialize<S>(&self, serializer: S)
7711 -> ::core::result::Result<S::Ok, S::Error>
7712 where
7713 S: ::serde::Serializer,
7714 {
7715 use ::serde::ser::SerializeStruct;
7716 let mut __state = serializer.serialize_struct(
7717 #struct_name_lit,
7718 #output_field_count,
7719 )?;
7720 #( #serialize_fields )*
7721 __state.end()
7722 }
7723 }
7724
7725 #openapi_impl
7726
7727 #validate_method
7728
7729 #many_setters_impl
7730 })
7731}
7732
7733#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
7737fn is_option(ty: &syn::Type) -> bool {
7738 if let syn::Type::Path(p) = ty {
7739 if let Some(last) = p.path.segments.last() {
7740 return last.ident == "Option";
7741 }
7742 }
7743 false
7744}