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))]
116pub fn derive_serializer(input: TokenStream) -> TokenStream {
117 let input = parse_macro_input!(input as DeriveInput);
118 expand_serializer(&input)
119 .unwrap_or_else(syn::Error::into_compile_error)
120 .into()
121}
122
123#[proc_macro]
158pub fn embed_migrations(input: TokenStream) -> TokenStream {
159 expand_embed_migrations(input.into())
160 .unwrap_or_else(syn::Error::into_compile_error)
161 .into()
162}
163
164#[proc_macro_attribute]
187pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
188 expand_main(args.into(), item.into())
189 .unwrap_or_else(syn::Error::into_compile_error)
190 .into()
191}
192
193fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
194 let mut input: syn::ItemFn = syn::parse2(item)?;
195 if input.sig.asyncness.is_none() {
196 return Err(syn::Error::new(
197 input.sig.ident.span(),
198 "`#[rustango::main]` must wrap an `async fn`",
199 ));
200 }
201
202 let flavor = parse_flavor(&args);
214 let builder_call = match flavor {
215 Flavor::CurrentThread => quote! {
216 ::rustango::__private_runtime::tokio::runtime::Builder::new_current_thread()
217 },
218 Flavor::MultiThread => quote! {
219 ::rustango::__private_runtime::tokio::runtime::Builder::new_multi_thread()
220 },
221 };
222
223 let user_body = input.block.clone();
226 input.sig.asyncness = None;
227 input.block = syn::parse2(quote! {{
228 {
229 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
230 let _ = tracing_subscriber::fmt()
233 .with_env_filter(
234 EnvFilter::try_from_default_env()
235 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
236 )
237 .try_init();
238 }
239 let __rt = #builder_call
240 .enable_all()
241 .build()
242 .expect("failed to build tokio runtime");
243 __rt.block_on(async move #user_body)
244 }})?;
245
246 Ok(quote! {
247 #input
248 })
249}
250
251enum Flavor {
252 MultiThread,
253 CurrentThread,
254}
255
256fn parse_flavor(args: &TokenStream2) -> Flavor {
257 let s = args.to_string();
261 if s.contains("current_thread") {
262 Flavor::CurrentThread
263 } else {
264 Flavor::MultiThread
265 }
266}
267
268fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
269 let path_str = if input.is_empty() {
271 "./migrations".to_string()
272 } else {
273 let lit: LitStr = syn::parse2(input)?;
274 lit.value()
275 };
276
277 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
278 syn::Error::new(
279 proc_macro2::Span::call_site(),
280 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
281 )
282 })?;
283 let abs = std::path::Path::new(&manifest).join(&path_str);
284
285 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
286 if abs.is_dir() {
287 let read = std::fs::read_dir(&abs).map_err(|e| {
288 syn::Error::new(
289 proc_macro2::Span::call_site(),
290 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
291 )
292 })?;
293 for entry in read.flatten() {
294 let path = entry.path();
295 if !path.is_file() {
296 continue;
297 }
298 if path.extension().and_then(|s| s.to_str()) != Some("json") {
299 continue;
300 }
301 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
302 continue;
303 };
304 entries.push((stem.to_owned(), path));
305 }
306 }
307 entries.sort_by(|a, b| a.0.cmp(&b.0));
308
309 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
322 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
323 for (stem, path) in &entries {
324 let raw = std::fs::read_to_string(path).map_err(|e| {
325 syn::Error::new(
326 proc_macro2::Span::call_site(),
327 format!(
328 "embed_migrations!: cannot read {} for chain validation: {e}",
329 path.display()
330 ),
331 )
332 })?;
333 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
334 syn::Error::new(
335 proc_macro2::Span::call_site(),
336 format!(
337 "embed_migrations!: {} is not valid JSON: {e}",
338 path.display()
339 ),
340 )
341 })?;
342 let name = json
343 .get("name")
344 .and_then(|v| v.as_str())
345 .ok_or_else(|| {
346 syn::Error::new(
347 proc_macro2::Span::call_site(),
348 format!(
349 "embed_migrations!: {} is missing the `name` field",
350 path.display()
351 ),
352 )
353 })?
354 .to_owned();
355 if name != *stem {
356 return Err(syn::Error::new(
357 proc_macro2::Span::call_site(),
358 format!(
359 "embed_migrations!: file stem `{stem}` does not match the migration's \
360 `name` field `{name}` — rename the file or fix the JSON",
361 ),
362 ));
363 }
364 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
365 chain_names.push(name.clone());
366 prev_refs.push((name, prev));
367 }
368
369 let name_set: std::collections::HashSet<&str> =
370 chain_names.iter().map(String::as_str).collect();
371 for (name, prev) in &prev_refs {
372 if let Some(p) = prev {
373 if !name_set.contains(p.as_str()) {
374 return Err(syn::Error::new(
375 proc_macro2::Span::call_site(),
376 format!(
377 "embed_migrations!: broken migration chain — `{name}` declares \
378 prev=`{p}` but no migration with that name exists in {}",
379 abs.display()
380 ),
381 ));
382 }
383 }
384 }
385
386 let pairs: Vec<TokenStream2> = entries
387 .iter()
388 .map(|(name, path)| {
389 let path_lit = path.display().to_string();
390 quote! { (#name, ::core::include_str!(#path_lit)) }
391 })
392 .collect();
393
394 Ok(quote! {
395 {
396 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
397 __RUSTANGO_EMBEDDED
398 }
399 })
400}
401
402fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
403 let struct_name = &input.ident;
404
405 let Data::Struct(data) = &input.data else {
406 return Err(syn::Error::new_spanned(
407 struct_name,
408 "Model can only be derived on structs",
409 ));
410 };
411 let Fields::Named(named) = &data.fields else {
412 return Err(syn::Error::new_spanned(
413 struct_name,
414 "Model requires a struct with named fields",
415 ));
416 };
417
418 let container = parse_container_attrs(input)?;
419 let table = container
420 .table
421 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
422 let model_name = struct_name.to_string();
423
424 let collected = collect_fields(named, &table)?;
425
426 if let Some((ref display, span)) = container.display {
428 if !collected.field_names.iter().any(|n| n == display) {
429 return Err(syn::Error::new(
430 span,
431 format!("`display = \"{display}\"` does not match any field on this struct"),
432 ));
433 }
434 }
435 let display = container.display.map(|(name, _)| name);
436 let app_label = container.app.clone();
437
438 if let Some(admin) = &container.admin {
448 for (label, list) in [
449 ("search_fields", &admin.search_fields),
450 ("readonly_fields", &admin.readonly_fields),
451 ("list_filter", &admin.list_filter),
452 ] {
453 if let Some((names, span)) = list {
454 for name in names {
455 if !collected.field_names.iter().any(|n| n == name) {
456 return Err(syn::Error::new(
457 *span,
458 format!(
459 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
460 ),
461 ));
462 }
463 }
464 }
465 }
466 if let Some((pairs, span)) = &admin.ordering {
467 for (name, _) in pairs {
468 if !collected.field_names.iter().any(|n| n == name) {
469 return Err(syn::Error::new(
470 *span,
471 format!(
472 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
473 ),
474 ));
475 }
476 }
477 }
478 if let Some((groups, span)) = &admin.fieldsets {
479 for (_, fields) in groups {
480 for name in fields {
481 if !collected.field_names.iter().any(|n| n == name) {
482 return Err(syn::Error::new(
483 *span,
484 format!(
485 "`fieldsets`: \"{name}\" is not a declared field on this struct"
486 ),
487 ));
488 }
489 }
490 }
491 }
492 }
493 if let Some(audit) = &container.audit {
494 if let Some((names, span)) = &audit.track {
495 for name in names {
496 if !collected.field_names.iter().any(|n| n == name) {
497 return Err(syn::Error::new(
498 *span,
499 format!(
500 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
501 ),
502 ));
503 }
504 }
505 }
506 }
507
508 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
511 audit
512 .track
513 .as_ref()
514 .map(|(names, _)| names.clone())
515 .unwrap_or_default()
516 });
517
518 let mut all_indexes: Vec<IndexAttr> = container.indexes;
520 for field in &named.named {
521 let ident = field.ident.as_ref().expect("named");
522 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
525 if fa.index {
526 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
527 let auto_name = if fa.index_unique {
528 format!("{table}_{col_name}_uq_idx")
529 } else {
530 format!("{table}_{col_name}_idx")
531 };
532 all_indexes.push(IndexAttr {
533 name: fa.index_name.or(Some(auto_name)),
534 columns: vec![col_name],
535 unique: fa.index_unique,
536 });
537 }
538 }
539 }
540
541 let model_impl = model_impl_tokens(
542 struct_name,
543 &model_name,
544 &table,
545 display.as_deref(),
546 app_label.as_deref(),
547 container.admin.as_ref(),
548 &collected.field_schemas,
549 collected.soft_delete_column.as_deref(),
550 container.permissions,
551 audit_track_names.as_deref(),
552 &container.m2m,
553 &all_indexes,
554 &container.checks,
555 &container.composite_fks,
556 &container.generic_fks,
557 container.scope.as_deref(),
558 );
559 let module_ident = column_module_ident(struct_name);
560 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
561 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
562 let track_set: Option<std::collections::HashSet<&str>> = audit
563 .track
564 .as_ref()
565 .map(|(names, _)| names.iter().map(String::as_str).collect());
566 collected
567 .column_entries
568 .iter()
569 .filter(|c| {
570 track_set
571 .as_ref()
572 .map_or(true, |s| s.contains(c.name.as_str()))
573 })
574 .collect()
575 });
576 let inherent_impl = inherent_impl_tokens(
577 struct_name,
578 &collected,
579 collected.primary_key.as_ref(),
580 &column_consts,
581 audited_fields.as_deref(),
582 &all_indexes,
583 );
584 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
585 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
586 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
587 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
588
589 Ok(quote! {
590 #model_impl
591 #inherent_impl
592 #from_row_impl
593 #column_module
594 #reverse_helpers
595 #m2m_accessors
596
597 ::rustango::core::inventory::submit! {
598 ::rustango::core::ModelEntry {
599 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
600 module_path: ::core::module_path!(),
605 }
606 }
607 })
608}
609
610fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
621 let arms = fk_relations.iter().map(|rel| {
622 let parent_ty = &rel.parent_type;
623 let fk_col = rel.fk_column.as_str();
624 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
627 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
628 let assign = if rel.nullable {
629 quote! {
630 self.#field_ident = ::core::option::Option::Some(
631 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
632 );
633 }
634 } else {
635 quote! {
636 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
637 }
638 };
639 quote! {
640 #fk_col => {
641 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
642 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
649 ::rustango::core::SqlValue::#variant_ident(v) => v,
650 _other => {
651 ::core::debug_assert!(
652 false,
653 "rustango macro bug: load_related on FK `{}` expected \
654 SqlValue::{} from parent's __rustango_pk_value but got \
655 {:?} — file a bug at https://github.com/ujeenet/rustango",
656 #fk_col,
657 ::core::stringify!(#variant_ident),
658 _other,
659 );
660 #default_expr
661 }
662 };
663 #assign
664 ::core::result::Result::Ok(true)
665 }
666 }
667 });
668 quote! {
669 #[cfg(feature = "postgres")]
670 impl ::rustango::sql::LoadRelated for #struct_name {
671 #[allow(unused_variables)]
672 fn __rustango_load_related(
673 &mut self,
674 row: &::rustango::sql::sqlx::postgres::PgRow,
675 field_name: &str,
676 alias: &str,
677 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
678 match field_name {
679 #( #arms )*
680 _ => ::core::result::Result::Ok(false),
681 }
682 }
683 }
684 }
685}
686
687fn load_related_impl_my_tokens(
695 struct_name: &syn::Ident,
696 fk_relations: &[FkRelation],
697) -> TokenStream2 {
698 let arms = fk_relations.iter().map(|rel| {
699 let parent_ty = &rel.parent_type;
700 let fk_col = rel.fk_column.as_str();
701 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
702 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
703 let assign = if rel.nullable {
704 quote! {
705 __self.#field_ident = ::core::option::Option::Some(
706 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
707 );
708 }
709 } else {
710 quote! {
711 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
712 }
713 };
714 quote! {
719 #fk_col => {
720 let _parent: #parent_ty =
721 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
722 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
725 ::rustango::core::SqlValue::#variant_ident(v) => v,
726 _other => {
727 ::core::debug_assert!(
728 false,
729 "rustango macro bug: load_related on FK `{}` expected \
730 SqlValue::{} from parent's __rustango_pk_value but got \
731 {:?} — file a bug at https://github.com/ujeenet/rustango",
732 #fk_col,
733 ::core::stringify!(#variant_ident),
734 _other,
735 );
736 #default_expr
737 }
738 };
739 #assign
740 ::core::result::Result::Ok(true)
741 }
742 }
743 });
744 quote! {
745 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
746 #( #arms )*
747 });
748 }
749}
750
751fn load_related_impl_sqlite_tokens(
755 struct_name: &syn::Ident,
756 fk_relations: &[FkRelation],
757) -> TokenStream2 {
758 let arms = fk_relations.iter().map(|rel| {
759 let parent_ty = &rel.parent_type;
760 let fk_col = rel.fk_column.as_str();
761 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
762 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
763 let assign = if rel.nullable {
764 quote! {
765 __self.#field_ident = ::core::option::Option::Some(
766 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
767 );
768 }
769 } else {
770 quote! {
771 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
772 }
773 };
774 quote! {
775 #fk_col => {
776 let _parent: #parent_ty =
777 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
778 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
779 ::rustango::core::SqlValue::#variant_ident(v) => v,
780 _other => {
781 ::core::debug_assert!(
782 false,
783 "rustango macro bug: load_related on FK `{}` expected \
784 SqlValue::{} from parent's __rustango_pk_value but got \
785 {:?} — file a bug at https://github.com/ujeenet/rustango",
786 #fk_col,
787 ::core::stringify!(#variant_ident),
788 _other,
789 );
790 #default_expr
791 }
792 };
793 #assign
794 ::core::result::Result::Ok(true)
795 }
796 }
797 });
798 quote! {
799 ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
800 #( #arms )*
801 });
802 }
803}
804
805fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
813 let arms = fk_relations.iter().map(|rel| {
814 let fk_col = rel.fk_column.as_str();
815 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
816 if rel.pk_kind == DetectedKind::I64 {
817 if rel.nullable {
823 quote! {
824 #fk_col => self.#field_ident
825 .as_ref()
826 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
827 }
828 } else {
829 quote! {
830 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
831 }
832 }
833 } else {
834 quote! {
842 #fk_col => ::core::option::Option::None,
843 }
844 }
845 });
846 let value_arms = fk_relations.iter().map(|rel| {
852 let fk_col = rel.fk_column.as_str();
853 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
854 if rel.nullable {
855 quote! {
856 #fk_col => self.#field_ident
857 .as_ref()
858 .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
859 ::rustango::sql::ForeignKey::pk(fk)
860 )),
861 }
862 } else {
863 quote! {
864 #fk_col => ::core::option::Option::Some(
865 ::core::convert::Into::<::rustango::core::SqlValue>::into(
866 self.#field_ident.pk()
867 )
868 ),
869 }
870 }
871 });
872 quote! {
873 impl ::rustango::sql::FkPkAccess for #struct_name {
874 #[allow(unused_variables)]
875 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
876 match field_name {
877 #( #arms )*
878 _ => ::core::option::Option::None,
879 }
880 }
881 #[allow(unused_variables)]
882 fn __rustango_fk_pk_value(
883 &self,
884 field_name: &str,
885 ) -> ::core::option::Option<::rustango::core::SqlValue> {
886 match field_name {
887 #( #value_arms )*
888 _ => ::core::option::Option::None,
889 }
890 }
891 }
892 }
893}
894
895fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
901 if fk_relations.is_empty() {
902 return TokenStream2::new();
903 }
904 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
908 let method_ident = syn::Ident::new(&suffix, child_ident.span());
909 let impls = fk_relations.iter().map(|rel| {
910 let parent_ty = &rel.parent_type;
911 let fk_col = rel.fk_column.as_str();
912 let doc = format!(
913 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
914 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
915 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
916 further `{child_ident}::objects()` filters via direct queryset use."
917 );
918 quote! {
919 impl #parent_ty {
920 #[doc = #doc]
921 pub async fn #method_ident<'_c, _E>(
926 &self,
927 _executor: _E,
928 ) -> ::core::result::Result<
929 ::std::vec::Vec<#child_ident>,
930 ::rustango::sql::ExecError,
931 >
932 where
933 _E: ::rustango::sql::sqlx::Executor<
934 '_c,
935 Database = ::rustango::sql::sqlx::Postgres,
936 >,
937 {
938 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
939 ::rustango::query::QuerySet::<#child_ident>::new()
940 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
941 .fetch_on(_executor)
942 .await
943 }
944 }
945 }
946 });
947 quote! { #( #impls )* }
948}
949
950fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
953 if m2m_relations.is_empty() {
954 return TokenStream2::new();
955 }
956 let methods = m2m_relations.iter().map(|rel| {
957 let method_name = format!("{}_m2m", rel.name);
958 let method_ident = syn::Ident::new(&method_name, struct_name.span());
959 let through = rel.through.as_str();
960 let src_col = rel.src.as_str();
961 let dst_col = rel.dst.as_str();
962 quote! {
963 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
964 ::rustango::sql::M2MManager {
965 src_pk: self.__rustango_pk_value(),
966 through: #through,
967 src_col: #src_col,
968 dst_col: #dst_col,
969 }
970 }
971 }
972 });
973 quote! {
974 impl #struct_name {
975 #( #methods )*
976 }
977 }
978}
979
980struct ColumnEntry {
981 ident: syn::Ident,
984 value_ty: Type,
986 name: String,
988 column: String,
990 field_type_tokens: TokenStream2,
992}
993
994struct CollectedFields {
995 field_schemas: Vec<TokenStream2>,
996 from_row_inits: Vec<TokenStream2>,
997 from_aliased_row_inits: Vec<TokenStream2>,
1001 insert_columns: Vec<TokenStream2>,
1004 insert_values: Vec<TokenStream2>,
1007 insert_pushes: Vec<TokenStream2>,
1012 returning_cols: Vec<TokenStream2>,
1015 auto_assigns: Vec<TokenStream2>,
1018 auto_field_idents: Vec<(syn::Ident, String)>,
1022 first_auto_value_ty: Option<Type>,
1025 bulk_pushes_no_auto: Vec<TokenStream2>,
1029 bulk_pushes_all: Vec<TokenStream2>,
1033 bulk_columns_no_auto: Vec<TokenStream2>,
1036 bulk_columns_all: Vec<TokenStream2>,
1039 bulk_auto_uniformity: Vec<TokenStream2>,
1043 first_auto_ident: Option<syn::Ident>,
1046 has_auto: bool,
1048 pk_is_auto: bool,
1052 update_assignments: Vec<TokenStream2>,
1055 upsert_update_columns: Vec<TokenStream2>,
1058 primary_key: Option<(syn::Ident, String)>,
1059 column_entries: Vec<ColumnEntry>,
1060 field_names: Vec<String>,
1063 fk_relations: Vec<FkRelation>,
1068 soft_delete_column: Option<String>,
1073}
1074
1075#[derive(Clone)]
1076struct FkRelation {
1077 parent_type: Type,
1080 fk_column: String,
1083 pk_kind: DetectedKind,
1088 nullable: bool,
1093}
1094
1095fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1096 let cap = named.named.len();
1097 let mut out = CollectedFields {
1098 field_schemas: Vec::with_capacity(cap),
1099 from_row_inits: Vec::with_capacity(cap),
1100 from_aliased_row_inits: Vec::with_capacity(cap),
1101 insert_columns: Vec::with_capacity(cap),
1102 insert_values: Vec::with_capacity(cap),
1103 insert_pushes: Vec::with_capacity(cap),
1104 returning_cols: Vec::new(),
1105 auto_assigns: Vec::new(),
1106 auto_field_idents: Vec::new(),
1107 first_auto_value_ty: None,
1108 bulk_pushes_no_auto: Vec::with_capacity(cap),
1109 bulk_pushes_all: Vec::with_capacity(cap),
1110 bulk_columns_no_auto: Vec::with_capacity(cap),
1111 bulk_columns_all: Vec::with_capacity(cap),
1112 bulk_auto_uniformity: Vec::new(),
1113 first_auto_ident: None,
1114 has_auto: false,
1115 pk_is_auto: false,
1116 update_assignments: Vec::with_capacity(cap),
1117 upsert_update_columns: Vec::with_capacity(cap),
1118 primary_key: None,
1119 column_entries: Vec::with_capacity(cap),
1120 field_names: Vec::with_capacity(cap),
1121 fk_relations: Vec::new(),
1122 soft_delete_column: None,
1123 };
1124
1125 for field in &named.named {
1126 let info = process_field(field, table)?;
1127 out.field_names.push(info.ident.to_string());
1128 out.field_schemas.push(info.schema);
1129 out.from_row_inits.push(info.from_row_init);
1130 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1131 if let Some(parent_ty) = info.fk_inner.clone() {
1132 out.fk_relations.push(FkRelation {
1133 parent_type: parent_ty,
1134 fk_column: info.column.clone(),
1135 pk_kind: info.fk_pk_kind,
1136 nullable: info.nullable,
1137 });
1138 }
1139 if info.soft_delete {
1140 if out.soft_delete_column.is_some() {
1141 return Err(syn::Error::new_spanned(
1142 field,
1143 "only one field may be marked `#[rustango(soft_delete)]`",
1144 ));
1145 }
1146 out.soft_delete_column = Some(info.column.clone());
1147 }
1148 let column = info.column.as_str();
1149 let ident = info.ident;
1150 if info.generated_as.is_some() {
1159 out.column_entries.push(ColumnEntry {
1160 ident: ident.clone(),
1161 value_ty: info.value_ty.clone(),
1162 name: ident.to_string(),
1163 column: info.column.clone(),
1164 field_type_tokens: info.field_type_tokens,
1165 });
1166 continue;
1167 }
1168 out.insert_columns.push(quote!(#column));
1169 out.insert_values.push(quote! {
1170 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1171 ::core::clone::Clone::clone(&self.#ident)
1172 )
1173 });
1174 if info.auto {
1175 out.has_auto = true;
1176 if out.first_auto_ident.is_none() {
1177 out.first_auto_ident = Some(ident.clone());
1178 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1179 }
1180 out.returning_cols.push(quote!(#column));
1181 out.auto_field_idents
1182 .push((ident.clone(), info.column.clone()));
1183 out.auto_assigns.push(quote! {
1184 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1185 });
1186 out.insert_pushes.push(quote! {
1187 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1188 _columns.push(#column);
1189 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1190 ::core::clone::Clone::clone(_v)
1191 ));
1192 }
1193 });
1194 out.bulk_columns_all.push(quote!(#column));
1197 out.bulk_pushes_all.push(quote! {
1198 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1199 ::core::clone::Clone::clone(&_row.#ident)
1200 ));
1201 });
1202 let ident_clone = ident.clone();
1206 out.bulk_auto_uniformity.push(quote! {
1207 for _r in rows.iter().skip(1) {
1208 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1209 return ::core::result::Result::Err(
1210 ::rustango::sql::ExecError::Sql(
1211 ::rustango::sql::SqlError::BulkAutoMixed
1212 )
1213 );
1214 }
1215 }
1216 });
1217 } else {
1218 out.insert_pushes.push(quote! {
1219 _columns.push(#column);
1220 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1221 ::core::clone::Clone::clone(&self.#ident)
1222 ));
1223 });
1224 out.bulk_columns_no_auto.push(quote!(#column));
1226 out.bulk_columns_all.push(quote!(#column));
1227 let push_expr = quote! {
1228 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1229 ::core::clone::Clone::clone(&_row.#ident)
1230 ));
1231 };
1232 out.bulk_pushes_no_auto.push(push_expr.clone());
1233 out.bulk_pushes_all.push(push_expr);
1234 }
1235 if info.primary_key {
1236 if out.primary_key.is_some() {
1237 return Err(syn::Error::new_spanned(
1238 field,
1239 "only one field may be marked `#[rustango(primary_key)]`",
1240 ));
1241 }
1242 out.primary_key = Some((ident.clone(), info.column.clone()));
1243 if info.auto {
1244 out.pk_is_auto = true;
1245 }
1246 } else if info.auto_now_add {
1247 } else if info.auto_now {
1249 out.update_assignments.push(quote! {
1254 ::rustango::core::Assignment {
1255 column: #column,
1256 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1257 ::chrono::Utc::now()
1258 ),
1259 }
1260 });
1261 out.upsert_update_columns.push(quote!(#column));
1262 } else {
1263 out.update_assignments.push(quote! {
1264 ::rustango::core::Assignment {
1265 column: #column,
1266 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1267 ::core::clone::Clone::clone(&self.#ident)
1268 ),
1269 }
1270 });
1271 out.upsert_update_columns.push(quote!(#column));
1272 }
1273 out.column_entries.push(ColumnEntry {
1274 ident: ident.clone(),
1275 value_ty: info.value_ty.clone(),
1276 name: ident.to_string(),
1277 column: info.column.clone(),
1278 field_type_tokens: info.field_type_tokens,
1279 });
1280 }
1281 Ok(out)
1282}
1283
1284fn model_impl_tokens(
1285 struct_name: &syn::Ident,
1286 model_name: &str,
1287 table: &str,
1288 display: Option<&str>,
1289 app_label: Option<&str>,
1290 admin: Option<&AdminAttrs>,
1291 field_schemas: &[TokenStream2],
1292 soft_delete_column: Option<&str>,
1293 permissions: bool,
1294 audit_track: Option<&[String]>,
1295 m2m_relations: &[M2MAttr],
1296 indexes: &[IndexAttr],
1297 checks: &[CheckAttr],
1298 composite_fks: &[CompositeFkAttr],
1299 generic_fks: &[GenericFkAttr],
1300 scope: Option<&str>,
1301) -> TokenStream2 {
1302 let display_tokens = if let Some(name) = display {
1303 quote!(::core::option::Option::Some(#name))
1304 } else {
1305 quote!(::core::option::Option::None)
1306 };
1307 let app_label_tokens = if let Some(name) = app_label {
1308 quote!(::core::option::Option::Some(#name))
1309 } else {
1310 quote!(::core::option::Option::None)
1311 };
1312 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1313 quote!(::core::option::Option::Some(#col))
1314 } else {
1315 quote!(::core::option::Option::None)
1316 };
1317 let audit_track_tokens = match audit_track {
1318 None => quote!(::core::option::Option::None),
1319 Some(names) => {
1320 let lits = names.iter().map(|n| n.as_str());
1321 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1322 }
1323 };
1324 let admin_tokens = admin_config_tokens(admin);
1325 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1329 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1330 _ => quote!(::rustango::core::ModelScope::Tenant),
1331 };
1332 let indexes_tokens = indexes.iter().map(|idx| {
1333 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1334 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1335 let unique = idx.unique;
1336 quote! {
1337 ::rustango::core::IndexSchema {
1338 name: #name,
1339 columns: &[ #(#cols),* ],
1340 unique: #unique,
1341 }
1342 }
1343 });
1344 let checks_tokens = checks.iter().map(|c| {
1345 let name = c.name.as_str();
1346 let expr = c.expr.as_str();
1347 quote! {
1348 ::rustango::core::CheckConstraint {
1349 name: #name,
1350 expr: #expr,
1351 }
1352 }
1353 });
1354 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1355 let name = rel.name.as_str();
1356 let to = rel.to.as_str();
1357 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1358 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1359 quote! {
1360 ::rustango::core::CompositeFkRelation {
1361 name: #name,
1362 to: #to,
1363 from: &[ #(#from_cols),* ],
1364 on: &[ #(#on_cols),* ],
1365 }
1366 }
1367 });
1368 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1369 let name = rel.name.as_str();
1370 let ct_col = rel.ct_column.as_str();
1371 let pk_col = rel.pk_column.as_str();
1372 quote! {
1373 ::rustango::core::GenericRelation {
1374 name: #name,
1375 ct_column: #ct_col,
1376 pk_column: #pk_col,
1377 }
1378 }
1379 });
1380 let m2m_tokens = m2m_relations.iter().map(|rel| {
1381 let name = rel.name.as_str();
1382 let to = rel.to.as_str();
1383 let through = rel.through.as_str();
1384 let src = rel.src.as_str();
1385 let dst = rel.dst.as_str();
1386 quote! {
1387 ::rustango::core::M2MRelation {
1388 name: #name,
1389 to: #to,
1390 through: #through,
1391 src_col: #src,
1392 dst_col: #dst,
1393 }
1394 }
1395 });
1396 quote! {
1397 impl ::rustango::core::Model for #struct_name {
1398 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1399 name: #model_name,
1400 table: #table,
1401 fields: &[ #(#field_schemas),* ],
1402 display: #display_tokens,
1403 app_label: #app_label_tokens,
1404 admin: #admin_tokens,
1405 soft_delete_column: #soft_delete_tokens,
1406 permissions: #permissions,
1407 audit_track: #audit_track_tokens,
1408 m2m: &[ #(#m2m_tokens),* ],
1409 indexes: &[ #(#indexes_tokens),* ],
1410 check_constraints: &[ #(#checks_tokens),* ],
1411 composite_relations: &[ #(#composite_fk_tokens),* ],
1412 generic_relations: &[ #(#generic_fk_tokens),* ],
1413 scope: #scope_tokens,
1414 };
1415 }
1416 }
1417}
1418
1419fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1423 let Some(admin) = admin else {
1424 return quote!(::core::option::Option::None);
1425 };
1426
1427 let list_display = admin
1428 .list_display
1429 .as_ref()
1430 .map(|(v, _)| v.as_slice())
1431 .unwrap_or(&[]);
1432 let list_display_lits = list_display.iter().map(|s| s.as_str());
1433
1434 let search_fields = admin
1435 .search_fields
1436 .as_ref()
1437 .map(|(v, _)| v.as_slice())
1438 .unwrap_or(&[]);
1439 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1440
1441 let readonly_fields = admin
1442 .readonly_fields
1443 .as_ref()
1444 .map(|(v, _)| v.as_slice())
1445 .unwrap_or(&[]);
1446 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1447
1448 let list_filter = admin
1449 .list_filter
1450 .as_ref()
1451 .map(|(v, _)| v.as_slice())
1452 .unwrap_or(&[]);
1453 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1454
1455 let actions = admin
1456 .actions
1457 .as_ref()
1458 .map(|(v, _)| v.as_slice())
1459 .unwrap_or(&[]);
1460 let actions_lits = actions.iter().map(|s| s.as_str());
1461
1462 let fieldsets = admin
1463 .fieldsets
1464 .as_ref()
1465 .map(|(v, _)| v.as_slice())
1466 .unwrap_or(&[]);
1467 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1468 let title = title.as_str();
1469 let field_lits = fields.iter().map(|s| s.as_str());
1470 quote!(::rustango::core::Fieldset {
1471 title: #title,
1472 fields: &[ #( #field_lits ),* ],
1473 })
1474 });
1475
1476 let list_per_page = admin.list_per_page.unwrap_or(0);
1477
1478 let ordering_pairs = admin
1479 .ordering
1480 .as_ref()
1481 .map(|(v, _)| v.as_slice())
1482 .unwrap_or(&[]);
1483 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1484 let name = name.as_str();
1485 let desc = *desc;
1486 quote!((#name, #desc))
1487 });
1488
1489 quote! {
1490 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1491 list_display: &[ #( #list_display_lits ),* ],
1492 search_fields: &[ #( #search_fields_lits ),* ],
1493 list_per_page: #list_per_page,
1494 ordering: &[ #( #ordering_tokens ),* ],
1495 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1496 list_filter: &[ #( #list_filter_lits ),* ],
1497 actions: &[ #( #actions_lits ),* ],
1498 fieldsets: &[ #( #fieldset_tokens ),* ],
1499 })
1500 }
1501}
1502
1503fn inherent_impl_tokens(
1504 struct_name: &syn::Ident,
1505 fields: &CollectedFields,
1506 primary_key: Option<&(syn::Ident, String)>,
1507 column_consts: &TokenStream2,
1508 audited_fields: Option<&[&ColumnEntry]>,
1509 indexes: &[IndexAttr],
1510) -> TokenStream2 {
1511 let executor_passes_to_data_write = if audited_fields.is_some() {
1517 quote!(&mut *_executor)
1518 } else {
1519 quote!(_executor)
1520 };
1521 let executor_param = if audited_fields.is_some() {
1522 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1523 } else {
1524 quote!(_executor: _E)
1525 };
1526 let executor_generics = if audited_fields.is_some() {
1527 quote!()
1528 } else {
1529 quote!(<'_c, _E>)
1530 };
1531 let executor_where = if audited_fields.is_some() {
1532 quote!()
1533 } else {
1534 quote! {
1535 where
1536 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1537 }
1538 };
1539 let pool_to_save_on = if audited_fields.is_some() {
1544 quote! {
1545 let mut _conn = pool.acquire().await?;
1546 self.save_on(&mut *_conn).await
1547 }
1548 } else {
1549 quote!(self.save_on(pool).await)
1550 };
1551 let pool_to_insert_on = if audited_fields.is_some() {
1552 quote! {
1553 let mut _conn = pool.acquire().await?;
1554 self.insert_on(&mut *_conn).await
1555 }
1556 } else {
1557 quote!(self.insert_on(pool).await)
1558 };
1559 let pool_to_delete_on = if audited_fields.is_some() {
1560 quote! {
1561 let mut _conn = pool.acquire().await?;
1562 self.delete_on(&mut *_conn).await
1563 }
1564 } else {
1565 quote!(self.delete_on(pool).await)
1566 };
1567 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1568 quote! {
1569 let mut _conn = pool.acquire().await?;
1570 Self::bulk_insert_on(rows, &mut *_conn).await
1571 }
1572 } else {
1573 quote!(Self::bulk_insert_on(rows, pool).await)
1574 };
1575 let pool_to_upsert_on = if audited_fields.is_some() {
1582 quote! {
1583 let mut _conn = pool.acquire().await?;
1584 self.upsert_on(&mut *_conn).await
1585 }
1586 } else {
1587 quote!(self.upsert_on(pool).await)
1588 };
1589
1590 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1608 quote!()
1617 } else if audited_fields.is_some() && fields.has_auto {
1618 quote!()
1621 } else if fields.has_auto {
1622 let pushes = &fields.insert_pushes;
1623 let returning_cols = &fields.returning_cols;
1624 quote! {
1625 pub async fn insert_pool(
1631 &mut self,
1632 pool: &::rustango::sql::Pool,
1633 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1634 let mut _columns: ::std::vec::Vec<&'static str> =
1635 ::std::vec::Vec::new();
1636 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1637 ::std::vec::Vec::new();
1638 #( #pushes )*
1639 let _query = ::rustango::core::InsertQuery {
1640 model: <Self as ::rustango::core::Model>::SCHEMA,
1641 columns: _columns,
1642 values: _values,
1643 returning: ::std::vec![ #( #returning_cols ),* ],
1644 on_conflict: ::core::option::Option::None,
1645 };
1646 let _result = ::rustango::sql::insert_returning_pool(
1647 pool, &_query,
1648 ).await?;
1649 ::rustango::sql::apply_auto_pk_pool(_result, self)
1650 }
1651 }
1652 } else {
1653 let insert_columns = &fields.insert_columns;
1654 let insert_values = &fields.insert_values;
1655 quote! {
1656 pub async fn insert_pool(
1663 &self,
1664 pool: &::rustango::sql::Pool,
1665 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1666 let _query = ::rustango::core::InsertQuery {
1667 model: <Self as ::rustango::core::Model>::SCHEMA,
1668 columns: ::std::vec![ #( #insert_columns ),* ],
1669 values: ::std::vec![ #( #insert_values ),* ],
1670 returning: ::std::vec::Vec::new(),
1671 on_conflict: ::core::option::Option::None,
1672 };
1673 ::rustango::sql::insert_pool(pool, &_query).await
1674 }
1675 }
1676 };
1677
1678 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1691 .map(|tracked| {
1692 tracked
1693 .iter()
1694 .map(|c| {
1695 let column_lit = c.column.as_str();
1696 let ident = &c.ident;
1697 quote! {
1698 (
1699 #column_lit,
1700 ::serde_json::to_value(&self.#ident)
1701 .unwrap_or(::serde_json::Value::Null),
1702 )
1703 }
1704 })
1705 .collect()
1706 })
1707 .unwrap_or_default();
1708 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1709 if fields.pk_is_auto {
1710 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1711 } else {
1712 quote!(::std::format!("{}", &self.#pk_ident))
1713 }
1714 } else {
1715 quote!(::std::string::String::new())
1716 };
1717 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1718 if audited_fields.is_some() {
1719 let pairs = audit_pair_tokens.iter();
1720 let pk_str = audit_pk_to_string.clone();
1721 quote! {
1722 let _audit_entry = ::rustango::audit::PendingEntry {
1723 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1724 entity_pk: #pk_str,
1725 operation: #op_path,
1726 source: ::rustango::audit::current_source(),
1727 changes: ::rustango::audit::snapshot_changes(&[
1728 #( #pairs ),*
1729 ]),
1730 };
1731 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1732 }
1733 } else {
1734 quote!()
1735 }
1736 };
1737 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1738 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1739 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1740 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1741
1742 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1758 let pk_column_lit = pk_col.as_str();
1759 let assignments = &fields.update_assignments;
1760 if audited_fields.is_some() {
1761 if fields.pk_is_auto {
1762 quote!()
1766 } else {
1767 let pairs = audit_pair_tokens.iter();
1768 let pk_str = audit_pk_to_string.clone();
1769 quote! {
1770 pub async fn save_pool(
1784 &mut self,
1785 pool: &::rustango::sql::Pool,
1786 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1787 let _query = ::rustango::core::UpdateQuery {
1788 model: <Self as ::rustango::core::Model>::SCHEMA,
1789 set: ::std::vec![ #( #assignments ),* ],
1790 where_clause: ::rustango::core::WhereExpr::Predicate(
1791 ::rustango::core::Filter {
1792 column: #pk_column_lit,
1793 op: ::rustango::core::Op::Eq,
1794 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1795 ::core::clone::Clone::clone(&self.#pk_ident)
1796 ),
1797 }
1798 ),
1799 };
1800 let _audit_entry = ::rustango::audit::PendingEntry {
1801 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1802 entity_pk: #pk_str,
1803 operation: ::rustango::audit::AuditOp::Update,
1804 source: ::rustango::audit::current_source(),
1805 changes: ::rustango::audit::snapshot_changes(&[
1806 #( #pairs ),*
1807 ]),
1808 };
1809 let _ = ::rustango::audit::save_one_with_audit_pool(
1810 pool, &_query, &_audit_entry,
1811 ).await?;
1812 ::core::result::Result::Ok(())
1813 }
1814 }
1815 }
1816 } else {
1817 let dispatch_unset = if fields.pk_is_auto {
1818 quote! {
1819 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1820 return self.insert_pool(pool).await;
1821 }
1822 }
1823 } else {
1824 quote!()
1825 };
1826 quote! {
1827 pub async fn save_pool(
1834 &mut self,
1835 pool: &::rustango::sql::Pool,
1836 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1837 #dispatch_unset
1838 let _query = ::rustango::core::UpdateQuery {
1839 model: <Self as ::rustango::core::Model>::SCHEMA,
1840 set: ::std::vec![ #( #assignments ),* ],
1841 where_clause: ::rustango::core::WhereExpr::Predicate(
1842 ::rustango::core::Filter {
1843 column: #pk_column_lit,
1844 op: ::rustango::core::Op::Eq,
1845 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1846 ::core::clone::Clone::clone(&self.#pk_ident)
1847 ),
1848 }
1849 ),
1850 };
1851 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1852 ::core::result::Result::Ok(())
1853 }
1854 }
1855 }
1856 } else {
1857 quote!()
1858 };
1859
1860 let pool_insert_method = if audited_fields.is_some() {
1867 if let Some(_) = primary_key {
1868 let pushes = if fields.has_auto {
1869 fields.insert_pushes.clone()
1870 } else {
1871 fields
1876 .insert_columns
1877 .iter()
1878 .zip(&fields.insert_values)
1879 .map(|(col, val)| {
1880 quote! {
1881 _columns.push(#col);
1882 _values.push(#val);
1883 }
1884 })
1885 .collect()
1886 };
1887 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1888 fields.returning_cols.clone()
1889 } else {
1890 primary_key
1897 .map(|(_, col)| {
1898 let lit = col.as_str();
1899 vec![quote!(#lit)]
1900 })
1901 .unwrap_or_default()
1902 };
1903 let pairs = audit_pair_tokens.iter();
1904 let pk_str = audit_pk_to_string.clone();
1905 quote! {
1906 pub async fn insert_pool(
1915 &mut self,
1916 pool: &::rustango::sql::Pool,
1917 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1918 let mut _columns: ::std::vec::Vec<&'static str> =
1919 ::std::vec::Vec::new();
1920 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1921 ::std::vec::Vec::new();
1922 #( #pushes )*
1923 let _query = ::rustango::core::InsertQuery {
1924 model: <Self as ::rustango::core::Model>::SCHEMA,
1925 columns: _columns,
1926 values: _values,
1927 returning: ::std::vec![ #( #returning_cols ),* ],
1928 on_conflict: ::core::option::Option::None,
1929 };
1930 let _audit_entry = ::rustango::audit::PendingEntry {
1931 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1932 entity_pk: #pk_str,
1933 operation: ::rustango::audit::AuditOp::Create,
1934 source: ::rustango::audit::current_source(),
1935 changes: ::rustango::audit::snapshot_changes(&[
1936 #( #pairs ),*
1937 ]),
1938 };
1939 let _result = ::rustango::audit::insert_one_with_audit_pool(
1940 pool, &_query, &_audit_entry,
1941 ).await?;
1942 ::rustango::sql::apply_auto_pk_pool(_result, self)
1943 }
1944 }
1945 } else {
1946 quote!()
1947 }
1948 } else {
1949 pool_insert_method
1951 };
1952
1953 let pool_save_method = if let Some(tracked) = audited_fields {
1974 if let Some((pk_ident, pk_col)) = primary_key {
1975 let pk_column_lit = pk_col.as_str();
1976 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1980 let pk_str = audit_pk_to_string.clone();
1981 let mk_before_pairs =
1986 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1987 tracked
1988 .iter()
1989 .map(|c| {
1990 let column_lit = c.column.as_str();
1991 let value_ty = &c.value_ty;
1992 quote! {
1993 (
1994 #column_lit,
1995 match #getter::<#value_ty>(
1996 _audit_before_row, #column_lit,
1997 ) {
1998 ::core::result::Result::Ok(v) => {
1999 ::serde_json::to_value(&v)
2000 .unwrap_or(::serde_json::Value::Null)
2001 }
2002 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2003 },
2004 )
2005 }
2006 })
2007 .collect()
2008 };
2009 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
2010 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
2011 let before_pairs_my: Vec<proc_macro2::TokenStream> =
2012 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
2013 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
2014 mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
2015 let pg_select_cols: String = tracked
2016 .iter()
2017 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2018 .collect::<Vec<_>>()
2019 .join(", ");
2020 let my_select_cols: String = tracked
2021 .iter()
2022 .map(|c| format!("`{}`", c.column.replace('`', "``")))
2023 .collect::<Vec<_>>()
2024 .join(", ");
2025 let sqlite_select_cols: String = pg_select_cols.clone();
2029 let pk_value_for_bind = if fields.pk_is_auto {
2030 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2031 } else {
2032 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2033 };
2034 let assignments = &fields.update_assignments;
2035 let unset_dispatch = if fields.has_auto {
2036 quote! {
2037 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2038 return self.insert_pool(pool).await;
2039 }
2040 }
2041 } else {
2042 quote!()
2043 };
2044 quote! {
2045 pub async fn save_pool(
2059 &mut self,
2060 pool: &::rustango::sql::Pool,
2061 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2062 #unset_dispatch
2063 let _query = ::rustango::core::UpdateQuery {
2064 model: <Self as ::rustango::core::Model>::SCHEMA,
2065 set: ::std::vec![ #( #assignments ),* ],
2066 where_clause: ::rustango::core::WhereExpr::Predicate(
2067 ::rustango::core::Filter {
2068 column: #pk_column_lit,
2069 op: ::rustango::core::Op::Eq,
2070 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2071 ::core::clone::Clone::clone(&self.#pk_ident)
2072 ),
2073 }
2074 ),
2075 };
2076 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2077 ::std::vec![ #( #after_pairs_pg ),* ];
2078 ::rustango::audit::save_one_with_diff_pool(
2079 pool,
2080 &_query,
2081 #pk_column_lit,
2082 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2083 #pk_value_for_bind,
2084 ),
2085 <Self as ::rustango::core::Model>::SCHEMA.table,
2086 #pk_str,
2087 _after_pairs,
2088 #pg_select_cols,
2089 #my_select_cols,
2090 #sqlite_select_cols,
2091 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2092 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2093 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2094 ).await
2095 }
2096 }
2097 } else {
2098 quote!()
2099 }
2100 } else {
2101 pool_save_method
2102 };
2103
2104 let pool_delete_method = {
2111 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2112 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2113 if let Some(pk_ident) = pk_ident_for_pool {
2114 if audited_fields.is_some() {
2115 let pairs = audit_pair_tokens.iter();
2116 let pk_str = audit_pk_to_string.clone();
2117 quote! {
2118 pub async fn delete_pool(
2125 &self,
2126 pool: &::rustango::sql::Pool,
2127 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2128 let _query = ::rustango::core::DeleteQuery {
2129 model: <Self as ::rustango::core::Model>::SCHEMA,
2130 where_clause: ::rustango::core::WhereExpr::Predicate(
2131 ::rustango::core::Filter {
2132 column: #pk_column_lit,
2133 op: ::rustango::core::Op::Eq,
2134 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2135 ::core::clone::Clone::clone(&self.#pk_ident)
2136 ),
2137 }
2138 ),
2139 };
2140 let _audit_entry = ::rustango::audit::PendingEntry {
2141 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2142 entity_pk: #pk_str,
2143 operation: ::rustango::audit::AuditOp::Delete,
2144 source: ::rustango::audit::current_source(),
2145 changes: ::rustango::audit::snapshot_changes(&[
2146 #( #pairs ),*
2147 ]),
2148 };
2149 ::rustango::audit::delete_one_with_audit_pool(
2150 pool, &_query, &_audit_entry,
2151 ).await
2152 }
2153 }
2154 } else {
2155 quote! {
2156 pub async fn delete_pool(
2163 &self,
2164 pool: &::rustango::sql::Pool,
2165 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2166 let _query = ::rustango::core::DeleteQuery {
2167 model: <Self as ::rustango::core::Model>::SCHEMA,
2168 where_clause: ::rustango::core::WhereExpr::Predicate(
2169 ::rustango::core::Filter {
2170 column: #pk_column_lit,
2171 op: ::rustango::core::Op::Eq,
2172 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2173 ::core::clone::Clone::clone(&self.#pk_ident)
2174 ),
2175 }
2176 ),
2177 };
2178 ::rustango::sql::delete_pool(pool, &_query).await
2179 }
2180 }
2181 }
2182 } else {
2183 quote!()
2184 }
2185 };
2186
2187 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
2197 audited_fields
2198 {
2199 if tracked.is_empty() {
2200 (quote!(), quote!())
2201 } else {
2202 let select_cols: String = tracked
2203 .iter()
2204 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2205 .collect::<Vec<_>>()
2206 .join(", ");
2207 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
2208 let select_cols_lit = select_cols;
2209 let pk_column_lit_for_select = pk_column_for_select;
2210 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2211 if fields.pk_is_auto {
2212 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2213 } else {
2214 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2215 }
2216 } else {
2217 quote!(0_i64)
2218 };
2219 let before_pairs = tracked.iter().map(|c| {
2220 let column_lit = c.column.as_str();
2221 let value_ty = &c.value_ty;
2222 quote! {
2223 (
2224 #column_lit,
2225 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2226 &_audit_before_row, #column_lit,
2227 ) {
2228 ::core::result::Result::Ok(v) => {
2229 ::serde_json::to_value(&v)
2230 .unwrap_or(::serde_json::Value::Null)
2231 }
2232 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2233 },
2234 )
2235 }
2236 });
2237 let after_pairs = tracked.iter().map(|c| {
2238 let column_lit = c.column.as_str();
2239 let ident = &c.ident;
2240 quote! {
2241 (
2242 #column_lit,
2243 ::serde_json::to_value(&self.#ident)
2244 .unwrap_or(::serde_json::Value::Null),
2245 )
2246 }
2247 });
2248 let pk_str = audit_pk_to_string.clone();
2249 let pre = quote! {
2250 let _audit_select_sql = ::std::format!(
2251 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2252 #select_cols_lit,
2253 <Self as ::rustango::core::Model>::SCHEMA.table,
2254 #pk_column_lit_for_select,
2255 );
2256 let _audit_before_pairs:
2257 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2258 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2259 .bind(#pk_value_for_bind)
2260 .fetch_optional(&mut *_executor)
2261 .await
2262 {
2263 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2264 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2265 }
2266 _ => ::core::option::Option::None,
2267 };
2268 };
2269 let post = quote! {
2270 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2271 let _audit_after:
2272 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2273 ::std::vec![ #( #after_pairs ),* ];
2274 let _audit_entry = ::rustango::audit::PendingEntry {
2275 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2276 entity_pk: #pk_str,
2277 operation: ::rustango::audit::AuditOp::Update,
2278 source: ::rustango::audit::current_source(),
2279 changes: ::rustango::audit::diff_changes(
2280 &_audit_before,
2281 &_audit_after,
2282 ),
2283 };
2284 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2285 }
2286 };
2287 (pre, post)
2288 }
2289 } else {
2290 (quote!(), quote!())
2291 };
2292
2293 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2297 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2298 if fields.pk_is_auto {
2299 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2300 } else {
2301 quote!(::std::format!("{}", &_row.#pk_ident))
2302 }
2303 } else {
2304 quote!(::std::string::String::new())
2305 };
2306 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
2307 let column_lit = c.column.as_str();
2308 let ident = &c.ident;
2309 quote! {
2310 (
2311 #column_lit,
2312 ::serde_json::to_value(&_row.#ident)
2313 .unwrap_or(::serde_json::Value::Null),
2314 )
2315 }
2316 });
2317 quote! {
2318 let _audit_source = ::rustango::audit::current_source();
2319 let mut _audit_entries:
2320 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2321 ::std::vec::Vec::with_capacity(rows.len());
2322 for _row in rows.iter() {
2323 _audit_entries.push(::rustango::audit::PendingEntry {
2324 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2325 entity_pk: #row_pk_str,
2326 operation: ::rustango::audit::AuditOp::Create,
2327 source: _audit_source.clone(),
2328 changes: ::rustango::audit::snapshot_changes(&[
2329 #( #row_pairs ),*
2330 ]),
2331 });
2332 }
2333 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2334 }
2335 } else {
2336 quote!()
2337 };
2338
2339 let save_method = if fields.pk_is_auto {
2340 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
2341 let pk_column_lit = pk_column.as_str();
2342 let assignments = &fields.update_assignments;
2343 let upsert_cols = &fields.upsert_update_columns;
2344 let upsert_pushes = &fields.insert_pushes;
2345 let upsert_returning = &fields.returning_cols;
2346 let upsert_auto_assigns = &fields.auto_assigns;
2347 let upsert_target_columns: Vec<String> = indexes
2356 .iter()
2357 .find(|i| i.unique && !i.columns.is_empty())
2358 .map(|i| i.columns.clone())
2359 .unwrap_or_else(|| vec![pk_column.clone()]);
2360 let upsert_target_lits = upsert_target_columns
2361 .iter()
2362 .map(String::as_str)
2363 .collect::<Vec<_>>();
2364 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2365 quote!(::rustango::core::ConflictClause::DoNothing)
2366 } else {
2367 quote!(::rustango::core::ConflictClause::DoUpdate {
2368 target: ::std::vec![ #( #upsert_target_lits ),* ],
2369 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2370 })
2371 };
2372 Some(quote! {
2373 #[cfg(feature = "postgres")]
2391 pub async fn save(
2392 &mut self,
2393 pool: &::rustango::sql::sqlx::PgPool,
2394 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2395 #pool_to_save_on
2396 }
2397
2398 #[cfg(feature = "postgres")]
2409 pub async fn save_on #executor_generics (
2410 &mut self,
2411 #executor_param,
2412 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2413 #executor_where
2414 {
2415 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2416 return self.insert_on(#executor_passes_to_data_write).await;
2417 }
2418 #audit_update_pre
2419 let _query = ::rustango::core::UpdateQuery {
2420 model: <Self as ::rustango::core::Model>::SCHEMA,
2421 set: ::std::vec![ #( #assignments ),* ],
2422 where_clause: ::rustango::core::WhereExpr::Predicate(
2423 ::rustango::core::Filter {
2424 column: #pk_column_lit,
2425 op: ::rustango::core::Op::Eq,
2426 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2427 ::core::clone::Clone::clone(&self.#pk_ident)
2428 ),
2429 }
2430 ),
2431 };
2432 let _ = ::rustango::sql::update_on(
2433 #executor_passes_to_data_write,
2434 &_query,
2435 ).await?;
2436 #audit_update_post
2437 ::core::result::Result::Ok(())
2438 }
2439
2440 #[cfg(feature = "postgres")]
2451 pub async fn save_on_with #executor_generics (
2452 &mut self,
2453 #executor_param,
2454 source: ::rustango::audit::AuditSource,
2455 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2456 #executor_where
2457 {
2458 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2459 }
2460
2461 #[cfg(feature = "postgres")]
2471 pub async fn upsert(
2472 &mut self,
2473 pool: &::rustango::sql::sqlx::PgPool,
2474 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2475 #pool_to_upsert_on
2476 }
2477
2478 #[cfg(feature = "postgres")]
2484 pub async fn upsert_on #executor_generics (
2485 &mut self,
2486 #executor_param,
2487 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2488 #executor_where
2489 {
2490 let mut _columns: ::std::vec::Vec<&'static str> =
2491 ::std::vec::Vec::new();
2492 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2493 ::std::vec::Vec::new();
2494 #( #upsert_pushes )*
2495 let query = ::rustango::core::InsertQuery {
2496 model: <Self as ::rustango::core::Model>::SCHEMA,
2497 columns: _columns,
2498 values: _values,
2499 returning: ::std::vec![ #( #upsert_returning ),* ],
2500 on_conflict: ::core::option::Option::Some(#conflict_clause),
2501 };
2502 let _returning_row_v = ::rustango::sql::insert_returning_on(
2503 #executor_passes_to_data_write,
2504 &query,
2505 ).await?;
2506 let _returning_row = &_returning_row_v;
2507 #( #upsert_auto_assigns )*
2508 ::core::result::Result::Ok(())
2509 }
2510 })
2511 } else {
2512 None
2513 };
2514
2515 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2516 let pk_column_lit = pk_column.as_str();
2517 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2524 let col_lit = col;
2525 quote! {
2526 pub async fn soft_delete_on #executor_generics (
2536 &self,
2537 #executor_param,
2538 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2539 #executor_where
2540 {
2541 let _query = ::rustango::core::UpdateQuery {
2542 model: <Self as ::rustango::core::Model>::SCHEMA,
2543 set: ::std::vec![
2544 ::rustango::core::Assignment {
2545 column: #col_lit,
2546 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2547 ::chrono::Utc::now()
2548 ),
2549 },
2550 ],
2551 where_clause: ::rustango::core::WhereExpr::Predicate(
2552 ::rustango::core::Filter {
2553 column: #pk_column_lit,
2554 op: ::rustango::core::Op::Eq,
2555 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2556 ::core::clone::Clone::clone(&self.#pk_ident)
2557 ),
2558 }
2559 ),
2560 };
2561 let _affected = ::rustango::sql::update_on(
2562 #executor_passes_to_data_write,
2563 &_query,
2564 ).await?;
2565 #audit_softdelete_emit
2566 ::core::result::Result::Ok(_affected)
2567 }
2568
2569 pub async fn restore_on #executor_generics (
2576 &self,
2577 #executor_param,
2578 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2579 #executor_where
2580 {
2581 let _query = ::rustango::core::UpdateQuery {
2582 model: <Self as ::rustango::core::Model>::SCHEMA,
2583 set: ::std::vec![
2584 ::rustango::core::Assignment {
2585 column: #col_lit,
2586 value: ::rustango::core::SqlValue::Null,
2587 },
2588 ],
2589 where_clause: ::rustango::core::WhereExpr::Predicate(
2590 ::rustango::core::Filter {
2591 column: #pk_column_lit,
2592 op: ::rustango::core::Op::Eq,
2593 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2594 ::core::clone::Clone::clone(&self.#pk_ident)
2595 ),
2596 }
2597 ),
2598 };
2599 let _affected = ::rustango::sql::update_on(
2600 #executor_passes_to_data_write,
2601 &_query,
2602 ).await?;
2603 #audit_restore_emit
2604 ::core::result::Result::Ok(_affected)
2605 }
2606 }
2607 } else {
2608 quote!()
2609 };
2610 quote! {
2611 #[cfg(feature = "postgres")]
2619 pub async fn delete(
2620 &self,
2621 pool: &::rustango::sql::sqlx::PgPool,
2622 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2623 #pool_to_delete_on
2624 }
2625
2626 #[cfg(feature = "postgres")]
2633 pub async fn delete_on #executor_generics (
2634 &self,
2635 #executor_param,
2636 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2637 #executor_where
2638 {
2639 let query = ::rustango::core::DeleteQuery {
2640 model: <Self as ::rustango::core::Model>::SCHEMA,
2641 where_clause: ::rustango::core::WhereExpr::Predicate(
2642 ::rustango::core::Filter {
2643 column: #pk_column_lit,
2644 op: ::rustango::core::Op::Eq,
2645 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2646 ::core::clone::Clone::clone(&self.#pk_ident)
2647 ),
2648 }
2649 ),
2650 };
2651 let _affected = ::rustango::sql::delete_on(
2652 #executor_passes_to_data_write,
2653 &query,
2654 ).await?;
2655 #audit_delete_emit
2656 ::core::result::Result::Ok(_affected)
2657 }
2658
2659 #[cfg(feature = "postgres")]
2665 pub async fn delete_on_with #executor_generics (
2666 &self,
2667 #executor_param,
2668 source: ::rustango::audit::AuditSource,
2669 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2670 #executor_where
2671 {
2672 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2673 }
2674 #pool_delete_method
2675 #pool_insert_method
2676 #pool_save_method
2677 #soft_delete_methods
2678 }
2679 });
2680
2681 let insert_method = if fields.has_auto {
2682 let pushes = &fields.insert_pushes;
2683 let returning_cols = &fields.returning_cols;
2684 let auto_assigns = &fields.auto_assigns;
2685 quote! {
2686 #[cfg(feature = "postgres")]
2695 pub async fn insert(
2696 &mut self,
2697 pool: &::rustango::sql::sqlx::PgPool,
2698 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2699 #pool_to_insert_on
2700 }
2701
2702 #[cfg(feature = "postgres")]
2708 pub async fn insert_on #executor_generics (
2709 &mut self,
2710 #executor_param,
2711 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2712 #executor_where
2713 {
2714 let mut _columns: ::std::vec::Vec<&'static str> =
2715 ::std::vec::Vec::new();
2716 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2717 ::std::vec::Vec::new();
2718 #( #pushes )*
2719 let query = ::rustango::core::InsertQuery {
2720 model: <Self as ::rustango::core::Model>::SCHEMA,
2721 columns: _columns,
2722 values: _values,
2723 returning: ::std::vec![ #( #returning_cols ),* ],
2724 on_conflict: ::core::option::Option::None,
2725 };
2726 let _returning_row_v = ::rustango::sql::insert_returning_on(
2727 #executor_passes_to_data_write,
2728 &query,
2729 ).await?;
2730 let _returning_row = &_returning_row_v;
2731 #( #auto_assigns )*
2732 #audit_insert_emit
2733 ::core::result::Result::Ok(())
2734 }
2735
2736 #[cfg(feature = "postgres")]
2742 pub async fn insert_on_with #executor_generics (
2743 &mut self,
2744 #executor_param,
2745 source: ::rustango::audit::AuditSource,
2746 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2747 #executor_where
2748 {
2749 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2750 }
2751 }
2752 } else {
2753 let insert_columns = &fields.insert_columns;
2754 let insert_values = &fields.insert_values;
2755 quote! {
2756 #[cfg(feature = "postgres")]
2762 pub async fn insert(
2763 &self,
2764 pool: &::rustango::sql::sqlx::PgPool,
2765 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2766 self.insert_on(pool).await
2767 }
2768
2769 #[cfg(feature = "postgres")]
2775 pub async fn insert_on<'_c, _E>(
2776 &self,
2777 _executor: _E,
2778 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2779 where
2780 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2781 {
2782 let query = ::rustango::core::InsertQuery {
2783 model: <Self as ::rustango::core::Model>::SCHEMA,
2784 columns: ::std::vec![ #( #insert_columns ),* ],
2785 values: ::std::vec![ #( #insert_values ),* ],
2786 returning: ::std::vec::Vec::new(),
2787 on_conflict: ::core::option::Option::None,
2788 };
2789 ::rustango::sql::insert_on(_executor, &query).await
2790 }
2791 }
2792 };
2793
2794 let bulk_insert_method = if fields.has_auto {
2795 let cols_no_auto = &fields.bulk_columns_no_auto;
2796 let cols_all = &fields.bulk_columns_all;
2797 let pushes_no_auto = &fields.bulk_pushes_no_auto;
2798 let pushes_all = &fields.bulk_pushes_all;
2799 let returning_cols = &fields.returning_cols;
2800 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2801 let uniformity = &fields.bulk_auto_uniformity;
2802 let first_auto_ident = fields
2803 .first_auto_ident
2804 .as_ref()
2805 .expect("has_auto implies first_auto_ident is Some");
2806 quote! {
2807 #[cfg(feature = "postgres")]
2821 pub async fn bulk_insert(
2822 rows: &mut [Self],
2823 pool: &::rustango::sql::sqlx::PgPool,
2824 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2825 #pool_to_bulk_insert_on
2826 }
2827
2828 #[cfg(feature = "postgres")]
2834 pub async fn bulk_insert_on #executor_generics (
2835 rows: &mut [Self],
2836 #executor_param,
2837 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2838 #executor_where
2839 {
2840 if rows.is_empty() {
2841 return ::core::result::Result::Ok(());
2842 }
2843 let _first_unset = matches!(
2844 rows[0].#first_auto_ident,
2845 ::rustango::sql::Auto::Unset
2846 );
2847 #( #uniformity )*
2848
2849 let mut _all_rows: ::std::vec::Vec<
2850 ::std::vec::Vec<::rustango::core::SqlValue>,
2851 > = ::std::vec::Vec::with_capacity(rows.len());
2852 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2853 for _row in rows.iter() {
2854 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2855 ::std::vec::Vec::new();
2856 #( #pushes_no_auto )*
2857 _all_rows.push(_row_vals);
2858 }
2859 ::std::vec![ #( #cols_no_auto ),* ]
2860 } else {
2861 for _row in rows.iter() {
2862 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2863 ::std::vec::Vec::new();
2864 #( #pushes_all )*
2865 _all_rows.push(_row_vals);
2866 }
2867 ::std::vec![ #( #cols_all ),* ]
2868 };
2869
2870 let _query = ::rustango::core::BulkInsertQuery {
2871 model: <Self as ::rustango::core::Model>::SCHEMA,
2872 columns: _columns,
2873 rows: _all_rows,
2874 returning: ::std::vec![ #( #returning_cols ),* ],
2875 on_conflict: ::core::option::Option::None,
2876 };
2877 let _returned = ::rustango::sql::bulk_insert_on(
2878 #executor_passes_to_data_write,
2879 &_query,
2880 ).await?;
2881 if _returned.len() != rows.len() {
2882 return ::core::result::Result::Err(
2883 ::rustango::sql::ExecError::Sql(
2884 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2885 expected: rows.len(),
2886 actual: _returned.len(),
2887 }
2888 )
2889 );
2890 }
2891 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2892 #auto_assigns_for_row
2893 }
2894 #audit_bulk_insert_emit
2895 ::core::result::Result::Ok(())
2896 }
2897 }
2898 } else {
2899 let cols_all = &fields.bulk_columns_all;
2900 let pushes_all = &fields.bulk_pushes_all;
2901 quote! {
2902 #[cfg(feature = "postgres")]
2912 pub async fn bulk_insert(
2913 rows: &[Self],
2914 pool: &::rustango::sql::sqlx::PgPool,
2915 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2916 Self::bulk_insert_on(rows, pool).await
2917 }
2918
2919 #[cfg(feature = "postgres")]
2925 pub async fn bulk_insert_on<'_c, _E>(
2926 rows: &[Self],
2927 _executor: _E,
2928 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2929 where
2930 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2931 {
2932 if rows.is_empty() {
2933 return ::core::result::Result::Ok(());
2934 }
2935 let mut _all_rows: ::std::vec::Vec<
2936 ::std::vec::Vec<::rustango::core::SqlValue>,
2937 > = ::std::vec::Vec::with_capacity(rows.len());
2938 for _row in rows.iter() {
2939 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2940 ::std::vec::Vec::new();
2941 #( #pushes_all )*
2942 _all_rows.push(_row_vals);
2943 }
2944 let _query = ::rustango::core::BulkInsertQuery {
2945 model: <Self as ::rustango::core::Model>::SCHEMA,
2946 columns: ::std::vec![ #( #cols_all ),* ],
2947 rows: _all_rows,
2948 returning: ::std::vec::Vec::new(),
2949 on_conflict: ::core::option::Option::None,
2950 };
2951 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2952 ::core::result::Result::Ok(())
2953 }
2954 }
2955 };
2956
2957 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2958 quote! {
2959 #[doc(hidden)]
2964 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2965 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2966 ::core::clone::Clone::clone(&self.#pk_ident)
2967 )
2968 }
2969 }
2970 });
2971
2972 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2973 quote! {
2974 impl ::rustango::sql::HasPkValue for #struct_name {
2975 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2976 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2977 ::core::clone::Clone::clone(&self.#pk_ident)
2978 )
2979 }
2980 }
2981 }
2982 });
2983
2984 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2985
2986 let assign_auto_pk_pool_impl = {
2992 let auto_assigns = &fields.auto_assigns;
2993 let auto_assigns_sqlite: Vec<TokenStream2> = fields
2999 .auto_field_idents
3000 .iter()
3001 .map(|(ident, column)| {
3002 quote! {
3003 self.#ident = ::rustango::sql::try_get_returning_sqlite(
3004 _returning_row, #column
3005 )?;
3006 }
3007 })
3008 .collect();
3009 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
3010 let value_ty = fields
3028 .first_auto_value_ty
3029 .as_ref()
3030 .expect("first_auto_value_ty set whenever first_auto_ident is");
3031 quote! {
3032 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
3033 ::rustango_from_mysql_auto_id(_id)?;
3034 self.#first = ::rustango::sql::Auto::Set(_converted);
3035 ::core::result::Result::Ok(())
3036 }
3037 } else {
3038 quote! {
3039 let _ = _id;
3040 ::core::result::Result::Ok(())
3041 }
3042 };
3043 quote! {
3044 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
3045 fn __rustango_assign_from_pg_row(
3046 &mut self,
3047 _returning_row: &::rustango::sql::PgReturningRow,
3048 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3049 #( #auto_assigns )*
3050 ::core::result::Result::Ok(())
3051 }
3052 fn __rustango_assign_from_mysql_id(
3053 &mut self,
3054 _id: i64,
3055 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3056 #mysql_body
3057 }
3058 fn __rustango_assign_from_sqlite_row(
3059 &mut self,
3060 _returning_row: &::rustango::sql::SqliteReturningRow,
3061 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3062 #( #auto_assigns_sqlite )*
3063 ::core::result::Result::Ok(())
3064 }
3065 }
3066 }
3067 };
3068
3069 let from_aliased_row_inits = &fields.from_aliased_row_inits;
3070 let aliased_row_helper = quote! {
3071 #[doc(hidden)]
3077 #[cfg(feature = "postgres")]
3078 pub fn __rustango_from_aliased_row(
3079 row: &::rustango::sql::sqlx::postgres::PgRow,
3080 prefix: &str,
3081 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3082 ::core::result::Result::Ok(Self {
3083 #( #from_aliased_row_inits ),*
3084 })
3085 }
3086 };
3087 let aliased_row_helper_my = quote! {
3090 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
3091 #( #from_aliased_row_inits ),*
3092 });
3093 };
3094
3095 let aliased_row_helper_sqlite = quote! {
3098 ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
3099 #( #from_aliased_row_inits ),*
3100 });
3101 };
3102
3103 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
3104 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
3105 let load_related_impl_sqlite =
3106 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
3107
3108 quote! {
3109 impl #struct_name {
3110 #[must_use]
3112 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
3113 ::rustango::query::QuerySet::new()
3114 }
3115
3116 #insert_method
3117
3118 #bulk_insert_method
3119
3120 #save_method
3121
3122 #pk_methods
3123
3124 #pk_value_helper
3125
3126 #aliased_row_helper
3127
3128 #column_consts
3129 }
3130
3131 #aliased_row_helper_my
3132
3133 #aliased_row_helper_sqlite
3134
3135 #load_related_impl
3136
3137 #load_related_impl_my
3138
3139 #load_related_impl_sqlite
3140
3141 #has_pk_value_impl
3142
3143 #fk_pk_access_impl
3144
3145 #assign_auto_pk_pool_impl
3146 }
3147}
3148
3149fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
3153 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
3154 let col_lit = column.as_str();
3155 quote! {
3156 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
3157 _returning_row,
3158 #col_lit,
3159 )?;
3160 }
3161 });
3162 quote! { #( #lines )* }
3163}
3164
3165fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
3167 let lines = entries.iter().map(|e| {
3168 let ident = &e.ident;
3169 let col_ty = column_type_ident(ident);
3170 quote! {
3171 #[allow(non_upper_case_globals)]
3172 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
3173 }
3174 });
3175 quote! { #(#lines)* }
3176}
3177
3178fn column_module_tokens(
3181 module_ident: &syn::Ident,
3182 struct_name: &syn::Ident,
3183 entries: &[ColumnEntry],
3184) -> TokenStream2 {
3185 let items = entries.iter().map(|e| {
3186 let col_ty = column_type_ident(&e.ident);
3187 let value_ty = &e.value_ty;
3188 let name = &e.name;
3189 let column = &e.column;
3190 let field_type_tokens = &e.field_type_tokens;
3191 quote! {
3192 #[derive(::core::clone::Clone, ::core::marker::Copy)]
3193 pub struct #col_ty;
3194
3195 impl ::rustango::core::Column for #col_ty {
3196 type Model = super::#struct_name;
3197 type Value = #value_ty;
3198 const NAME: &'static str = #name;
3199 const COLUMN: &'static str = #column;
3200 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
3201 }
3202 }
3203 });
3204 quote! {
3205 #[doc(hidden)]
3206 #[allow(non_camel_case_types, non_snake_case)]
3207 pub mod #module_ident {
3208 #[allow(unused_imports)]
3213 use super::*;
3214 #(#items)*
3215 }
3216 }
3217}
3218
3219fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3220 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3221}
3222
3223fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3224 syn::Ident::new(
3225 &format!("__rustango_cols_{struct_name}"),
3226 struct_name.span(),
3227 )
3228}
3229
3230fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3231 quote! {
3242 #[cfg(feature = "postgres")]
3243 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3244 for #struct_name
3245 {
3246 fn from_row(
3247 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3248 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3249 ::core::result::Result::Ok(Self {
3250 #( #from_row_inits ),*
3251 })
3252 }
3253 }
3254
3255 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3256 #( #from_row_inits ),*
3257 });
3258
3259 ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
3260 #( #from_row_inits ),*
3261 });
3262 }
3263}
3264
3265struct ContainerAttrs {
3266 table: Option<String>,
3267 display: Option<(String, proc_macro2::Span)>,
3268 app: Option<String>,
3275 admin: Option<AdminAttrs>,
3280 audit: Option<AuditAttrs>,
3286 permissions: bool,
3290 m2m: Vec<M2MAttr>,
3294 indexes: Vec<IndexAttr>,
3300 checks: Vec<CheckAttr>,
3303 composite_fks: Vec<CompositeFkAttr>,
3307 generic_fks: Vec<GenericFkAttr>,
3311 scope: Option<String>,
3317}
3318
3319struct IndexAttr {
3321 name: Option<String>,
3323 columns: Vec<String>,
3325 unique: bool,
3327}
3328
3329struct CheckAttr {
3331 name: String,
3332 expr: String,
3333}
3334
3335struct CompositeFkAttr {
3341 name: String,
3343 to: String,
3345 from: Vec<String>,
3347 on: Vec<String>,
3349}
3350
3351struct GenericFkAttr {
3356 name: String,
3358 ct_column: String,
3360 pk_column: String,
3362}
3363
3364struct M2MAttr {
3366 name: String,
3368 to: String,
3370 through: String,
3372 src: String,
3374 dst: String,
3376}
3377
3378#[derive(Default)]
3384struct AuditAttrs {
3385 track: Option<(Vec<String>, proc_macro2::Span)>,
3389}
3390
3391#[derive(Default)]
3396struct AdminAttrs {
3397 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3398 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3399 list_per_page: Option<usize>,
3400 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3401 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3402 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3403 actions: Option<(Vec<String>, proc_macro2::Span)>,
3406 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3410}
3411
3412fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3413 let mut out = ContainerAttrs {
3414 table: None,
3415 display: None,
3416 app: None,
3417 admin: None,
3418 audit: None,
3419 permissions: true,
3428 m2m: Vec::new(),
3429 indexes: Vec::new(),
3430 checks: Vec::new(),
3431 composite_fks: Vec::new(),
3432 generic_fks: Vec::new(),
3433 scope: None,
3434 };
3435 for attr in &input.attrs {
3436 if !attr.path().is_ident("rustango") {
3437 continue;
3438 }
3439 attr.parse_nested_meta(|meta| {
3440 if meta.path.is_ident("table") {
3441 let s: LitStr = meta.value()?.parse()?;
3442 let name = s.value();
3443 validate_table_name(&name, s.span())?;
3453 out.table = Some(name);
3454 return Ok(());
3455 }
3456 if meta.path.is_ident("display") {
3457 let s: LitStr = meta.value()?.parse()?;
3458 out.display = Some((s.value(), s.span()));
3459 return Ok(());
3460 }
3461 if meta.path.is_ident("app") {
3462 let s: LitStr = meta.value()?.parse()?;
3463 out.app = Some(s.value());
3464 return Ok(());
3465 }
3466 if meta.path.is_ident("scope") {
3467 let s: LitStr = meta.value()?.parse()?;
3468 let val = s.value();
3469 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
3470 return Err(meta.error(format!(
3471 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
3472 )));
3473 }
3474 out.scope = Some(val);
3475 return Ok(());
3476 }
3477 if meta.path.is_ident("admin") {
3478 let mut admin = AdminAttrs::default();
3479 meta.parse_nested_meta(|inner| {
3480 if inner.path.is_ident("list_display") {
3481 let s: LitStr = inner.value()?.parse()?;
3482 admin.list_display =
3483 Some((split_field_list(&s.value()), s.span()));
3484 return Ok(());
3485 }
3486 if inner.path.is_ident("search_fields") {
3487 let s: LitStr = inner.value()?.parse()?;
3488 admin.search_fields =
3489 Some((split_field_list(&s.value()), s.span()));
3490 return Ok(());
3491 }
3492 if inner.path.is_ident("readonly_fields") {
3493 let s: LitStr = inner.value()?.parse()?;
3494 admin.readonly_fields =
3495 Some((split_field_list(&s.value()), s.span()));
3496 return Ok(());
3497 }
3498 if inner.path.is_ident("list_per_page") {
3499 let lit: syn::LitInt = inner.value()?.parse()?;
3500 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3501 return Ok(());
3502 }
3503 if inner.path.is_ident("ordering") {
3504 let s: LitStr = inner.value()?.parse()?;
3505 admin.ordering = Some((
3506 parse_ordering_list(&s.value()),
3507 s.span(),
3508 ));
3509 return Ok(());
3510 }
3511 if inner.path.is_ident("list_filter") {
3512 let s: LitStr = inner.value()?.parse()?;
3513 admin.list_filter =
3514 Some((split_field_list(&s.value()), s.span()));
3515 return Ok(());
3516 }
3517 if inner.path.is_ident("actions") {
3518 let s: LitStr = inner.value()?.parse()?;
3519 admin.actions =
3520 Some((split_field_list(&s.value()), s.span()));
3521 return Ok(());
3522 }
3523 if inner.path.is_ident("fieldsets") {
3524 let s: LitStr = inner.value()?.parse()?;
3525 admin.fieldsets =
3526 Some((parse_fieldset_list(&s.value()), s.span()));
3527 return Ok(());
3528 }
3529 Err(inner.error(
3530 "unknown admin attribute (supported: \
3531 `list_display`, `search_fields`, `readonly_fields`, \
3532 `list_filter`, `list_per_page`, `ordering`, `actions`, \
3533 `fieldsets`)",
3534 ))
3535 })?;
3536 out.admin = Some(admin);
3537 return Ok(());
3538 }
3539 if meta.path.is_ident("audit") {
3540 let mut audit = AuditAttrs::default();
3541 meta.parse_nested_meta(|inner| {
3542 if inner.path.is_ident("track") {
3543 let s: LitStr = inner.value()?.parse()?;
3544 audit.track =
3545 Some((split_field_list(&s.value()), s.span()));
3546 return Ok(());
3547 }
3548 Err(inner.error(
3549 "unknown audit attribute (supported: `track`)",
3550 ))
3551 })?;
3552 out.audit = Some(audit);
3553 return Ok(());
3554 }
3555 if meta.path.is_ident("permissions") {
3556 if let Ok(v) = meta.value() {
3561 let lit: syn::LitBool = v.parse()?;
3562 out.permissions = lit.value;
3563 } else {
3564 out.permissions = true;
3565 }
3566 return Ok(());
3567 }
3568 if meta.path.is_ident("unique_together") {
3569 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3578 out.indexes.push(IndexAttr { name, columns, unique: true });
3579 return Ok(());
3580 }
3581 if meta.path.is_ident("index_together") {
3582 let (columns, name) = parse_together_attr(&meta, "index_together")?;
3588 out.indexes.push(IndexAttr { name, columns, unique: false });
3589 return Ok(());
3590 }
3591 if meta.path.is_ident("index") {
3592 let cols_lit: LitStr = meta.value()?.parse()?;
3600 let columns = split_field_list(&cols_lit.value());
3601 out.indexes.push(IndexAttr { name: None, columns, unique: false });
3602 return Ok(());
3603 }
3604 if meta.path.is_ident("check") {
3605 let mut name: Option<String> = None;
3607 let mut expr: Option<String> = None;
3608 meta.parse_nested_meta(|inner| {
3609 if inner.path.is_ident("name") {
3610 let s: LitStr = inner.value()?.parse()?;
3611 name = Some(s.value());
3612 return Ok(());
3613 }
3614 if inner.path.is_ident("expr") {
3615 let s: LitStr = inner.value()?.parse()?;
3616 expr = Some(s.value());
3617 return Ok(());
3618 }
3619 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3620 })?;
3621 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3622 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3623 out.checks.push(CheckAttr { name, expr });
3624 return Ok(());
3625 }
3626 if meta.path.is_ident("generic_fk") {
3627 let mut gfk = GenericFkAttr {
3628 name: String::new(),
3629 ct_column: String::new(),
3630 pk_column: String::new(),
3631 };
3632 meta.parse_nested_meta(|inner| {
3633 if inner.path.is_ident("name") {
3634 let s: LitStr = inner.value()?.parse()?;
3635 gfk.name = s.value();
3636 return Ok(());
3637 }
3638 if inner.path.is_ident("ct_column") {
3639 let s: LitStr = inner.value()?.parse()?;
3640 gfk.ct_column = s.value();
3641 return Ok(());
3642 }
3643 if inner.path.is_ident("pk_column") {
3644 let s: LitStr = inner.value()?.parse()?;
3645 gfk.pk_column = s.value();
3646 return Ok(());
3647 }
3648 Err(inner.error(
3649 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3650 ))
3651 })?;
3652 if gfk.name.is_empty() {
3653 return Err(meta.error("generic_fk requires `name = \"...\"`"));
3654 }
3655 if gfk.ct_column.is_empty() {
3656 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3657 }
3658 if gfk.pk_column.is_empty() {
3659 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3660 }
3661 out.generic_fks.push(gfk);
3662 return Ok(());
3663 }
3664 if meta.path.is_ident("fk_composite") {
3665 let mut fk = CompositeFkAttr {
3666 name: String::new(),
3667 to: String::new(),
3668 from: Vec::new(),
3669 on: Vec::new(),
3670 };
3671 meta.parse_nested_meta(|inner| {
3672 if inner.path.is_ident("name") {
3673 let s: LitStr = inner.value()?.parse()?;
3674 fk.name = s.value();
3675 return Ok(());
3676 }
3677 if inner.path.is_ident("to") {
3678 let s: LitStr = inner.value()?.parse()?;
3679 fk.to = s.value();
3680 return Ok(());
3681 }
3682 if inner.path.is_ident("on") || inner.path.is_ident("from") {
3685 let value = inner.value()?;
3686 let content;
3687 syn::parenthesized!(content in value);
3688 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3689 content.parse_terminated(
3690 |p| p.parse::<syn::LitStr>(),
3691 syn::Token![,],
3692 )?;
3693 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3694 if inner.path.is_ident("on") {
3695 fk.on = cols;
3696 } else {
3697 fk.from = cols;
3698 }
3699 return Ok(());
3700 }
3701 Err(inner.error(
3702 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3703 ))
3704 })?;
3705 if fk.name.is_empty() {
3706 return Err(meta.error("fk_composite requires `name = \"...\"`"));
3707 }
3708 if fk.to.is_empty() {
3709 return Err(meta.error("fk_composite requires `to = \"...\"`"));
3710 }
3711 if fk.from.is_empty() || fk.on.is_empty() {
3712 return Err(meta.error(
3713 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3714 ));
3715 }
3716 if fk.from.len() != fk.on.len() {
3717 return Err(meta.error(format!(
3718 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3719 fk.from.len(),
3720 fk.on.len(),
3721 )));
3722 }
3723 out.composite_fks.push(fk);
3724 return Ok(());
3725 }
3726 if meta.path.is_ident("m2m") {
3727 let mut m2m = M2MAttr {
3728 name: String::new(),
3729 to: String::new(),
3730 through: String::new(),
3731 src: String::new(),
3732 dst: String::new(),
3733 };
3734 meta.parse_nested_meta(|inner| {
3735 if inner.path.is_ident("name") {
3736 let s: LitStr = inner.value()?.parse()?;
3737 m2m.name = s.value();
3738 return Ok(());
3739 }
3740 if inner.path.is_ident("to") {
3741 let s: LitStr = inner.value()?.parse()?;
3742 m2m.to = s.value();
3743 return Ok(());
3744 }
3745 if inner.path.is_ident("through") {
3746 let s: LitStr = inner.value()?.parse()?;
3747 m2m.through = s.value();
3748 return Ok(());
3749 }
3750 if inner.path.is_ident("src") {
3751 let s: LitStr = inner.value()?.parse()?;
3752 m2m.src = s.value();
3753 return Ok(());
3754 }
3755 if inner.path.is_ident("dst") {
3756 let s: LitStr = inner.value()?.parse()?;
3757 m2m.dst = s.value();
3758 return Ok(());
3759 }
3760 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3761 })?;
3762 if m2m.name.is_empty() {
3763 return Err(meta.error("m2m requires `name = \"...\"`"));
3764 }
3765 if m2m.to.is_empty() {
3766 return Err(meta.error("m2m requires `to = \"...\"`"));
3767 }
3768 if m2m.through.is_empty() {
3769 return Err(meta.error("m2m requires `through = \"...\"`"));
3770 }
3771 if m2m.src.is_empty() {
3772 return Err(meta.error("m2m requires `src = \"...\"`"));
3773 }
3774 if m2m.dst.is_empty() {
3775 return Err(meta.error("m2m requires `dst = \"...\"`"));
3776 }
3777 out.m2m.push(m2m);
3778 return Ok(());
3779 }
3780 Err(meta.error("unknown rustango container attribute"))
3781 })?;
3782 }
3783 Ok(out)
3784}
3785
3786fn split_field_list(raw: &str) -> Vec<String> {
3790 raw.split(',')
3791 .map(str::trim)
3792 .filter(|s| !s.is_empty())
3793 .map(str::to_owned)
3794 .collect()
3795}
3796
3797fn parse_together_attr(
3805 meta: &syn::meta::ParseNestedMeta<'_>,
3806 attr: &str,
3807) -> syn::Result<(Vec<String>, Option<String>)> {
3808 if meta.input.peek(syn::Token![=]) {
3811 let cols_lit: LitStr = meta.value()?.parse()?;
3812 let columns = split_field_list(&cols_lit.value());
3813 check_together_columns(meta, attr, &columns)?;
3814 return Ok((columns, None));
3815 }
3816 let mut columns: Option<Vec<String>> = None;
3817 let mut name: Option<String> = None;
3818 meta.parse_nested_meta(|inner| {
3819 if inner.path.is_ident("columns") {
3820 let s: LitStr = inner.value()?.parse()?;
3821 columns = Some(split_field_list(&s.value()));
3822 return Ok(());
3823 }
3824 if inner.path.is_ident("name") {
3825 let s: LitStr = inner.value()?.parse()?;
3826 name = Some(s.value());
3827 return Ok(());
3828 }
3829 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3830 })?;
3831 let columns = columns.ok_or_else(|| {
3832 meta.error(format!(
3833 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3834 ))
3835 })?;
3836 check_together_columns(meta, attr, &columns)?;
3837 Ok((columns, name))
3838}
3839
3840fn check_together_columns(
3841 meta: &syn::meta::ParseNestedMeta<'_>,
3842 attr: &str,
3843 columns: &[String],
3844) -> syn::Result<()> {
3845 if columns.len() < 2 {
3846 let single = if attr == "unique_together" {
3847 "#[rustango(unique)] on the field"
3848 } else {
3849 "#[rustango(index)] on the field"
3850 };
3851 return Err(meta.error(format!(
3852 "{attr} expects two or more columns; for a single-column equivalent use {single}",
3853 )));
3854 }
3855 Ok(())
3856}
3857
3858fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3867 raw.split('|')
3868 .map(str::trim)
3869 .filter(|s| !s.is_empty())
3870 .map(|section| {
3871 let (title, rest) = match section.split_once(':') {
3873 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
3874 _ => (String::new(), section),
3875 };
3876 let fields = split_field_list(rest);
3877 (title, fields)
3878 })
3879 .collect()
3880}
3881
3882fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3885 raw.split(',')
3886 .map(str::trim)
3887 .filter(|s| !s.is_empty())
3888 .map(|spec| {
3889 spec.strip_prefix('-')
3890 .map_or((spec.to_owned(), false), |rest| {
3891 (rest.trim().to_owned(), true)
3892 })
3893 })
3894 .collect()
3895}
3896
3897struct FieldAttrs {
3898 column: Option<String>,
3899 primary_key: bool,
3900 fk: Option<String>,
3901 o2o: Option<String>,
3902 on: Option<String>,
3903 max_length: Option<u32>,
3904 min: Option<i64>,
3905 max: Option<i64>,
3906 default: Option<String>,
3907 auto_uuid: bool,
3913 auto_now_add: bool,
3918 auto_now: bool,
3924 soft_delete: bool,
3929 unique: bool,
3932 index: bool,
3936 index_unique: bool,
3937 index_name: Option<String>,
3938 generated_as: Option<String>,
3944}
3945
3946fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3947 let mut out = FieldAttrs {
3948 column: None,
3949 primary_key: false,
3950 fk: None,
3951 o2o: None,
3952 on: None,
3953 max_length: None,
3954 min: None,
3955 max: None,
3956 default: None,
3957 auto_uuid: false,
3958 auto_now_add: false,
3959 auto_now: false,
3960 soft_delete: false,
3961 unique: false,
3962 index: false,
3963 index_unique: false,
3964 index_name: None,
3965 generated_as: None,
3966 };
3967 for attr in &field.attrs {
3968 if !attr.path().is_ident("rustango") {
3969 continue;
3970 }
3971 attr.parse_nested_meta(|meta| {
3972 if meta.path.is_ident("column") {
3973 let s: LitStr = meta.value()?.parse()?;
3974 let name = s.value();
3975 validate_sql_identifier(&name, "column", s.span())?;
3976 out.column = Some(name);
3977 return Ok(());
3978 }
3979 if meta.path.is_ident("primary_key") {
3980 out.primary_key = true;
3981 return Ok(());
3982 }
3983 if meta.path.is_ident("fk") {
3984 let s: LitStr = meta.value()?.parse()?;
3985 out.fk = Some(s.value());
3986 return Ok(());
3987 }
3988 if meta.path.is_ident("o2o") {
3989 let s: LitStr = meta.value()?.parse()?;
3990 out.o2o = Some(s.value());
3991 return Ok(());
3992 }
3993 if meta.path.is_ident("on") {
3994 let s: LitStr = meta.value()?.parse()?;
3995 out.on = Some(s.value());
3996 return Ok(());
3997 }
3998 if meta.path.is_ident("max_length") {
3999 let lit: syn::LitInt = meta.value()?.parse()?;
4000 out.max_length = Some(lit.base10_parse::<u32>()?);
4001 return Ok(());
4002 }
4003 if meta.path.is_ident("min") {
4004 out.min = Some(parse_signed_i64(&meta)?);
4005 return Ok(());
4006 }
4007 if meta.path.is_ident("max") {
4008 out.max = Some(parse_signed_i64(&meta)?);
4009 return Ok(());
4010 }
4011 if meta.path.is_ident("default") {
4012 let s: LitStr = meta.value()?.parse()?;
4013 out.default = Some(s.value());
4014 return Ok(());
4015 }
4016 if meta.path.is_ident("generated_as") {
4017 let s: LitStr = meta.value()?.parse()?;
4018 out.generated_as = Some(s.value());
4019 return Ok(());
4020 }
4021 if meta.path.is_ident("auto_uuid") {
4022 out.auto_uuid = true;
4023 out.primary_key = true;
4027 if out.default.is_none() {
4028 out.default = Some("gen_random_uuid()".into());
4029 }
4030 return Ok(());
4031 }
4032 if meta.path.is_ident("auto_now_add") {
4033 out.auto_now_add = true;
4034 if out.default.is_none() {
4035 out.default = Some("now()".into());
4036 }
4037 return Ok(());
4038 }
4039 if meta.path.is_ident("auto_now") {
4040 out.auto_now = true;
4041 if out.default.is_none() {
4042 out.default = Some("now()".into());
4043 }
4044 return Ok(());
4045 }
4046 if meta.path.is_ident("soft_delete") {
4047 out.soft_delete = true;
4048 return Ok(());
4049 }
4050 if meta.path.is_ident("unique") {
4051 out.unique = true;
4052 return Ok(());
4053 }
4054 if meta.path.is_ident("index") {
4055 out.index = true;
4056 if meta.input.peek(syn::token::Paren) {
4058 meta.parse_nested_meta(|inner| {
4059 if inner.path.is_ident("unique") {
4060 out.index_unique = true;
4061 return Ok(());
4062 }
4063 if inner.path.is_ident("name") {
4064 let s: LitStr = inner.value()?.parse()?;
4065 out.index_name = Some(s.value());
4066 return Ok(());
4067 }
4068 Err(inner
4069 .error("unknown index sub-attribute (supported: `unique`, `name`)"))
4070 })?;
4071 }
4072 return Ok(());
4073 }
4074 Err(meta.error("unknown rustango field attribute"))
4075 })?;
4076 }
4077 Ok(out)
4078}
4079
4080fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
4082 let expr: syn::Expr = meta.value()?.parse()?;
4083 match expr {
4084 syn::Expr::Lit(syn::ExprLit {
4085 lit: syn::Lit::Int(lit),
4086 ..
4087 }) => lit.base10_parse::<i64>(),
4088 syn::Expr::Unary(syn::ExprUnary {
4089 op: syn::UnOp::Neg(_),
4090 expr,
4091 ..
4092 }) => {
4093 if let syn::Expr::Lit(syn::ExprLit {
4094 lit: syn::Lit::Int(lit),
4095 ..
4096 }) = *expr
4097 {
4098 let v: i64 = lit.base10_parse()?;
4099 Ok(-v)
4100 } else {
4101 Err(syn::Error::new_spanned(expr, "expected integer literal"))
4102 }
4103 }
4104 other => Err(syn::Error::new_spanned(
4105 other,
4106 "expected integer literal (signed)",
4107 )),
4108 }
4109}
4110
4111struct FieldInfo<'a> {
4112 ident: &'a syn::Ident,
4113 column: String,
4114 primary_key: bool,
4115 auto: bool,
4119 value_ty: &'a Type,
4122 field_type_tokens: TokenStream2,
4124 schema: TokenStream2,
4125 from_row_init: TokenStream2,
4126 from_aliased_row_init: TokenStream2,
4132 fk_inner: Option<Type>,
4136 fk_pk_kind: DetectedKind,
4142 nullable: bool,
4150 auto_now: bool,
4156 auto_now_add: bool,
4162 soft_delete: bool,
4167 generated_as: Option<String>,
4172}
4173
4174fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
4188 validate_sql_identifier(name, "table", span)
4189}
4190
4191fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
4196 if name.is_empty() {
4197 return Err(syn::Error::new(
4198 span,
4199 format!("`{kind} = \"\"` is not a valid SQL identifier"),
4200 ));
4201 }
4202 let mut chars = name.chars();
4203 let first = chars.next().unwrap();
4204 if !(first.is_ascii_alphabetic() || first == '_') {
4205 return Err(syn::Error::new(
4206 span,
4207 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
4208 ));
4209 }
4210 for c in chars {
4211 if !(c.is_ascii_alphanumeric() || c == '_') {
4212 return Err(syn::Error::new(
4213 span,
4214 format!(
4215 "{kind} name `{name}` contains invalid character {c:?} — \
4216 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
4217 Hyphens in particular break FK / index name derivation \
4218 downstream; use underscores instead (e.g. `{}`)",
4219 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
4220 ),
4221 ));
4222 }
4223 }
4224 Ok(())
4225}
4226
4227fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
4228 let attrs = parse_field_attrs(field)?;
4229 let ident = field
4230 .ident
4231 .as_ref()
4232 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4233 let name = ident.to_string();
4234 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
4235 let primary_key = attrs.primary_key;
4236 let DetectedType {
4237 kind,
4238 nullable,
4239 auto: detected_auto,
4240 fk_inner,
4241 } = detect_type(&field.ty)?;
4242 check_bound_compatibility(field, &attrs, kind)?;
4243 let auto = detected_auto;
4244 if attrs.auto_uuid {
4250 if kind != DetectedKind::Uuid {
4251 return Err(syn::Error::new_spanned(
4252 field,
4253 "`#[rustango(auto_uuid)]` requires the field type to be \
4254 `Auto<uuid::Uuid>`",
4255 ));
4256 }
4257 if !detected_auto {
4258 return Err(syn::Error::new_spanned(
4259 field,
4260 "`#[rustango(auto_uuid)]` requires the field type to be \
4261 wrapped in `Auto<...>` so the macro skips the column on \
4262 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
4263 ));
4264 }
4265 }
4266 if attrs.auto_now_add || attrs.auto_now {
4267 if kind != DetectedKind::DateTime {
4268 return Err(syn::Error::new_spanned(
4269 field,
4270 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4271 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
4272 ));
4273 }
4274 if !detected_auto {
4275 return Err(syn::Error::new_spanned(
4276 field,
4277 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4278 the field type to be wrapped in `Auto<...>` so the macro skips \
4279 the column on INSERT and the DB DEFAULT (`now()`) fires",
4280 ));
4281 }
4282 }
4283 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
4284 return Err(syn::Error::new_spanned(
4285 field,
4286 "`#[rustango(soft_delete)]` requires the field type to be \
4287 `Option<chrono::DateTime<chrono::Utc>>`",
4288 ));
4289 }
4290 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
4291 if detected_auto && !primary_key && !is_mixin_auto {
4292 return Err(syn::Error::new_spanned(
4293 field,
4294 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
4295 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
4296 `auto_now`",
4297 ));
4298 }
4299 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
4300 return Err(syn::Error::new_spanned(
4301 field,
4302 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
4303 SERIAL / BIGSERIAL already supplies a default sequence.",
4304 ));
4305 }
4306 if fk_inner.is_some() && primary_key {
4307 return Err(syn::Error::new_spanned(
4308 field,
4309 "`ForeignKey<T>` is not allowed on a primary-key field — \
4310 a row's PK is its own identity, not a reference to a parent.",
4311 ));
4312 }
4313 if attrs.generated_as.is_some() {
4314 if primary_key {
4315 return Err(syn::Error::new_spanned(
4316 field,
4317 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4318 primary-key field — a PK must be writable so the row \
4319 has an identity at INSERT time.",
4320 ));
4321 }
4322 if attrs.default.is_some() {
4323 return Err(syn::Error::new_spanned(
4324 field,
4325 "`#[rustango(generated_as = \"…\")]` cannot combine with \
4326 `default = \"…\"` — Postgres rejects DEFAULT on \
4327 generated columns. The expression IS the default.",
4328 ));
4329 }
4330 if detected_auto {
4331 return Err(syn::Error::new_spanned(
4332 field,
4333 "`#[rustango(generated_as = \"…\")]` is not allowed on \
4334 an `Auto<T>` field — generated columns are computed \
4335 by the DB, not server-assigned via a sequence. Use a \
4336 plain Rust type (e.g. `f64`).",
4337 ));
4338 }
4339 if fk_inner.is_some() {
4340 return Err(syn::Error::new_spanned(
4341 field,
4342 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4343 ForeignKey field.",
4344 ));
4345 }
4346 }
4347 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
4348 let column_lit = column.as_str();
4349 let field_type_tokens = kind.variant_tokens();
4350 let max_length = optional_u32(attrs.max_length);
4351 let min = optional_i64(attrs.min);
4352 let max = optional_i64(attrs.max);
4353 let default = optional_str(attrs.default.as_deref());
4354
4355 let unique = attrs.unique;
4356 let generated_as = optional_str(attrs.generated_as.as_deref());
4357 let schema = quote! {
4358 ::rustango::core::FieldSchema {
4359 name: #name,
4360 column: #column_lit,
4361 ty: #field_type_tokens,
4362 nullable: #nullable,
4363 primary_key: #primary_key,
4364 relation: #relation,
4365 max_length: #max_length,
4366 min: #min,
4367 max: #max,
4368 default: #default,
4369 auto: #auto,
4370 unique: #unique,
4371 generated_as: #generated_as,
4372 }
4373 };
4374
4375 let from_row_init = quote! {
4376 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4377 };
4378 let from_aliased_row_init = quote! {
4379 #ident: ::rustango::sql::sqlx::Row::try_get(
4380 row,
4381 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4382 )?
4383 };
4384
4385 Ok(FieldInfo {
4386 ident,
4387 column,
4388 primary_key,
4389 auto,
4390 value_ty: &field.ty,
4391 field_type_tokens,
4392 schema,
4393 from_row_init,
4394 from_aliased_row_init,
4395 fk_inner: fk_inner.cloned(),
4396 fk_pk_kind: kind,
4397 nullable,
4398 auto_now: attrs.auto_now,
4399 auto_now_add: attrs.auto_now_add,
4400 soft_delete: attrs.soft_delete,
4401 generated_as: attrs.generated_as.clone(),
4402 })
4403}
4404
4405fn check_bound_compatibility(
4406 field: &syn::Field,
4407 attrs: &FieldAttrs,
4408 kind: DetectedKind,
4409) -> syn::Result<()> {
4410 if attrs.max_length.is_some() && kind != DetectedKind::String {
4411 return Err(syn::Error::new_spanned(
4412 field,
4413 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4414 ));
4415 }
4416 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4417 return Err(syn::Error::new_spanned(
4418 field,
4419 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4420 ));
4421 }
4422 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4423 if min > max {
4424 return Err(syn::Error::new_spanned(
4425 field,
4426 format!("`min` ({min}) is greater than `max` ({max})"),
4427 ));
4428 }
4429 }
4430 Ok(())
4431}
4432
4433fn optional_u32(value: Option<u32>) -> TokenStream2 {
4434 if let Some(v) = value {
4435 quote!(::core::option::Option::Some(#v))
4436 } else {
4437 quote!(::core::option::Option::None)
4438 }
4439}
4440
4441fn optional_i64(value: Option<i64>) -> TokenStream2 {
4442 if let Some(v) = value {
4443 quote!(::core::option::Option::Some(#v))
4444 } else {
4445 quote!(::core::option::Option::None)
4446 }
4447}
4448
4449fn optional_str(value: Option<&str>) -> TokenStream2 {
4450 if let Some(v) = value {
4451 quote!(::core::option::Option::Some(#v))
4452 } else {
4453 quote!(::core::option::Option::None)
4454 }
4455}
4456
4457fn relation_tokens(
4458 field: &syn::Field,
4459 attrs: &FieldAttrs,
4460 fk_inner: Option<&syn::Type>,
4461 table: &str,
4462) -> syn::Result<TokenStream2> {
4463 if let Some(inner) = fk_inner {
4464 if attrs.fk.is_some() || attrs.o2o.is_some() {
4465 return Err(syn::Error::new_spanned(
4466 field,
4467 "`ForeignKey<T>` already declares the FK target via the type parameter — \
4468 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4469 ));
4470 }
4471 let on = attrs.on.as_deref().unwrap_or("id");
4472 return Ok(quote! {
4473 ::core::option::Option::Some(::rustango::core::Relation::Fk {
4474 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4475 on: #on,
4476 })
4477 });
4478 }
4479 match (&attrs.fk, &attrs.o2o) {
4480 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4481 field,
4482 "`fk` and `o2o` are mutually exclusive",
4483 )),
4484 (Some(to), None) => {
4485 let on = attrs.on.as_deref().unwrap_or("id");
4486 let resolved = if to == "self" { table } else { to };
4492 Ok(quote! {
4493 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4494 })
4495 }
4496 (None, Some(to)) => {
4497 let on = attrs.on.as_deref().unwrap_or("id");
4498 let resolved = if to == "self" { table } else { to };
4499 Ok(quote! {
4500 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4501 })
4502 }
4503 (None, None) => {
4504 if attrs.on.is_some() {
4505 return Err(syn::Error::new_spanned(
4506 field,
4507 "`on` requires `fk` or `o2o`",
4508 ));
4509 }
4510 Ok(quote!(::core::option::Option::None))
4511 }
4512 }
4513}
4514
4515#[derive(Clone, Copy, PartialEq, Eq)]
4519enum DetectedKind {
4520 I16,
4521 I32,
4522 I64,
4523 F32,
4524 F64,
4525 Bool,
4526 String,
4527 DateTime,
4528 Date,
4529 Uuid,
4530 Json,
4531}
4532
4533impl DetectedKind {
4534 fn variant_tokens(self) -> TokenStream2 {
4535 match self {
4536 Self::I16 => quote!(::rustango::core::FieldType::I16),
4537 Self::I32 => quote!(::rustango::core::FieldType::I32),
4538 Self::I64 => quote!(::rustango::core::FieldType::I64),
4539 Self::F32 => quote!(::rustango::core::FieldType::F32),
4540 Self::F64 => quote!(::rustango::core::FieldType::F64),
4541 Self::Bool => quote!(::rustango::core::FieldType::Bool),
4542 Self::String => quote!(::rustango::core::FieldType::String),
4543 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4544 Self::Date => quote!(::rustango::core::FieldType::Date),
4545 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4546 Self::Json => quote!(::rustango::core::FieldType::Json),
4547 }
4548 }
4549
4550 fn is_integer(self) -> bool {
4551 matches!(self, Self::I16 | Self::I32 | Self::I64)
4552 }
4553
4554 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4562 match self {
4563 Self::I16 => (quote!(I16), quote!(0i16)),
4564 Self::I32 => (quote!(I32), quote!(0i32)),
4565 Self::I64 => (quote!(I64), quote!(0i64)),
4566 Self::F32 => (quote!(F32), quote!(0f32)),
4567 Self::F64 => (quote!(F64), quote!(0f64)),
4568 Self::Bool => (quote!(Bool), quote!(false)),
4569 Self::String => (quote!(String), quote!(::std::string::String::new())),
4570 Self::DateTime => (
4571 quote!(DateTime),
4572 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4573 ),
4574 Self::Date => (
4575 quote!(Date),
4576 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4577 ),
4578 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4579 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4580 }
4581 }
4582}
4583
4584#[derive(Clone, Copy)]
4590struct DetectedType<'a> {
4591 kind: DetectedKind,
4592 nullable: bool,
4593 auto: bool,
4594 fk_inner: Option<&'a syn::Type>,
4595}
4596
4597fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4602 let Type::Path(TypePath { path, qself: None }) = ty else {
4603 return None;
4604 };
4605 let last = path.segments.last()?;
4606 if last.ident != "Auto" {
4607 return None;
4608 }
4609 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4610 return None;
4611 };
4612 args.args.iter().find_map(|a| match a {
4613 syn::GenericArgument::Type(t) => Some(t),
4614 _ => None,
4615 })
4616}
4617
4618fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4619 let Type::Path(TypePath { path, qself: None }) = ty else {
4620 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4621 };
4622 let last = path
4623 .segments
4624 .last()
4625 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4626
4627 if last.ident == "Option" {
4628 let inner = generic_inner(ty, &last.arguments, "Option")?;
4629 let inner_det = detect_type(inner)?;
4630 if inner_det.nullable {
4631 return Err(syn::Error::new_spanned(
4632 ty,
4633 "nested Option is not supported",
4634 ));
4635 }
4636 if inner_det.auto {
4637 return Err(syn::Error::new_spanned(
4638 ty,
4639 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4640 ));
4641 }
4642 return Ok(DetectedType {
4643 nullable: true,
4644 ..inner_det
4645 });
4646 }
4647
4648 if last.ident == "Auto" {
4649 let inner = generic_inner(ty, &last.arguments, "Auto")?;
4650 let inner_det = detect_type(inner)?;
4651 if inner_det.auto {
4652 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
4653 }
4654 if inner_det.nullable {
4655 return Err(syn::Error::new_spanned(
4656 ty,
4657 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4658 ));
4659 }
4660 if inner_det.fk_inner.is_some() {
4661 return Err(syn::Error::new_spanned(
4662 ty,
4663 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4664 ));
4665 }
4666 if !matches!(
4667 inner_det.kind,
4668 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4669 ) {
4670 return Err(syn::Error::new_spanned(
4671 ty,
4672 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4673 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4674 (DEFAULT now())",
4675 ));
4676 }
4677 return Ok(DetectedType {
4678 auto: true,
4679 ..inner_det
4680 });
4681 }
4682
4683 if last.ident == "ForeignKey" {
4684 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4685 let kind = match key_ty {
4693 Some(k) => detect_type(k)?.kind,
4694 None => DetectedKind::I64,
4695 };
4696 return Ok(DetectedType {
4697 kind,
4698 nullable: false,
4699 auto: false,
4700 fk_inner: Some(inner),
4701 });
4702 }
4703
4704 let kind = match last.ident.to_string().as_str() {
4705 "i16" => DetectedKind::I16,
4706 "i32" => DetectedKind::I32,
4707 "i64" => DetectedKind::I64,
4708 "f32" => DetectedKind::F32,
4709 "f64" => DetectedKind::F64,
4710 "bool" => DetectedKind::Bool,
4711 "String" => DetectedKind::String,
4712 "DateTime" => DetectedKind::DateTime,
4713 "NaiveDate" => DetectedKind::Date,
4714 "Uuid" => DetectedKind::Uuid,
4715 "Value" => DetectedKind::Json,
4716 other => {
4717 return Err(syn::Error::new_spanned(
4718 ty,
4719 format!("unsupported field type `{other}`; v0.1 supports i32/i64/f32/f64/bool/String/DateTime/NaiveDate/Uuid/serde_json::Value, optionally wrapped in Option or Auto (Auto only on integers)"),
4720 ));
4721 }
4722 };
4723 Ok(DetectedType {
4724 kind,
4725 nullable: false,
4726 auto: false,
4727 fk_inner: None,
4728 })
4729}
4730
4731fn generic_inner<'a>(
4732 ty: &'a Type,
4733 arguments: &'a PathArguments,
4734 wrapper: &str,
4735) -> syn::Result<&'a Type> {
4736 let PathArguments::AngleBracketed(args) = arguments else {
4737 return Err(syn::Error::new_spanned(
4738 ty,
4739 format!("{wrapper} requires a generic argument"),
4740 ));
4741 };
4742 args.args
4743 .iter()
4744 .find_map(|a| match a {
4745 GenericArgument::Type(t) => Some(t),
4746 _ => None,
4747 })
4748 .ok_or_else(|| {
4749 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4750 })
4751}
4752
4753fn generic_pair<'a>(
4757 ty: &'a Type,
4758 arguments: &'a PathArguments,
4759 wrapper: &str,
4760) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4761 let PathArguments::AngleBracketed(args) = arguments else {
4762 return Err(syn::Error::new_spanned(
4763 ty,
4764 format!("{wrapper} requires a generic argument"),
4765 ));
4766 };
4767 let mut types = args.args.iter().filter_map(|a| match a {
4768 GenericArgument::Type(t) => Some(t),
4769 _ => None,
4770 });
4771 let first = types.next().ok_or_else(|| {
4772 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4773 })?;
4774 let second = types.next();
4775 Ok((first, second))
4776}
4777
4778fn to_snake_case(s: &str) -> String {
4779 let mut out = String::with_capacity(s.len() + 4);
4780 for (i, ch) in s.chars().enumerate() {
4781 if ch.is_ascii_uppercase() {
4782 if i > 0 {
4783 out.push('_');
4784 }
4785 out.push(ch.to_ascii_lowercase());
4786 } else {
4787 out.push(ch);
4788 }
4789 }
4790 out
4791}
4792
4793#[derive(Default)]
4799struct FormFieldAttrs {
4800 min: Option<i64>,
4801 max: Option<i64>,
4802 min_length: Option<u32>,
4803 max_length: Option<u32>,
4804}
4805
4806#[derive(Clone, Copy)]
4808enum FormFieldKind {
4809 String,
4810 I16,
4811 I32,
4812 I64,
4813 F32,
4814 F64,
4815 Bool,
4816}
4817
4818impl FormFieldKind {
4819 fn parse_method(self) -> &'static str {
4820 match self {
4821 Self::I16 => "i16",
4822 Self::I32 => "i32",
4823 Self::I64 => "i64",
4824 Self::F32 => "f32",
4825 Self::F64 => "f64",
4826 Self::String | Self::Bool => "",
4829 }
4830 }
4831}
4832
4833fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4834 let struct_name = &input.ident;
4835
4836 let Data::Struct(data) = &input.data else {
4837 return Err(syn::Error::new_spanned(
4838 struct_name,
4839 "Form can only be derived on structs",
4840 ));
4841 };
4842 let Fields::Named(named) = &data.fields else {
4843 return Err(syn::Error::new_spanned(
4844 struct_name,
4845 "Form requires a struct with named fields",
4846 ));
4847 };
4848
4849 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4850 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4851
4852 for field in &named.named {
4853 let ident = field
4854 .ident
4855 .as_ref()
4856 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4857 let attrs = parse_form_field_attrs(field)?;
4858 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4859
4860 let name_lit = ident.to_string();
4861 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4862 field_blocks.push(parse_block);
4863 field_idents.push(ident);
4864 }
4865
4866 Ok(quote! {
4867 impl ::rustango::forms::Form for #struct_name {
4868 fn parse(
4869 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4870 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4871 let mut __errors = ::rustango::forms::FormErrors::default();
4872 #( #field_blocks )*
4873 if !__errors.is_empty() {
4874 return ::core::result::Result::Err(__errors);
4875 }
4876 ::core::result::Result::Ok(Self {
4877 #( #field_idents ),*
4878 })
4879 }
4880 }
4881 })
4882}
4883
4884fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4885 let mut out = FormFieldAttrs::default();
4886 for attr in &field.attrs {
4887 if !attr.path().is_ident("form") {
4888 continue;
4889 }
4890 attr.parse_nested_meta(|meta| {
4891 if meta.path.is_ident("min") {
4892 let lit: syn::LitInt = meta.value()?.parse()?;
4893 out.min = Some(lit.base10_parse::<i64>()?);
4894 return Ok(());
4895 }
4896 if meta.path.is_ident("max") {
4897 let lit: syn::LitInt = meta.value()?.parse()?;
4898 out.max = Some(lit.base10_parse::<i64>()?);
4899 return Ok(());
4900 }
4901 if meta.path.is_ident("min_length") {
4902 let lit: syn::LitInt = meta.value()?.parse()?;
4903 out.min_length = Some(lit.base10_parse::<u32>()?);
4904 return Ok(());
4905 }
4906 if meta.path.is_ident("max_length") {
4907 let lit: syn::LitInt = meta.value()?.parse()?;
4908 out.max_length = Some(lit.base10_parse::<u32>()?);
4909 return Ok(());
4910 }
4911 Err(meta.error(
4912 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4913 ))
4914 })?;
4915 }
4916 Ok(out)
4917}
4918
4919fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4920 let Type::Path(TypePath { path, qself: None }) = ty else {
4921 return Err(syn::Error::new(
4922 span,
4923 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4924 ));
4925 };
4926 let last = path
4927 .segments
4928 .last()
4929 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4930
4931 if last.ident == "Option" {
4932 let inner = generic_inner(ty, &last.arguments, "Option")?;
4933 let (kind, nested) = detect_form_field(inner, span)?;
4934 if nested {
4935 return Err(syn::Error::new(
4936 span,
4937 "nested Option in Form fields is not supported",
4938 ));
4939 }
4940 return Ok((kind, true));
4941 }
4942
4943 let kind = match last.ident.to_string().as_str() {
4944 "String" => FormFieldKind::String,
4945 "i16" => FormFieldKind::I16,
4946 "i32" => FormFieldKind::I32,
4947 "i64" => FormFieldKind::I64,
4948 "f32" => FormFieldKind::F32,
4949 "f64" => FormFieldKind::F64,
4950 "bool" => FormFieldKind::Bool,
4951 other => {
4952 return Err(syn::Error::new(
4953 span,
4954 format!(
4955 "Form field type `{other}` is not supported in v0.8 — use String / \
4956 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4957 ),
4958 ));
4959 }
4960 };
4961 Ok((kind, false))
4962}
4963
4964#[allow(clippy::too_many_lines)]
4965fn render_form_field_parse(
4966 ident: &syn::Ident,
4967 name_lit: &str,
4968 kind: FormFieldKind,
4969 nullable: bool,
4970 attrs: &FormFieldAttrs,
4971) -> TokenStream2 {
4972 let lookup = quote! {
4975 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4976 };
4977
4978 let parsed_value = match kind {
4979 FormFieldKind::Bool => quote! {
4980 let __v: bool = match __raw {
4981 ::core::option::Option::None => false,
4982 ::core::option::Option::Some(__s) => !matches!(
4983 __s.to_ascii_lowercase().as_str(),
4984 "" | "false" | "0" | "off" | "no"
4985 ),
4986 };
4987 },
4988 FormFieldKind::String => {
4989 if nullable {
4990 quote! {
4991 let __v: ::core::option::Option<::std::string::String> = match __raw {
4992 ::core::option::Option::None => ::core::option::Option::None,
4993 ::core::option::Option::Some(__s) if __s.is_empty() => {
4994 ::core::option::Option::None
4995 }
4996 ::core::option::Option::Some(__s) => {
4997 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4998 }
4999 };
5000 }
5001 } else {
5002 quote! {
5003 let __v: ::std::string::String = match __raw {
5004 ::core::option::Option::Some(__s) if !__s.is_empty() => {
5005 ::core::clone::Clone::clone(__s)
5006 }
5007 _ => {
5008 __errors.add(#name_lit, "This field is required.");
5009 ::std::string::String::new()
5010 }
5011 };
5012 }
5013 }
5014 }
5015 FormFieldKind::I16
5016 | FormFieldKind::I32
5017 | FormFieldKind::I64
5018 | FormFieldKind::F32
5019 | FormFieldKind::F64 => {
5020 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
5021 let ty_lit = kind.parse_method();
5022 let default_val = match kind {
5023 FormFieldKind::I16 => quote! { 0i16 },
5024 FormFieldKind::I32 => quote! { 0i32 },
5025 FormFieldKind::I64 => quote! { 0i64 },
5026 FormFieldKind::F32 => quote! { 0f32 },
5027 FormFieldKind::F64 => quote! { 0f64 },
5028 _ => quote! { Default::default() },
5029 };
5030 if nullable {
5031 quote! {
5032 let __v: ::core::option::Option<#parse_ty> = match __raw {
5033 ::core::option::Option::None => ::core::option::Option::None,
5034 ::core::option::Option::Some(__s) if __s.is_empty() => {
5035 ::core::option::Option::None
5036 }
5037 ::core::option::Option::Some(__s) => {
5038 match __s.parse::<#parse_ty>() {
5039 ::core::result::Result::Ok(__n) => {
5040 ::core::option::Option::Some(__n)
5041 }
5042 ::core::result::Result::Err(__e) => {
5043 __errors.add(
5044 #name_lit,
5045 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5046 );
5047 ::core::option::Option::None
5048 }
5049 }
5050 }
5051 };
5052 }
5053 } else {
5054 quote! {
5055 let __v: #parse_ty = match __raw {
5056 ::core::option::Option::Some(__s) if !__s.is_empty() => {
5057 match __s.parse::<#parse_ty>() {
5058 ::core::result::Result::Ok(__n) => __n,
5059 ::core::result::Result::Err(__e) => {
5060 __errors.add(
5061 #name_lit,
5062 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5063 );
5064 #default_val
5065 }
5066 }
5067 }
5068 _ => {
5069 __errors.add(#name_lit, "This field is required.");
5070 #default_val
5071 }
5072 };
5073 }
5074 }
5075 }
5076 };
5077
5078 let validators = render_form_validators(name_lit, kind, nullable, attrs);
5079
5080 quote! {
5081 let #ident = {
5082 #lookup
5083 #parsed_value
5084 #validators
5085 __v
5086 };
5087 }
5088}
5089
5090fn render_form_validators(
5091 name_lit: &str,
5092 kind: FormFieldKind,
5093 nullable: bool,
5094 attrs: &FormFieldAttrs,
5095) -> TokenStream2 {
5096 let mut checks: Vec<TokenStream2> = Vec::new();
5097
5098 let val_ref = if nullable {
5099 quote! { __v.as_ref() }
5100 } else {
5101 quote! { ::core::option::Option::Some(&__v) }
5102 };
5103
5104 let is_string = matches!(kind, FormFieldKind::String);
5105 let is_numeric = matches!(
5106 kind,
5107 FormFieldKind::I16
5108 | FormFieldKind::I32
5109 | FormFieldKind::I64
5110 | FormFieldKind::F32
5111 | FormFieldKind::F64
5112 );
5113
5114 if is_string {
5115 if let Some(min_len) = attrs.min_length {
5116 let min_len_usize = min_len as usize;
5117 checks.push(quote! {
5118 if let ::core::option::Option::Some(__s) = #val_ref {
5119 if __s.len() < #min_len_usize {
5120 __errors.add(
5121 #name_lit,
5122 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
5123 );
5124 }
5125 }
5126 });
5127 }
5128 if let Some(max_len) = attrs.max_length {
5129 let max_len_usize = max_len as usize;
5130 checks.push(quote! {
5131 if let ::core::option::Option::Some(__s) = #val_ref {
5132 if __s.len() > #max_len_usize {
5133 __errors.add(
5134 #name_lit,
5135 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
5136 );
5137 }
5138 }
5139 });
5140 }
5141 }
5142
5143 if is_numeric {
5144 if let Some(min) = attrs.min {
5145 checks.push(quote! {
5146 if let ::core::option::Option::Some(__n) = #val_ref {
5147 if (*__n as f64) < (#min as f64) {
5148 __errors.add(
5149 #name_lit,
5150 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
5151 );
5152 }
5153 }
5154 });
5155 }
5156 if let Some(max) = attrs.max {
5157 checks.push(quote! {
5158 if let ::core::option::Option::Some(__n) = #val_ref {
5159 if (*__n as f64) > (#max as f64) {
5160 __errors.add(
5161 #name_lit,
5162 ::std::format!("Ensure this value is less than or equal to {}.", #max),
5163 );
5164 }
5165 }
5166 });
5167 }
5168 }
5169
5170 quote! { #( #checks )* }
5171}
5172
5173struct ViewSetAttrs {
5178 model: syn::Path,
5179 fields: Option<Vec<String>>,
5180 filter_fields: Vec<String>,
5181 search_fields: Vec<String>,
5182 ordering: Vec<(String, bool)>,
5184 page_size: Option<usize>,
5185 read_only: bool,
5186 perms: ViewSetPermsAttrs,
5187}
5188
5189#[derive(Default)]
5190struct ViewSetPermsAttrs {
5191 list: Vec<String>,
5192 retrieve: Vec<String>,
5193 create: Vec<String>,
5194 update: Vec<String>,
5195 destroy: Vec<String>,
5196}
5197
5198fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
5199 let struct_name = &input.ident;
5200
5201 match &input.data {
5203 Data::Struct(s) => match &s.fields {
5204 Fields::Unit | Fields::Named(_) => {}
5205 Fields::Unnamed(_) => {
5206 return Err(syn::Error::new_spanned(
5207 struct_name,
5208 "ViewSet can only be derived on a unit struct or an empty named struct",
5209 ));
5210 }
5211 },
5212 _ => {
5213 return Err(syn::Error::new_spanned(
5214 struct_name,
5215 "ViewSet can only be derived on a struct",
5216 ));
5217 }
5218 }
5219
5220 let attrs = parse_viewset_attrs(input)?;
5221 let model_path = &attrs.model;
5222
5223 let fields_call = if let Some(ref fields) = attrs.fields {
5225 let lits = fields.iter().map(|f| f.as_str());
5226 quote!(.fields(&[ #(#lits),* ]))
5227 } else {
5228 quote!()
5229 };
5230
5231 let filter_fields_call = if attrs.filter_fields.is_empty() {
5232 quote!()
5233 } else {
5234 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
5235 quote!(.filter_fields(&[ #(#lits),* ]))
5236 };
5237
5238 let search_fields_call = if attrs.search_fields.is_empty() {
5239 quote!()
5240 } else {
5241 let lits = attrs.search_fields.iter().map(|f| f.as_str());
5242 quote!(.search_fields(&[ #(#lits),* ]))
5243 };
5244
5245 let ordering_call = if attrs.ordering.is_empty() {
5246 quote!()
5247 } else {
5248 let pairs = attrs.ordering.iter().map(|(f, desc)| {
5249 let f = f.as_str();
5250 quote!((#f, #desc))
5251 });
5252 quote!(.ordering(&[ #(#pairs),* ]))
5253 };
5254
5255 let page_size_call = if let Some(n) = attrs.page_size {
5256 quote!(.page_size(#n))
5257 } else {
5258 quote!()
5259 };
5260
5261 let read_only_call = if attrs.read_only {
5262 quote!(.read_only())
5263 } else {
5264 quote!()
5265 };
5266
5267 let perms = &attrs.perms;
5268 let perms_call = if perms.list.is_empty()
5269 && perms.retrieve.is_empty()
5270 && perms.create.is_empty()
5271 && perms.update.is_empty()
5272 && perms.destroy.is_empty()
5273 {
5274 quote!()
5275 } else {
5276 let list_lits = perms.list.iter().map(|s| s.as_str());
5277 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
5278 let create_lits = perms.create.iter().map(|s| s.as_str());
5279 let update_lits = perms.update.iter().map(|s| s.as_str());
5280 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
5281 quote! {
5282 .permissions(::rustango::viewset::ViewSetPerms {
5283 list: ::std::vec![ #(#list_lits.to_owned()),* ],
5284 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
5285 create: ::std::vec![ #(#create_lits.to_owned()),* ],
5286 update: ::std::vec![ #(#update_lits.to_owned()),* ],
5287 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
5288 })
5289 }
5290 };
5291
5292 Ok(quote! {
5293 impl #struct_name {
5294 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
5297 ::rustango::viewset::ViewSet::for_model(
5298 <#model_path as ::rustango::core::Model>::SCHEMA
5299 )
5300 #fields_call
5301 #filter_fields_call
5302 #search_fields_call
5303 #ordering_call
5304 #page_size_call
5305 #perms_call
5306 #read_only_call
5307 .router(prefix, pool)
5308 }
5309 }
5310 })
5311}
5312
5313fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
5314 let mut model: Option<syn::Path> = None;
5315 let mut fields: Option<Vec<String>> = None;
5316 let mut filter_fields: Vec<String> = Vec::new();
5317 let mut search_fields: Vec<String> = Vec::new();
5318 let mut ordering: Vec<(String, bool)> = Vec::new();
5319 let mut page_size: Option<usize> = None;
5320 let mut read_only = false;
5321 let mut perms = ViewSetPermsAttrs::default();
5322
5323 for attr in &input.attrs {
5324 if !attr.path().is_ident("viewset") {
5325 continue;
5326 }
5327 attr.parse_nested_meta(|meta| {
5328 if meta.path.is_ident("model") {
5329 let path: syn::Path = meta.value()?.parse()?;
5330 model = Some(path);
5331 return Ok(());
5332 }
5333 if meta.path.is_ident("fields") {
5334 let s: LitStr = meta.value()?.parse()?;
5335 fields = Some(split_field_list(&s.value()));
5336 return Ok(());
5337 }
5338 if meta.path.is_ident("filter_fields") {
5339 let s: LitStr = meta.value()?.parse()?;
5340 filter_fields = split_field_list(&s.value());
5341 return Ok(());
5342 }
5343 if meta.path.is_ident("search_fields") {
5344 let s: LitStr = meta.value()?.parse()?;
5345 search_fields = split_field_list(&s.value());
5346 return Ok(());
5347 }
5348 if meta.path.is_ident("ordering") {
5349 let s: LitStr = meta.value()?.parse()?;
5350 ordering = parse_ordering_list(&s.value());
5351 return Ok(());
5352 }
5353 if meta.path.is_ident("page_size") {
5354 let lit: syn::LitInt = meta.value()?.parse()?;
5355 page_size = Some(lit.base10_parse::<usize>()?);
5356 return Ok(());
5357 }
5358 if meta.path.is_ident("read_only") {
5359 read_only = true;
5360 return Ok(());
5361 }
5362 if meta.path.is_ident("permissions") {
5363 meta.parse_nested_meta(|inner| {
5364 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5365 let s: LitStr = inner.value()?.parse()?;
5366 Ok(split_field_list(&s.value()))
5367 };
5368 if inner.path.is_ident("list") {
5369 perms.list = parse_codenames(&inner)?;
5370 } else if inner.path.is_ident("retrieve") {
5371 perms.retrieve = parse_codenames(&inner)?;
5372 } else if inner.path.is_ident("create") {
5373 perms.create = parse_codenames(&inner)?;
5374 } else if inner.path.is_ident("update") {
5375 perms.update = parse_codenames(&inner)?;
5376 } else if inner.path.is_ident("destroy") {
5377 perms.destroy = parse_codenames(&inner)?;
5378 } else {
5379 return Err(inner.error(
5380 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5381 ));
5382 }
5383 Ok(())
5384 })?;
5385 return Ok(());
5386 }
5387 Err(meta.error(
5388 "unknown viewset attribute (supported: model, fields, filter_fields, \
5389 search_fields, ordering, page_size, read_only, permissions(...))",
5390 ))
5391 })?;
5392 }
5393
5394 let model = model.ok_or_else(|| {
5395 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
5396 })?;
5397
5398 Ok(ViewSetAttrs {
5399 model,
5400 fields,
5401 filter_fields,
5402 search_fields,
5403 ordering,
5404 page_size,
5405 read_only,
5406 perms,
5407 })
5408}
5409
5410struct SerializerContainerAttrs {
5413 model: syn::Path,
5414}
5415
5416#[derive(Default)]
5417struct SerializerFieldAttrs {
5418 read_only: bool,
5419 write_only: bool,
5420 source: Option<String>,
5421 skip: bool,
5422 method: Option<String>,
5426 validate: Option<String>,
5431 nested: bool,
5441 nested_strict: bool,
5446 many: Option<syn::Type>,
5455}
5456
5457fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5458 let mut model: Option<syn::Path> = None;
5459 for attr in &input.attrs {
5460 if !attr.path().is_ident("serializer") {
5461 continue;
5462 }
5463 attr.parse_nested_meta(|meta| {
5464 if meta.path.is_ident("model") {
5465 let _eq: syn::Token![=] = meta.input.parse()?;
5466 model = Some(meta.input.parse()?);
5467 return Ok(());
5468 }
5469 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5470 })?;
5471 }
5472 let model = model.ok_or_else(|| {
5473 syn::Error::new_spanned(
5474 &input.ident,
5475 "`#[serializer(model = SomeModel)]` is required",
5476 )
5477 })?;
5478 Ok(SerializerContainerAttrs { model })
5479}
5480
5481fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5482 let mut out = SerializerFieldAttrs::default();
5483 for attr in &field.attrs {
5484 if !attr.path().is_ident("serializer") {
5485 continue;
5486 }
5487 attr.parse_nested_meta(|meta| {
5488 if meta.path.is_ident("read_only") {
5489 out.read_only = true;
5490 return Ok(());
5491 }
5492 if meta.path.is_ident("write_only") {
5493 out.write_only = true;
5494 return Ok(());
5495 }
5496 if meta.path.is_ident("skip") {
5497 out.skip = true;
5498 return Ok(());
5499 }
5500 if meta.path.is_ident("source") {
5501 let s: LitStr = meta.value()?.parse()?;
5502 out.source = Some(s.value());
5503 return Ok(());
5504 }
5505 if meta.path.is_ident("method") {
5506 let s: LitStr = meta.value()?.parse()?;
5507 out.method = Some(s.value());
5508 return Ok(());
5509 }
5510 if meta.path.is_ident("validate") {
5511 let s: LitStr = meta.value()?.parse()?;
5512 out.validate = Some(s.value());
5513 return Ok(());
5514 }
5515 if meta.path.is_ident("many") {
5516 let _eq: syn::Token![=] = meta.input.parse()?;
5517 out.many = Some(meta.input.parse()?);
5518 return Ok(());
5519 }
5520 if meta.path.is_ident("nested") {
5521 out.nested = true;
5522 if meta.input.peek(syn::token::Paren) {
5525 meta.parse_nested_meta(|inner| {
5526 if inner.path.is_ident("strict") {
5527 out.nested_strict = true;
5528 return Ok(());
5529 }
5530 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5531 })?;
5532 }
5533 return Ok(());
5534 }
5535 Err(meta.error(
5536 "unknown serializer field attribute (supported: \
5537 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5538 ))
5539 })?;
5540 }
5541 if out.read_only && out.write_only {
5543 return Err(syn::Error::new_spanned(
5544 field,
5545 "a field cannot be both `read_only` and `write_only`",
5546 ));
5547 }
5548 if out.method.is_some() && out.source.is_some() {
5549 return Err(syn::Error::new_spanned(
5550 field,
5551 "`method` and `source` are mutually exclusive — `method` computes \
5552 the value from a method, `source` reads it from a different model field",
5553 ));
5554 }
5555 Ok(out)
5556}
5557
5558fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5559 let struct_name = &input.ident;
5560 let struct_name_lit = struct_name.to_string();
5561
5562 let Data::Struct(data) = &input.data else {
5563 return Err(syn::Error::new_spanned(
5564 struct_name,
5565 "Serializer can only be derived on structs",
5566 ));
5567 };
5568 let Fields::Named(named) = &data.fields else {
5569 return Err(syn::Error::new_spanned(
5570 struct_name,
5571 "Serializer requires a struct with named fields",
5572 ));
5573 };
5574
5575 let container = parse_serializer_container_attrs(input)?;
5576 let model_path = &container.model;
5577
5578 #[allow(dead_code)]
5582 struct FieldInfo {
5583 ident: syn::Ident,
5584 ty: syn::Type,
5585 attrs: SerializerFieldAttrs,
5586 }
5587 let mut fields_info: Vec<FieldInfo> = Vec::new();
5588 for field in &named.named {
5589 let ident = field.ident.clone().expect("named field has ident");
5590 let attrs = parse_serializer_field_attrs(field)?;
5591 fields_info.push(FieldInfo {
5592 ident,
5593 ty: field.ty.clone(),
5594 attrs,
5595 });
5596 }
5597
5598 let from_model_fields = fields_info.iter().map(|fi| {
5600 let ident = &fi.ident;
5601 let ty = &fi.ty;
5602 if let Some(_inner) = &fi.attrs.many {
5603 quote! { #ident: ::std::vec::Vec::new() }
5607 } else if let Some(method) = &fi.attrs.method {
5608 let method_ident = syn::Ident::new(method, ident.span());
5612 quote! { #ident: Self::#method_ident(model) }
5613 } else if fi.attrs.nested {
5614 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5630 let src_ident = syn::Ident::new(&src_name, ident.span());
5631 if fi.attrs.nested_strict {
5632 let panic_msg = format!(
5633 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5634 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5635 );
5636 quote! {
5637 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5638 model.#src_ident.value().expect(#panic_msg),
5639 )
5640 }
5641 } else {
5642 quote! {
5643 #ident: match model.#src_ident.value() {
5644 ::core::option::Option::Some(__loaded) =>
5645 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5646 ::core::option::Option::None =>
5647 ::core::default::Default::default(),
5648 }
5649 }
5650 }
5651 } else if fi.attrs.write_only || fi.attrs.skip {
5652 quote! { #ident: ::core::default::Default::default() }
5654 } else if let Some(src) = &fi.attrs.source {
5655 let src_ident = syn::Ident::new(src, ident.span());
5656 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5657 } else {
5658 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5659 }
5660 });
5661
5662 let validator_calls: Vec<_> = fields_info
5666 .iter()
5667 .filter_map(|fi| {
5668 let ident = &fi.ident;
5669 let name_lit = ident.to_string();
5670 let method = fi.attrs.validate.as_ref()?;
5671 let method_ident = syn::Ident::new(method, ident.span());
5672 Some(quote! {
5673 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5674 __errors.add(#name_lit.to_owned(), __e);
5675 }
5676 })
5677 })
5678 .collect();
5679 let validate_method = if validator_calls.is_empty() {
5680 quote! {}
5681 } else {
5682 quote! {
5683 impl #struct_name {
5684 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5688 let mut __errors = ::rustango::forms::FormErrors::default();
5689 #( #validator_calls )*
5690 if __errors.is_empty() {
5691 ::core::result::Result::Ok(())
5692 } else {
5693 ::core::result::Result::Err(__errors)
5694 }
5695 }
5696 }
5697 }
5698 };
5699
5700 let many_setters: Vec<_> = fields_info
5704 .iter()
5705 .filter_map(|fi| {
5706 let many_ty = fi.attrs.many.as_ref()?;
5707 let ident = &fi.ident;
5708 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5709 Some(quote! {
5710 pub fn #setter(
5715 &mut self,
5716 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5717 ) -> &mut Self {
5718 self.#ident = models.iter()
5719 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5720 .collect();
5721 self
5722 }
5723 })
5724 })
5725 .collect();
5726 let many_setters_impl = if many_setters.is_empty() {
5727 quote! {}
5728 } else {
5729 quote! {
5730 impl #struct_name {
5731 #( #many_setters )*
5732 }
5733 }
5734 };
5735
5736 let output_fields: Vec<_> = fields_info
5738 .iter()
5739 .filter(|fi| !fi.attrs.write_only)
5740 .collect();
5741 let output_field_count = output_fields.len();
5742 let serialize_fields = output_fields.iter().map(|fi| {
5743 let ident = &fi.ident;
5744 let name_lit = ident.to_string();
5745 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5746 });
5747
5748 let writable_lits: Vec<_> = fields_info
5750 .iter()
5751 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5752 .map(|fi| fi.ident.to_string())
5753 .collect();
5754
5755 let openapi_impl = {
5759 #[cfg(feature = "openapi")]
5760 {
5761 let property_calls = output_fields.iter().map(|fi| {
5762 let ident = &fi.ident;
5763 let name_lit = ident.to_string();
5764 let ty = &fi.ty;
5765 let nullable_call = if is_option(ty) {
5766 quote! { .nullable() }
5767 } else {
5768 quote! {}
5769 };
5770 quote! {
5771 .property(
5772 #name_lit,
5773 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5774 #nullable_call,
5775 )
5776 }
5777 });
5778 let required_lits: Vec<_> = output_fields
5779 .iter()
5780 .filter(|fi| !is_option(&fi.ty))
5781 .map(|fi| fi.ident.to_string())
5782 .collect();
5783 quote! {
5784 impl ::rustango::openapi::OpenApiSchema for #struct_name {
5785 fn openapi_schema() -> ::rustango::openapi::Schema {
5786 ::rustango::openapi::Schema::object()
5787 #( #property_calls )*
5788 .required([ #( #required_lits ),* ])
5789 }
5790 }
5791 }
5792 }
5793 #[cfg(not(feature = "openapi"))]
5794 {
5795 quote! {}
5796 }
5797 };
5798
5799 Ok(quote! {
5800 impl ::rustango::serializer::ModelSerializer for #struct_name {
5801 type Model = #model_path;
5802
5803 fn from_model(model: &Self::Model) -> Self {
5804 Self {
5805 #( #from_model_fields ),*
5806 }
5807 }
5808
5809 fn writable_fields() -> &'static [&'static str] {
5810 &[ #( #writable_lits ),* ]
5811 }
5812 }
5813
5814 impl ::serde::Serialize for #struct_name {
5815 fn serialize<S>(&self, serializer: S)
5816 -> ::core::result::Result<S::Ok, S::Error>
5817 where
5818 S: ::serde::Serializer,
5819 {
5820 use ::serde::ser::SerializeStruct;
5821 let mut __state = serializer.serialize_struct(
5822 #struct_name_lit,
5823 #output_field_count,
5824 )?;
5825 #( #serialize_fields )*
5826 __state.end()
5827 }
5828 }
5829
5830 #openapi_impl
5831
5832 #validate_method
5833
5834 #many_setters_impl
5835 })
5836}
5837
5838#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5842fn is_option(ty: &syn::Type) -> bool {
5843 if let syn::Type::Path(p) = ty {
5844 if let Some(last) = p.path.segments.last() {
5845 return last.ident == "Option";
5846 }
5847 }
5848 false
5849}