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))]
114pub fn derive_serializer(input: TokenStream) -> TokenStream {
115 let input = parse_macro_input!(input as DeriveInput);
116 expand_serializer(&input)
117 .unwrap_or_else(syn::Error::into_compile_error)
118 .into()
119}
120
121#[proc_macro]
156pub fn embed_migrations(input: TokenStream) -> TokenStream {
157 expand_embed_migrations(input.into())
158 .unwrap_or_else(syn::Error::into_compile_error)
159 .into()
160}
161
162#[proc_macro_attribute]
185pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
186 expand_main(args.into(), item.into())
187 .unwrap_or_else(syn::Error::into_compile_error)
188 .into()
189}
190
191fn expand_main(
192 args: TokenStream2,
193 item: TokenStream2,
194) -> syn::Result<TokenStream2> {
195 let mut input: syn::ItemFn = syn::parse2(item)?;
196 if input.sig.asyncness.is_none() {
197 return Err(syn::Error::new(
198 input.sig.ident.span(),
199 "`#[rustango::main]` must wrap an `async fn`",
200 ));
201 }
202
203 let tokio_attr = if args.is_empty() {
206 quote! { #[::tokio::main] }
207 } else {
208 quote! { #[::tokio::main(#args)] }
209 };
210
211 let body = input.block.clone();
213 input.block = syn::parse2(quote! {{
214 {
215 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
216 let _ = tracing_subscriber::fmt()
219 .with_env_filter(
220 EnvFilter::try_from_default_env()
221 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
222 )
223 .try_init();
224 }
225 #body
226 }})?;
227
228 Ok(quote! {
229 #tokio_attr
230 #input
231 })
232}
233
234fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
235 let path_str = if input.is_empty() {
237 "./migrations".to_string()
238 } else {
239 let lit: LitStr = syn::parse2(input)?;
240 lit.value()
241 };
242
243 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
244 syn::Error::new(
245 proc_macro2::Span::call_site(),
246 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
247 )
248 })?;
249 let abs = std::path::Path::new(&manifest).join(&path_str);
250
251 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
252 if abs.is_dir() {
253 let read = std::fs::read_dir(&abs).map_err(|e| {
254 syn::Error::new(
255 proc_macro2::Span::call_site(),
256 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
257 )
258 })?;
259 for entry in read.flatten() {
260 let path = entry.path();
261 if !path.is_file() {
262 continue;
263 }
264 if path.extension().and_then(|s| s.to_str()) != Some("json") {
265 continue;
266 }
267 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
268 continue;
269 };
270 entries.push((stem.to_owned(), path));
271 }
272 }
273 entries.sort_by(|a, b| a.0.cmp(&b.0));
274
275 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
288 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
289 for (stem, path) in &entries {
290 let raw = std::fs::read_to_string(path).map_err(|e| {
291 syn::Error::new(
292 proc_macro2::Span::call_site(),
293 format!(
294 "embed_migrations!: cannot read {} for chain validation: {e}",
295 path.display()
296 ),
297 )
298 })?;
299 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
300 syn::Error::new(
301 proc_macro2::Span::call_site(),
302 format!(
303 "embed_migrations!: {} is not valid JSON: {e}",
304 path.display()
305 ),
306 )
307 })?;
308 let name = json
309 .get("name")
310 .and_then(|v| v.as_str())
311 .ok_or_else(|| {
312 syn::Error::new(
313 proc_macro2::Span::call_site(),
314 format!(
315 "embed_migrations!: {} is missing the `name` field",
316 path.display()
317 ),
318 )
319 })?
320 .to_owned();
321 if name != *stem {
322 return Err(syn::Error::new(
323 proc_macro2::Span::call_site(),
324 format!(
325 "embed_migrations!: file stem `{stem}` does not match the migration's \
326 `name` field `{name}` — rename the file or fix the JSON",
327 ),
328 ));
329 }
330 let prev = json
331 .get("prev")
332 .and_then(|v| v.as_str())
333 .map(str::to_owned);
334 chain_names.push(name.clone());
335 prev_refs.push((name, prev));
336 }
337
338 let name_set: std::collections::HashSet<&str> =
339 chain_names.iter().map(String::as_str).collect();
340 for (name, prev) in &prev_refs {
341 if let Some(p) = prev {
342 if !name_set.contains(p.as_str()) {
343 return Err(syn::Error::new(
344 proc_macro2::Span::call_site(),
345 format!(
346 "embed_migrations!: broken migration chain — `{name}` declares \
347 prev=`{p}` but no migration with that name exists in {}",
348 abs.display()
349 ),
350 ));
351 }
352 }
353 }
354
355 let pairs: Vec<TokenStream2> = entries
356 .iter()
357 .map(|(name, path)| {
358 let path_lit = path.display().to_string();
359 quote! { (#name, ::core::include_str!(#path_lit)) }
360 })
361 .collect();
362
363 Ok(quote! {
364 {
365 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
366 __RUSTANGO_EMBEDDED
367 }
368 })
369}
370
371fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
372 let struct_name = &input.ident;
373
374 let Data::Struct(data) = &input.data else {
375 return Err(syn::Error::new_spanned(
376 struct_name,
377 "Model can only be derived on structs",
378 ));
379 };
380 let Fields::Named(named) = &data.fields else {
381 return Err(syn::Error::new_spanned(
382 struct_name,
383 "Model requires a struct with named fields",
384 ));
385 };
386
387 let container = parse_container_attrs(input)?;
388 let table = container
389 .table
390 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
391 let model_name = struct_name.to_string();
392
393 let collected = collect_fields(named, &table)?;
394
395 if let Some((ref display, span)) = container.display {
397 if !collected.field_names.iter().any(|n| n == display) {
398 return Err(syn::Error::new(
399 span,
400 format!("`display = \"{display}\"` does not match any field on this struct"),
401 ));
402 }
403 }
404 let display = container.display.map(|(name, _)| name);
405 let app_label = container.app.clone();
406
407 if let Some(admin) = &container.admin {
409 for (label, list) in [
410 ("list_display", &admin.list_display),
411 ("search_fields", &admin.search_fields),
412 ("readonly_fields", &admin.readonly_fields),
413 ("list_filter", &admin.list_filter),
414 ] {
415 if let Some((names, span)) = list {
416 for name in names {
417 if !collected.field_names.iter().any(|n| n == name) {
418 return Err(syn::Error::new(
419 *span,
420 format!(
421 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
422 ),
423 ));
424 }
425 }
426 }
427 }
428 if let Some((pairs, span)) = &admin.ordering {
429 for (name, _) in pairs {
430 if !collected.field_names.iter().any(|n| n == name) {
431 return Err(syn::Error::new(
432 *span,
433 format!(
434 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
435 ),
436 ));
437 }
438 }
439 }
440 if let Some((groups, span)) = &admin.fieldsets {
441 for (_, fields) in groups {
442 for name in fields {
443 if !collected.field_names.iter().any(|n| n == name) {
444 return Err(syn::Error::new(
445 *span,
446 format!(
447 "`fieldsets`: \"{name}\" is not a declared field on this struct"
448 ),
449 ));
450 }
451 }
452 }
453 }
454 }
455 if let Some(audit) = &container.audit {
456 if let Some((names, span)) = &audit.track {
457 for name in names {
458 if !collected.field_names.iter().any(|n| n == name) {
459 return Err(syn::Error::new(
460 *span,
461 format!(
462 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
463 ),
464 ));
465 }
466 }
467 }
468 }
469
470 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
473 audit
474 .track
475 .as_ref()
476 .map(|(names, _)| names.clone())
477 .unwrap_or_default()
478 });
479
480 let mut all_indexes: Vec<IndexAttr> = container.indexes;
482 for field in &named.named {
483 let ident = field.ident.as_ref().expect("named");
484 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
487 if fa.index {
488 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
489 let auto_name = if fa.index_unique {
490 format!("{table}_{col_name}_uq_idx")
491 } else {
492 format!("{table}_{col_name}_idx")
493 };
494 all_indexes.push(IndexAttr {
495 name: fa.index_name.or(Some(auto_name)),
496 columns: vec![col_name],
497 unique: fa.index_unique,
498 });
499 }
500 }
501 }
502
503 let model_impl = model_impl_tokens(
504 struct_name,
505 &model_name,
506 &table,
507 display.as_deref(),
508 app_label.as_deref(),
509 container.admin.as_ref(),
510 &collected.field_schemas,
511 collected.soft_delete_column.as_deref(),
512 container.permissions,
513 audit_track_names.as_deref(),
514 &container.m2m,
515 &all_indexes,
516 &container.checks,
517 &container.composite_fks,
518 &container.generic_fks,
519 container.scope.as_deref(),
520 );
521 let module_ident = column_module_ident(struct_name);
522 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
523 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
524 let track_set: Option<std::collections::HashSet<&str>> = audit
525 .track
526 .as_ref()
527 .map(|(names, _)| names.iter().map(String::as_str).collect());
528 collected
529 .column_entries
530 .iter()
531 .filter(|c| {
532 track_set
533 .as_ref()
534 .map_or(true, |s| s.contains(c.name.as_str()))
535 })
536 .collect()
537 });
538 let inherent_impl = inherent_impl_tokens(
539 struct_name,
540 &collected,
541 collected.primary_key.as_ref(),
542 &column_consts,
543 audited_fields.as_deref(),
544 );
545 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
546 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
547 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
548 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
549
550 Ok(quote! {
551 #model_impl
552 #inherent_impl
553 #from_row_impl
554 #column_module
555 #reverse_helpers
556 #m2m_accessors
557
558 ::rustango::core::inventory::submit! {
559 ::rustango::core::ModelEntry {
560 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
561 module_path: ::core::module_path!(),
566 }
567 }
568 })
569}
570
571fn load_related_impl_tokens(
582 struct_name: &syn::Ident,
583 fk_relations: &[FkRelation],
584) -> TokenStream2 {
585 let arms = fk_relations.iter().map(|rel| {
586 let parent_ty = &rel.parent_type;
587 let fk_col = rel.fk_column.as_str();
588 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
591 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
592 let assign = if rel.nullable {
593 quote! {
594 self.#field_ident = ::core::option::Option::Some(
595 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
596 );
597 }
598 } else {
599 quote! {
600 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
601 }
602 };
603 quote! {
604 #fk_col => {
605 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
606 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
613 ::rustango::core::SqlValue::#variant_ident(v) => v,
614 _other => {
615 ::core::debug_assert!(
616 false,
617 "rustango macro bug: load_related on FK `{}` expected \
618 SqlValue::{} from parent's __rustango_pk_value but got \
619 {:?} — file a bug at https://github.com/ujeenet/rustango",
620 #fk_col,
621 ::core::stringify!(#variant_ident),
622 _other,
623 );
624 #default_expr
625 }
626 };
627 #assign
628 ::core::result::Result::Ok(true)
629 }
630 }
631 });
632 quote! {
633 impl ::rustango::sql::LoadRelated for #struct_name {
634 #[allow(unused_variables)]
635 fn __rustango_load_related(
636 &mut self,
637 row: &::rustango::sql::sqlx::postgres::PgRow,
638 field_name: &str,
639 alias: &str,
640 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
641 match field_name {
642 #( #arms )*
643 _ => ::core::result::Result::Ok(false),
644 }
645 }
646 }
647 }
648}
649
650fn load_related_impl_my_tokens(
658 struct_name: &syn::Ident,
659 fk_relations: &[FkRelation],
660) -> TokenStream2 {
661 let arms = fk_relations.iter().map(|rel| {
662 let parent_ty = &rel.parent_type;
663 let fk_col = rel.fk_column.as_str();
664 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
665 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
666 let assign = if rel.nullable {
667 quote! {
668 __self.#field_ident = ::core::option::Option::Some(
669 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
670 );
671 }
672 } else {
673 quote! {
674 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
675 }
676 };
677 quote! {
682 #fk_col => {
683 let _parent: #parent_ty =
684 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
685 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
688 ::rustango::core::SqlValue::#variant_ident(v) => v,
689 _other => {
690 ::core::debug_assert!(
691 false,
692 "rustango macro bug: load_related on FK `{}` expected \
693 SqlValue::{} from parent's __rustango_pk_value but got \
694 {:?} — file a bug at https://github.com/ujeenet/rustango",
695 #fk_col,
696 ::core::stringify!(#variant_ident),
697 _other,
698 );
699 #default_expr
700 }
701 };
702 #assign
703 ::core::result::Result::Ok(true)
704 }
705 }
706 });
707 quote! {
708 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
709 #( #arms )*
710 });
711 }
712}
713
714fn fk_pk_access_impl_tokens(
722 struct_name: &syn::Ident,
723 fk_relations: &[FkRelation],
724) -> TokenStream2 {
725 let arms = fk_relations.iter().map(|rel| {
726 let fk_col = rel.fk_column.as_str();
727 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
728 if rel.pk_kind == DetectedKind::I64 {
729 if rel.nullable {
735 quote! {
736 #fk_col => self.#field_ident
737 .as_ref()
738 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
739 }
740 } else {
741 quote! {
742 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
743 }
744 }
745 } else {
746 quote! {
754 #fk_col => ::core::option::Option::None,
755 }
756 }
757 });
758 quote! {
759 impl ::rustango::sql::FkPkAccess for #struct_name {
760 #[allow(unused_variables)]
761 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
762 match field_name {
763 #( #arms )*
764 _ => ::core::option::Option::None,
765 }
766 }
767 }
768 }
769}
770
771fn reverse_helper_tokens(
777 child_ident: &syn::Ident,
778 fk_relations: &[FkRelation],
779) -> TokenStream2 {
780 if fk_relations.is_empty() {
781 return TokenStream2::new();
782 }
783 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
787 let method_ident = syn::Ident::new(&suffix, child_ident.span());
788 let impls = fk_relations.iter().map(|rel| {
789 let parent_ty = &rel.parent_type;
790 let fk_col = rel.fk_column.as_str();
791 let doc = format!(
792 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
793 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
794 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
795 further `{child_ident}::objects()` filters via direct queryset use."
796 );
797 quote! {
798 impl #parent_ty {
799 #[doc = #doc]
800 pub async fn #method_ident<'_c, _E>(
805 &self,
806 _executor: _E,
807 ) -> ::core::result::Result<
808 ::std::vec::Vec<#child_ident>,
809 ::rustango::sql::ExecError,
810 >
811 where
812 _E: ::rustango::sql::sqlx::Executor<
813 '_c,
814 Database = ::rustango::sql::sqlx::Postgres,
815 >,
816 {
817 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
818 ::rustango::query::QuerySet::<#child_ident>::new()
819 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
820 .fetch_on(_executor)
821 .await
822 }
823 }
824 }
825 });
826 quote! { #( #impls )* }
827}
828
829fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
832 if m2m_relations.is_empty() {
833 return TokenStream2::new();
834 }
835 let methods = m2m_relations.iter().map(|rel| {
836 let method_name = format!("{}_m2m", rel.name);
837 let method_ident = syn::Ident::new(&method_name, struct_name.span());
838 let through = rel.through.as_str();
839 let src_col = rel.src.as_str();
840 let dst_col = rel.dst.as_str();
841 quote! {
842 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
843 ::rustango::sql::M2MManager {
844 src_pk: self.__rustango_pk_value(),
845 through: #through,
846 src_col: #src_col,
847 dst_col: #dst_col,
848 }
849 }
850 }
851 });
852 quote! {
853 impl #struct_name {
854 #( #methods )*
855 }
856 }
857}
858
859struct ColumnEntry {
860 ident: syn::Ident,
863 value_ty: Type,
865 name: String,
867 column: String,
869 field_type_tokens: TokenStream2,
871}
872
873struct CollectedFields {
874 field_schemas: Vec<TokenStream2>,
875 from_row_inits: Vec<TokenStream2>,
876 from_aliased_row_inits: Vec<TokenStream2>,
880 insert_columns: Vec<TokenStream2>,
883 insert_values: Vec<TokenStream2>,
886 insert_pushes: Vec<TokenStream2>,
891 returning_cols: Vec<TokenStream2>,
894 auto_assigns: Vec<TokenStream2>,
897 auto_field_idents: Vec<(syn::Ident, String)>,
901 first_auto_value_ty: Option<Type>,
904 bulk_pushes_no_auto: Vec<TokenStream2>,
908 bulk_pushes_all: Vec<TokenStream2>,
912 bulk_columns_no_auto: Vec<TokenStream2>,
915 bulk_columns_all: Vec<TokenStream2>,
918 bulk_auto_uniformity: Vec<TokenStream2>,
922 first_auto_ident: Option<syn::Ident>,
925 has_auto: bool,
927 pk_is_auto: bool,
931 update_assignments: Vec<TokenStream2>,
934 upsert_update_columns: Vec<TokenStream2>,
937 primary_key: Option<(syn::Ident, String)>,
938 column_entries: Vec<ColumnEntry>,
939 field_names: Vec<String>,
942 fk_relations: Vec<FkRelation>,
947 soft_delete_column: Option<String>,
952}
953
954#[derive(Clone)]
955struct FkRelation {
956 parent_type: Type,
959 fk_column: String,
962 pk_kind: DetectedKind,
967 nullable: bool,
972}
973
974fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
975 let cap = named.named.len();
976 let mut out = CollectedFields {
977 field_schemas: Vec::with_capacity(cap),
978 from_row_inits: Vec::with_capacity(cap),
979 from_aliased_row_inits: Vec::with_capacity(cap),
980 insert_columns: Vec::with_capacity(cap),
981 insert_values: Vec::with_capacity(cap),
982 insert_pushes: Vec::with_capacity(cap),
983 returning_cols: Vec::new(),
984 auto_assigns: Vec::new(),
985 auto_field_idents: Vec::new(),
986 first_auto_value_ty: None,
987 bulk_pushes_no_auto: Vec::with_capacity(cap),
988 bulk_pushes_all: Vec::with_capacity(cap),
989 bulk_columns_no_auto: Vec::with_capacity(cap),
990 bulk_columns_all: Vec::with_capacity(cap),
991 bulk_auto_uniformity: Vec::new(),
992 first_auto_ident: None,
993 has_auto: false,
994 pk_is_auto: false,
995 update_assignments: Vec::with_capacity(cap),
996 upsert_update_columns: Vec::with_capacity(cap),
997 primary_key: None,
998 column_entries: Vec::with_capacity(cap),
999 field_names: Vec::with_capacity(cap),
1000 fk_relations: Vec::new(),
1001 soft_delete_column: None,
1002 };
1003
1004 for field in &named.named {
1005 let info = process_field(field, table)?;
1006 out.field_names.push(info.ident.to_string());
1007 out.field_schemas.push(info.schema);
1008 out.from_row_inits.push(info.from_row_init);
1009 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1010 if let Some(parent_ty) = info.fk_inner.clone() {
1011 out.fk_relations.push(FkRelation {
1012 parent_type: parent_ty,
1013 fk_column: info.column.clone(),
1014 pk_kind: info.fk_pk_kind,
1015 nullable: info.nullable,
1016 });
1017 }
1018 if info.soft_delete {
1019 if out.soft_delete_column.is_some() {
1020 return Err(syn::Error::new_spanned(
1021 field,
1022 "only one field may be marked `#[rustango(soft_delete)]`",
1023 ));
1024 }
1025 out.soft_delete_column = Some(info.column.clone());
1026 }
1027 let column = info.column.as_str();
1028 let ident = info.ident;
1029 out.insert_columns.push(quote!(#column));
1030 out.insert_values.push(quote! {
1031 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1032 ::core::clone::Clone::clone(&self.#ident)
1033 )
1034 });
1035 if info.auto {
1036 out.has_auto = true;
1037 if out.first_auto_ident.is_none() {
1038 out.first_auto_ident = Some(ident.clone());
1039 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1040 }
1041 out.returning_cols.push(quote!(#column));
1042 out.auto_field_idents
1043 .push((ident.clone(), info.column.clone()));
1044 out.auto_assigns.push(quote! {
1045 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1046 });
1047 out.insert_pushes.push(quote! {
1048 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1049 _columns.push(#column);
1050 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1051 ::core::clone::Clone::clone(_v)
1052 ));
1053 }
1054 });
1055 out.bulk_columns_all.push(quote!(#column));
1058 out.bulk_pushes_all.push(quote! {
1059 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1060 ::core::clone::Clone::clone(&_row.#ident)
1061 ));
1062 });
1063 let ident_clone = ident.clone();
1067 out.bulk_auto_uniformity.push(quote! {
1068 for _r in rows.iter().skip(1) {
1069 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1070 return ::core::result::Result::Err(
1071 ::rustango::sql::ExecError::Sql(
1072 ::rustango::sql::SqlError::BulkAutoMixed
1073 )
1074 );
1075 }
1076 }
1077 });
1078 } else {
1079 out.insert_pushes.push(quote! {
1080 _columns.push(#column);
1081 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1082 ::core::clone::Clone::clone(&self.#ident)
1083 ));
1084 });
1085 out.bulk_columns_no_auto.push(quote!(#column));
1087 out.bulk_columns_all.push(quote!(#column));
1088 let push_expr = quote! {
1089 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1090 ::core::clone::Clone::clone(&_row.#ident)
1091 ));
1092 };
1093 out.bulk_pushes_no_auto.push(push_expr.clone());
1094 out.bulk_pushes_all.push(push_expr);
1095 }
1096 if info.primary_key {
1097 if out.primary_key.is_some() {
1098 return Err(syn::Error::new_spanned(
1099 field,
1100 "only one field may be marked `#[rustango(primary_key)]`",
1101 ));
1102 }
1103 out.primary_key = Some((ident.clone(), info.column.clone()));
1104 if info.auto {
1105 out.pk_is_auto = true;
1106 }
1107 } else if info.auto_now_add {
1108 } else if info.auto_now {
1110 out.update_assignments.push(quote! {
1115 ::rustango::core::Assignment {
1116 column: #column,
1117 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1118 ::chrono::Utc::now()
1119 ),
1120 }
1121 });
1122 out.upsert_update_columns.push(quote!(#column));
1123 } else {
1124 out.update_assignments.push(quote! {
1125 ::rustango::core::Assignment {
1126 column: #column,
1127 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1128 ::core::clone::Clone::clone(&self.#ident)
1129 ),
1130 }
1131 });
1132 out.upsert_update_columns.push(quote!(#column));
1133 }
1134 out.column_entries.push(ColumnEntry {
1135 ident: ident.clone(),
1136 value_ty: info.value_ty.clone(),
1137 name: ident.to_string(),
1138 column: info.column.clone(),
1139 field_type_tokens: info.field_type_tokens,
1140 });
1141 }
1142 Ok(out)
1143}
1144
1145fn model_impl_tokens(
1146 struct_name: &syn::Ident,
1147 model_name: &str,
1148 table: &str,
1149 display: Option<&str>,
1150 app_label: Option<&str>,
1151 admin: Option<&AdminAttrs>,
1152 field_schemas: &[TokenStream2],
1153 soft_delete_column: Option<&str>,
1154 permissions: bool,
1155 audit_track: Option<&[String]>,
1156 m2m_relations: &[M2MAttr],
1157 indexes: &[IndexAttr],
1158 checks: &[CheckAttr],
1159 composite_fks: &[CompositeFkAttr],
1160 generic_fks: &[GenericFkAttr],
1161 scope: Option<&str>,
1162) -> TokenStream2 {
1163 let display_tokens = if let Some(name) = display {
1164 quote!(::core::option::Option::Some(#name))
1165 } else {
1166 quote!(::core::option::Option::None)
1167 };
1168 let app_label_tokens = if let Some(name) = app_label {
1169 quote!(::core::option::Option::Some(#name))
1170 } else {
1171 quote!(::core::option::Option::None)
1172 };
1173 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1174 quote!(::core::option::Option::Some(#col))
1175 } else {
1176 quote!(::core::option::Option::None)
1177 };
1178 let audit_track_tokens = match audit_track {
1179 None => quote!(::core::option::Option::None),
1180 Some(names) => {
1181 let lits = names.iter().map(|n| n.as_str());
1182 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1183 }
1184 };
1185 let admin_tokens = admin_config_tokens(admin);
1186 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1190 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1191 _ => quote!(::rustango::core::ModelScope::Tenant),
1192 };
1193 let indexes_tokens = indexes.iter().map(|idx| {
1194 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1195 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1196 let unique = idx.unique;
1197 quote! {
1198 ::rustango::core::IndexSchema {
1199 name: #name,
1200 columns: &[ #(#cols),* ],
1201 unique: #unique,
1202 }
1203 }
1204 });
1205 let checks_tokens = checks.iter().map(|c| {
1206 let name = c.name.as_str();
1207 let expr = c.expr.as_str();
1208 quote! {
1209 ::rustango::core::CheckConstraint {
1210 name: #name,
1211 expr: #expr,
1212 }
1213 }
1214 });
1215 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1216 let name = rel.name.as_str();
1217 let to = rel.to.as_str();
1218 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1219 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1220 quote! {
1221 ::rustango::core::CompositeFkRelation {
1222 name: #name,
1223 to: #to,
1224 from: &[ #(#from_cols),* ],
1225 on: &[ #(#on_cols),* ],
1226 }
1227 }
1228 });
1229 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1230 let name = rel.name.as_str();
1231 let ct_col = rel.ct_column.as_str();
1232 let pk_col = rel.pk_column.as_str();
1233 quote! {
1234 ::rustango::core::GenericRelation {
1235 name: #name,
1236 ct_column: #ct_col,
1237 pk_column: #pk_col,
1238 }
1239 }
1240 });
1241 let m2m_tokens = m2m_relations.iter().map(|rel| {
1242 let name = rel.name.as_str();
1243 let to = rel.to.as_str();
1244 let through = rel.through.as_str();
1245 let src = rel.src.as_str();
1246 let dst = rel.dst.as_str();
1247 quote! {
1248 ::rustango::core::M2MRelation {
1249 name: #name,
1250 to: #to,
1251 through: #through,
1252 src_col: #src,
1253 dst_col: #dst,
1254 }
1255 }
1256 });
1257 quote! {
1258 impl ::rustango::core::Model for #struct_name {
1259 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1260 name: #model_name,
1261 table: #table,
1262 fields: &[ #(#field_schemas),* ],
1263 display: #display_tokens,
1264 app_label: #app_label_tokens,
1265 admin: #admin_tokens,
1266 soft_delete_column: #soft_delete_tokens,
1267 permissions: #permissions,
1268 audit_track: #audit_track_tokens,
1269 m2m: &[ #(#m2m_tokens),* ],
1270 indexes: &[ #(#indexes_tokens),* ],
1271 check_constraints: &[ #(#checks_tokens),* ],
1272 composite_relations: &[ #(#composite_fk_tokens),* ],
1273 generic_relations: &[ #(#generic_fk_tokens),* ],
1274 scope: #scope_tokens,
1275 };
1276 }
1277 }
1278}
1279
1280fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1284 let Some(admin) = admin else {
1285 return quote!(::core::option::Option::None);
1286 };
1287
1288 let list_display = admin
1289 .list_display
1290 .as_ref()
1291 .map(|(v, _)| v.as_slice())
1292 .unwrap_or(&[]);
1293 let list_display_lits = list_display.iter().map(|s| s.as_str());
1294
1295 let search_fields = admin
1296 .search_fields
1297 .as_ref()
1298 .map(|(v, _)| v.as_slice())
1299 .unwrap_or(&[]);
1300 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1301
1302 let readonly_fields = admin
1303 .readonly_fields
1304 .as_ref()
1305 .map(|(v, _)| v.as_slice())
1306 .unwrap_or(&[]);
1307 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1308
1309 let list_filter = admin
1310 .list_filter
1311 .as_ref()
1312 .map(|(v, _)| v.as_slice())
1313 .unwrap_or(&[]);
1314 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1315
1316 let actions = admin
1317 .actions
1318 .as_ref()
1319 .map(|(v, _)| v.as_slice())
1320 .unwrap_or(&[]);
1321 let actions_lits = actions.iter().map(|s| s.as_str());
1322
1323 let fieldsets = admin
1324 .fieldsets
1325 .as_ref()
1326 .map(|(v, _)| v.as_slice())
1327 .unwrap_or(&[]);
1328 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1329 let title = title.as_str();
1330 let field_lits = fields.iter().map(|s| s.as_str());
1331 quote!(::rustango::core::Fieldset {
1332 title: #title,
1333 fields: &[ #( #field_lits ),* ],
1334 })
1335 });
1336
1337 let list_per_page = admin.list_per_page.unwrap_or(0);
1338
1339 let ordering_pairs = admin
1340 .ordering
1341 .as_ref()
1342 .map(|(v, _)| v.as_slice())
1343 .unwrap_or(&[]);
1344 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1345 let name = name.as_str();
1346 let desc = *desc;
1347 quote!((#name, #desc))
1348 });
1349
1350 quote! {
1351 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1352 list_display: &[ #( #list_display_lits ),* ],
1353 search_fields: &[ #( #search_fields_lits ),* ],
1354 list_per_page: #list_per_page,
1355 ordering: &[ #( #ordering_tokens ),* ],
1356 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1357 list_filter: &[ #( #list_filter_lits ),* ],
1358 actions: &[ #( #actions_lits ),* ],
1359 fieldsets: &[ #( #fieldset_tokens ),* ],
1360 })
1361 }
1362}
1363
1364fn inherent_impl_tokens(
1365 struct_name: &syn::Ident,
1366 fields: &CollectedFields,
1367 primary_key: Option<&(syn::Ident, String)>,
1368 column_consts: &TokenStream2,
1369 audited_fields: Option<&[&ColumnEntry]>,
1370) -> TokenStream2 {
1371 let executor_passes_to_data_write = if audited_fields.is_some() {
1377 quote!(&mut *_executor)
1378 } else {
1379 quote!(_executor)
1380 };
1381 let executor_param = if audited_fields.is_some() {
1382 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1383 } else {
1384 quote!(_executor: _E)
1385 };
1386 let executor_generics = if audited_fields.is_some() {
1387 quote!()
1388 } else {
1389 quote!(<'_c, _E>)
1390 };
1391 let executor_where = if audited_fields.is_some() {
1392 quote!()
1393 } else {
1394 quote! {
1395 where
1396 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1397 }
1398 };
1399 let pool_to_save_on = if audited_fields.is_some() {
1404 quote! {
1405 let mut _conn = pool.acquire().await?;
1406 self.save_on(&mut *_conn).await
1407 }
1408 } else {
1409 quote!(self.save_on(pool).await)
1410 };
1411 let pool_to_insert_on = if audited_fields.is_some() {
1412 quote! {
1413 let mut _conn = pool.acquire().await?;
1414 self.insert_on(&mut *_conn).await
1415 }
1416 } else {
1417 quote!(self.insert_on(pool).await)
1418 };
1419 let pool_to_delete_on = if audited_fields.is_some() {
1420 quote! {
1421 let mut _conn = pool.acquire().await?;
1422 self.delete_on(&mut *_conn).await
1423 }
1424 } else {
1425 quote!(self.delete_on(pool).await)
1426 };
1427 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1428 quote! {
1429 let mut _conn = pool.acquire().await?;
1430 Self::bulk_insert_on(rows, &mut *_conn).await
1431 }
1432 } else {
1433 quote!(Self::bulk_insert_on(rows, pool).await)
1434 };
1435 let pool_to_upsert_on = if audited_fields.is_some() {
1442 quote! {
1443 let mut _conn = pool.acquire().await?;
1444 self.upsert_on(&mut *_conn).await
1445 }
1446 } else {
1447 quote!(self.upsert_on(pool).await)
1448 };
1449
1450 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1468 quote!()
1477 } else if audited_fields.is_some() && fields.has_auto {
1478 quote!()
1481 } else if fields.has_auto {
1482 let pushes = &fields.insert_pushes;
1483 let returning_cols = &fields.returning_cols;
1484 quote! {
1485 pub async fn insert_pool(
1491 &mut self,
1492 pool: &::rustango::sql::Pool,
1493 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1494 let mut _columns: ::std::vec::Vec<&'static str> =
1495 ::std::vec::Vec::new();
1496 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1497 ::std::vec::Vec::new();
1498 #( #pushes )*
1499 let _query = ::rustango::core::InsertQuery {
1500 model: <Self as ::rustango::core::Model>::SCHEMA,
1501 columns: _columns,
1502 values: _values,
1503 returning: ::std::vec![ #( #returning_cols ),* ],
1504 on_conflict: ::core::option::Option::None,
1505 };
1506 let _result = ::rustango::sql::insert_returning_pool(
1507 pool, &_query,
1508 ).await?;
1509 ::rustango::sql::apply_auto_pk_pool(_result, self)
1510 }
1511 }
1512 } else {
1513 let insert_columns = &fields.insert_columns;
1514 let insert_values = &fields.insert_values;
1515 quote! {
1516 pub async fn insert_pool(
1523 &self,
1524 pool: &::rustango::sql::Pool,
1525 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1526 let _query = ::rustango::core::InsertQuery {
1527 model: <Self as ::rustango::core::Model>::SCHEMA,
1528 columns: ::std::vec![ #( #insert_columns ),* ],
1529 values: ::std::vec![ #( #insert_values ),* ],
1530 returning: ::std::vec::Vec::new(),
1531 on_conflict: ::core::option::Option::None,
1532 };
1533 ::rustango::sql::insert_pool(pool, &_query).await
1534 }
1535 }
1536 };
1537
1538 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1551 .map(|tracked| {
1552 tracked
1553 .iter()
1554 .map(|c| {
1555 let column_lit = c.column.as_str();
1556 let ident = &c.ident;
1557 quote! {
1558 (
1559 #column_lit,
1560 ::serde_json::to_value(&self.#ident)
1561 .unwrap_or(::serde_json::Value::Null),
1562 )
1563 }
1564 })
1565 .collect()
1566 })
1567 .unwrap_or_default();
1568 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1569 if fields.pk_is_auto {
1570 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1571 } else {
1572 quote!(::std::format!("{}", &self.#pk_ident))
1573 }
1574 } else {
1575 quote!(::std::string::String::new())
1576 };
1577 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1578 if audited_fields.is_some() {
1579 let pairs = audit_pair_tokens.iter();
1580 let pk_str = audit_pk_to_string.clone();
1581 quote! {
1582 let _audit_entry = ::rustango::audit::PendingEntry {
1583 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1584 entity_pk: #pk_str,
1585 operation: #op_path,
1586 source: ::rustango::audit::current_source(),
1587 changes: ::rustango::audit::snapshot_changes(&[
1588 #( #pairs ),*
1589 ]),
1590 };
1591 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1592 }
1593 } else {
1594 quote!()
1595 }
1596 };
1597 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1598 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1599 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1600 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1601
1602 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1618 let pk_column_lit = pk_col.as_str();
1619 let assignments = &fields.update_assignments;
1620 if audited_fields.is_some() {
1621 if fields.pk_is_auto {
1622 quote!()
1626 } else {
1627 let pairs = audit_pair_tokens.iter();
1628 let pk_str = audit_pk_to_string.clone();
1629 quote! {
1630 pub async fn save_pool(
1644 &mut self,
1645 pool: &::rustango::sql::Pool,
1646 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1647 let _query = ::rustango::core::UpdateQuery {
1648 model: <Self as ::rustango::core::Model>::SCHEMA,
1649 set: ::std::vec![ #( #assignments ),* ],
1650 where_clause: ::rustango::core::WhereExpr::Predicate(
1651 ::rustango::core::Filter {
1652 column: #pk_column_lit,
1653 op: ::rustango::core::Op::Eq,
1654 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1655 ::core::clone::Clone::clone(&self.#pk_ident)
1656 ),
1657 }
1658 ),
1659 };
1660 let _audit_entry = ::rustango::audit::PendingEntry {
1661 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1662 entity_pk: #pk_str,
1663 operation: ::rustango::audit::AuditOp::Update,
1664 source: ::rustango::audit::current_source(),
1665 changes: ::rustango::audit::snapshot_changes(&[
1666 #( #pairs ),*
1667 ]),
1668 };
1669 let _ = ::rustango::audit::save_one_with_audit_pool(
1670 pool, &_query, &_audit_entry,
1671 ).await?;
1672 ::core::result::Result::Ok(())
1673 }
1674 }
1675 }
1676 } else {
1677 let dispatch_unset = if fields.pk_is_auto {
1678 quote! {
1679 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1680 return self.insert_pool(pool).await;
1681 }
1682 }
1683 } else {
1684 quote!()
1685 };
1686 quote! {
1687 pub async fn save_pool(
1694 &mut self,
1695 pool: &::rustango::sql::Pool,
1696 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1697 #dispatch_unset
1698 let _query = ::rustango::core::UpdateQuery {
1699 model: <Self as ::rustango::core::Model>::SCHEMA,
1700 set: ::std::vec![ #( #assignments ),* ],
1701 where_clause: ::rustango::core::WhereExpr::Predicate(
1702 ::rustango::core::Filter {
1703 column: #pk_column_lit,
1704 op: ::rustango::core::Op::Eq,
1705 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1706 ::core::clone::Clone::clone(&self.#pk_ident)
1707 ),
1708 }
1709 ),
1710 };
1711 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1712 ::core::result::Result::Ok(())
1713 }
1714 }
1715 }
1716 } else {
1717 quote!()
1718 };
1719
1720 let pool_insert_method = if audited_fields.is_some() {
1727 if let Some(_) = primary_key {
1728 let pushes = if fields.has_auto {
1729 fields.insert_pushes.clone()
1730 } else {
1731 fields
1736 .insert_columns
1737 .iter()
1738 .zip(&fields.insert_values)
1739 .map(|(col, val)| {
1740 quote! {
1741 _columns.push(#col);
1742 _values.push(#val);
1743 }
1744 })
1745 .collect()
1746 };
1747 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1748 fields.returning_cols.clone()
1749 } else {
1750 primary_key
1757 .map(|(_, col)| {
1758 let lit = col.as_str();
1759 vec![quote!(#lit)]
1760 })
1761 .unwrap_or_default()
1762 };
1763 let pairs = audit_pair_tokens.iter();
1764 let pk_str = audit_pk_to_string.clone();
1765 quote! {
1766 pub async fn insert_pool(
1775 &mut self,
1776 pool: &::rustango::sql::Pool,
1777 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1778 let mut _columns: ::std::vec::Vec<&'static str> =
1779 ::std::vec::Vec::new();
1780 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1781 ::std::vec::Vec::new();
1782 #( #pushes )*
1783 let _query = ::rustango::core::InsertQuery {
1784 model: <Self as ::rustango::core::Model>::SCHEMA,
1785 columns: _columns,
1786 values: _values,
1787 returning: ::std::vec![ #( #returning_cols ),* ],
1788 on_conflict: ::core::option::Option::None,
1789 };
1790 let _audit_entry = ::rustango::audit::PendingEntry {
1791 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1792 entity_pk: #pk_str,
1793 operation: ::rustango::audit::AuditOp::Create,
1794 source: ::rustango::audit::current_source(),
1795 changes: ::rustango::audit::snapshot_changes(&[
1796 #( #pairs ),*
1797 ]),
1798 };
1799 let _result = ::rustango::audit::insert_one_with_audit_pool(
1800 pool, &_query, &_audit_entry,
1801 ).await?;
1802 ::rustango::sql::apply_auto_pk_pool(_result, self)
1803 }
1804 }
1805 } else {
1806 quote!()
1807 }
1808 } else {
1809 pool_insert_method
1811 };
1812
1813 let pool_save_method = if let Some(tracked) = audited_fields {
1834 if let Some((pk_ident, pk_col)) = primary_key {
1835 let pk_column_lit = pk_col.as_str();
1836 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1840 let pk_str = audit_pk_to_string.clone();
1841 let mk_before_pairs = |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1846 tracked
1847 .iter()
1848 .map(|c| {
1849 let column_lit = c.column.as_str();
1850 let value_ty = &c.value_ty;
1851 quote! {
1852 (
1853 #column_lit,
1854 match #getter::<#value_ty>(
1855 _audit_before_row, #column_lit,
1856 ) {
1857 ::core::result::Result::Ok(v) => {
1858 ::serde_json::to_value(&v)
1859 .unwrap_or(::serde_json::Value::Null)
1860 }
1861 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1862 },
1863 )
1864 }
1865 })
1866 .collect()
1867 };
1868 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
1869 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
1870 let before_pairs_my: Vec<proc_macro2::TokenStream> =
1871 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
1872 let pg_select_cols: String = tracked
1873 .iter()
1874 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1875 .collect::<Vec<_>>()
1876 .join(", ");
1877 let my_select_cols: String = tracked
1878 .iter()
1879 .map(|c| format!("`{}`", c.column.replace('`', "``")))
1880 .collect::<Vec<_>>()
1881 .join(", ");
1882 let pk_value_for_bind = if fields.pk_is_auto {
1883 quote!(self.#pk_ident.get().copied().unwrap_or_default())
1884 } else {
1885 quote!(::core::clone::Clone::clone(&self.#pk_ident))
1886 };
1887 let assignments = &fields.update_assignments;
1888 let unset_dispatch = if fields.has_auto {
1889 quote! {
1890 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1891 return self.insert_pool(pool).await;
1892 }
1893 }
1894 } else {
1895 quote!()
1896 };
1897 quote! {
1898 pub async fn save_pool(
1912 &mut self,
1913 pool: &::rustango::sql::Pool,
1914 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1915 #unset_dispatch
1916 let _query = ::rustango::core::UpdateQuery {
1917 model: <Self as ::rustango::core::Model>::SCHEMA,
1918 set: ::std::vec![ #( #assignments ),* ],
1919 where_clause: ::rustango::core::WhereExpr::Predicate(
1920 ::rustango::core::Filter {
1921 column: #pk_column_lit,
1922 op: ::rustango::core::Op::Eq,
1923 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1924 ::core::clone::Clone::clone(&self.#pk_ident)
1925 ),
1926 }
1927 ),
1928 };
1929 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1930 ::std::vec![ #( #after_pairs_pg ),* ];
1931 ::rustango::audit::save_one_with_diff_pool(
1932 pool,
1933 &_query,
1934 #pk_column_lit,
1935 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1936 #pk_value_for_bind,
1937 ),
1938 <Self as ::rustango::core::Model>::SCHEMA.table,
1939 #pk_str,
1940 _after_pairs,
1941 #pg_select_cols,
1942 #my_select_cols,
1943 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
1944 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
1945 ).await
1946 }
1947 }
1948 } else {
1949 quote!()
1950 }
1951 } else {
1952 pool_save_method
1953 };
1954
1955 let pool_delete_method = {
1962 let pk_column_lit = primary_key
1963 .map(|(_, col)| col.as_str())
1964 .unwrap_or("id");
1965 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
1966 if let Some(pk_ident) = pk_ident_for_pool {
1967 if audited_fields.is_some() {
1968 let pairs = audit_pair_tokens.iter();
1969 let pk_str = audit_pk_to_string.clone();
1970 quote! {
1971 pub async fn delete_pool(
1978 &self,
1979 pool: &::rustango::sql::Pool,
1980 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1981 let _query = ::rustango::core::DeleteQuery {
1982 model: <Self as ::rustango::core::Model>::SCHEMA,
1983 where_clause: ::rustango::core::WhereExpr::Predicate(
1984 ::rustango::core::Filter {
1985 column: #pk_column_lit,
1986 op: ::rustango::core::Op::Eq,
1987 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1988 ::core::clone::Clone::clone(&self.#pk_ident)
1989 ),
1990 }
1991 ),
1992 };
1993 let _audit_entry = ::rustango::audit::PendingEntry {
1994 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1995 entity_pk: #pk_str,
1996 operation: ::rustango::audit::AuditOp::Delete,
1997 source: ::rustango::audit::current_source(),
1998 changes: ::rustango::audit::snapshot_changes(&[
1999 #( #pairs ),*
2000 ]),
2001 };
2002 ::rustango::audit::delete_one_with_audit_pool(
2003 pool, &_query, &_audit_entry,
2004 ).await
2005 }
2006 }
2007 } else {
2008 quote! {
2009 pub async fn delete_pool(
2016 &self,
2017 pool: &::rustango::sql::Pool,
2018 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2019 let _query = ::rustango::core::DeleteQuery {
2020 model: <Self as ::rustango::core::Model>::SCHEMA,
2021 where_clause: ::rustango::core::WhereExpr::Predicate(
2022 ::rustango::core::Filter {
2023 column: #pk_column_lit,
2024 op: ::rustango::core::Op::Eq,
2025 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2026 ::core::clone::Clone::clone(&self.#pk_ident)
2027 ),
2028 }
2029 ),
2030 };
2031 ::rustango::sql::delete_pool(pool, &_query).await
2032 }
2033 }
2034 }
2035 } else {
2036 quote!()
2037 }
2038 };
2039
2040 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
2050 if let Some(tracked) = audited_fields {
2051 if tracked.is_empty() {
2052 (quote!(), quote!())
2053 } else {
2054 let select_cols: String = tracked
2055 .iter()
2056 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2057 .collect::<Vec<_>>()
2058 .join(", ");
2059 let pk_column_for_select = primary_key
2060 .map(|(_, col)| col.clone())
2061 .unwrap_or_default();
2062 let select_cols_lit = select_cols;
2063 let pk_column_lit_for_select = pk_column_for_select;
2064 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2065 if fields.pk_is_auto {
2066 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2067 } else {
2068 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2069 }
2070 } else {
2071 quote!(0_i64)
2072 };
2073 let before_pairs = tracked.iter().map(|c| {
2074 let column_lit = c.column.as_str();
2075 let value_ty = &c.value_ty;
2076 quote! {
2077 (
2078 #column_lit,
2079 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2080 &_audit_before_row, #column_lit,
2081 ) {
2082 ::core::result::Result::Ok(v) => {
2083 ::serde_json::to_value(&v)
2084 .unwrap_or(::serde_json::Value::Null)
2085 }
2086 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2087 },
2088 )
2089 }
2090 });
2091 let after_pairs = tracked.iter().map(|c| {
2092 let column_lit = c.column.as_str();
2093 let ident = &c.ident;
2094 quote! {
2095 (
2096 #column_lit,
2097 ::serde_json::to_value(&self.#ident)
2098 .unwrap_or(::serde_json::Value::Null),
2099 )
2100 }
2101 });
2102 let pk_str = audit_pk_to_string.clone();
2103 let pre = quote! {
2104 let _audit_select_sql = ::std::format!(
2105 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2106 #select_cols_lit,
2107 <Self as ::rustango::core::Model>::SCHEMA.table,
2108 #pk_column_lit_for_select,
2109 );
2110 let _audit_before_pairs:
2111 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2112 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2113 .bind(#pk_value_for_bind)
2114 .fetch_optional(&mut *_executor)
2115 .await
2116 {
2117 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2118 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2119 }
2120 _ => ::core::option::Option::None,
2121 };
2122 };
2123 let post = quote! {
2124 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2125 let _audit_after:
2126 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2127 ::std::vec![ #( #after_pairs ),* ];
2128 let _audit_entry = ::rustango::audit::PendingEntry {
2129 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2130 entity_pk: #pk_str,
2131 operation: ::rustango::audit::AuditOp::Update,
2132 source: ::rustango::audit::current_source(),
2133 changes: ::rustango::audit::diff_changes(
2134 &_audit_before,
2135 &_audit_after,
2136 ),
2137 };
2138 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2139 }
2140 };
2141 (pre, post)
2142 }
2143 } else {
2144 (quote!(), quote!())
2145 };
2146
2147 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2151 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2152 if fields.pk_is_auto {
2153 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2154 } else {
2155 quote!(::std::format!("{}", &_row.#pk_ident))
2156 }
2157 } else {
2158 quote!(::std::string::String::new())
2159 };
2160 let row_pairs = audited_fields
2161 .unwrap_or(&[])
2162 .iter()
2163 .map(|c| {
2164 let column_lit = c.column.as_str();
2165 let ident = &c.ident;
2166 quote! {
2167 (
2168 #column_lit,
2169 ::serde_json::to_value(&_row.#ident)
2170 .unwrap_or(::serde_json::Value::Null),
2171 )
2172 }
2173 });
2174 quote! {
2175 let _audit_source = ::rustango::audit::current_source();
2176 let mut _audit_entries:
2177 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2178 ::std::vec::Vec::with_capacity(rows.len());
2179 for _row in rows.iter() {
2180 _audit_entries.push(::rustango::audit::PendingEntry {
2181 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2182 entity_pk: #row_pk_str,
2183 operation: ::rustango::audit::AuditOp::Create,
2184 source: _audit_source.clone(),
2185 changes: ::rustango::audit::snapshot_changes(&[
2186 #( #row_pairs ),*
2187 ]),
2188 });
2189 }
2190 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2191 }
2192 } else {
2193 quote!()
2194 };
2195
2196 let save_method = if fields.pk_is_auto {
2197 let (pk_ident, pk_column) = primary_key
2198 .expect("pk_is_auto implies primary_key is Some");
2199 let pk_column_lit = pk_column.as_str();
2200 let assignments = &fields.update_assignments;
2201 let upsert_cols = &fields.upsert_update_columns;
2202 let upsert_pushes = &fields.insert_pushes;
2203 let upsert_returning = &fields.returning_cols;
2204 let upsert_auto_assigns = &fields.auto_assigns;
2205 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2206 quote!(::rustango::core::ConflictClause::DoNothing)
2207 } else {
2208 quote!(::rustango::core::ConflictClause::DoUpdate {
2209 target: ::std::vec![#pk_column_lit],
2210 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2211 })
2212 };
2213 Some(quote! {
2214 pub async fn save(
2232 &mut self,
2233 pool: &::rustango::sql::sqlx::PgPool,
2234 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2235 #pool_to_save_on
2236 }
2237
2238 pub async fn save_on #executor_generics (
2249 &mut self,
2250 #executor_param,
2251 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2252 #executor_where
2253 {
2254 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2255 return self.insert_on(#executor_passes_to_data_write).await;
2256 }
2257 #audit_update_pre
2258 let _query = ::rustango::core::UpdateQuery {
2259 model: <Self as ::rustango::core::Model>::SCHEMA,
2260 set: ::std::vec![ #( #assignments ),* ],
2261 where_clause: ::rustango::core::WhereExpr::Predicate(
2262 ::rustango::core::Filter {
2263 column: #pk_column_lit,
2264 op: ::rustango::core::Op::Eq,
2265 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2266 ::core::clone::Clone::clone(&self.#pk_ident)
2267 ),
2268 }
2269 ),
2270 };
2271 let _ = ::rustango::sql::update_on(
2272 #executor_passes_to_data_write,
2273 &_query,
2274 ).await?;
2275 #audit_update_post
2276 ::core::result::Result::Ok(())
2277 }
2278
2279 pub async fn save_on_with #executor_generics (
2290 &mut self,
2291 #executor_param,
2292 source: ::rustango::audit::AuditSource,
2293 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2294 #executor_where
2295 {
2296 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2297 }
2298
2299 pub async fn upsert(
2309 &mut self,
2310 pool: &::rustango::sql::sqlx::PgPool,
2311 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2312 #pool_to_upsert_on
2313 }
2314
2315 pub async fn upsert_on #executor_generics (
2321 &mut self,
2322 #executor_param,
2323 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2324 #executor_where
2325 {
2326 let mut _columns: ::std::vec::Vec<&'static str> =
2327 ::std::vec::Vec::new();
2328 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2329 ::std::vec::Vec::new();
2330 #( #upsert_pushes )*
2331 let query = ::rustango::core::InsertQuery {
2332 model: <Self as ::rustango::core::Model>::SCHEMA,
2333 columns: _columns,
2334 values: _values,
2335 returning: ::std::vec![ #( #upsert_returning ),* ],
2336 on_conflict: ::core::option::Option::Some(#conflict_clause),
2337 };
2338 let _returning_row_v = ::rustango::sql::insert_returning_on(
2339 #executor_passes_to_data_write,
2340 &query,
2341 ).await?;
2342 let _returning_row = &_returning_row_v;
2343 #( #upsert_auto_assigns )*
2344 ::core::result::Result::Ok(())
2345 }
2346 })
2347 } else {
2348 None
2349 };
2350
2351 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2352 let pk_column_lit = pk_column.as_str();
2353 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2360 let col_lit = col;
2361 quote! {
2362 pub async fn soft_delete_on #executor_generics (
2372 &self,
2373 #executor_param,
2374 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2375 #executor_where
2376 {
2377 let _query = ::rustango::core::UpdateQuery {
2378 model: <Self as ::rustango::core::Model>::SCHEMA,
2379 set: ::std::vec![
2380 ::rustango::core::Assignment {
2381 column: #col_lit,
2382 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2383 ::chrono::Utc::now()
2384 ),
2385 },
2386 ],
2387 where_clause: ::rustango::core::WhereExpr::Predicate(
2388 ::rustango::core::Filter {
2389 column: #pk_column_lit,
2390 op: ::rustango::core::Op::Eq,
2391 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2392 ::core::clone::Clone::clone(&self.#pk_ident)
2393 ),
2394 }
2395 ),
2396 };
2397 let _affected = ::rustango::sql::update_on(
2398 #executor_passes_to_data_write,
2399 &_query,
2400 ).await?;
2401 #audit_softdelete_emit
2402 ::core::result::Result::Ok(_affected)
2403 }
2404
2405 pub async fn restore_on #executor_generics (
2412 &self,
2413 #executor_param,
2414 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2415 #executor_where
2416 {
2417 let _query = ::rustango::core::UpdateQuery {
2418 model: <Self as ::rustango::core::Model>::SCHEMA,
2419 set: ::std::vec![
2420 ::rustango::core::Assignment {
2421 column: #col_lit,
2422 value: ::rustango::core::SqlValue::Null,
2423 },
2424 ],
2425 where_clause: ::rustango::core::WhereExpr::Predicate(
2426 ::rustango::core::Filter {
2427 column: #pk_column_lit,
2428 op: ::rustango::core::Op::Eq,
2429 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2430 ::core::clone::Clone::clone(&self.#pk_ident)
2431 ),
2432 }
2433 ),
2434 };
2435 let _affected = ::rustango::sql::update_on(
2436 #executor_passes_to_data_write,
2437 &_query,
2438 ).await?;
2439 #audit_restore_emit
2440 ::core::result::Result::Ok(_affected)
2441 }
2442 }
2443 } else {
2444 quote!()
2445 };
2446 quote! {
2447 pub async fn delete(
2455 &self,
2456 pool: &::rustango::sql::sqlx::PgPool,
2457 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2458 #pool_to_delete_on
2459 }
2460
2461 pub async fn delete_on #executor_generics (
2468 &self,
2469 #executor_param,
2470 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2471 #executor_where
2472 {
2473 let query = ::rustango::core::DeleteQuery {
2474 model: <Self as ::rustango::core::Model>::SCHEMA,
2475 where_clause: ::rustango::core::WhereExpr::Predicate(
2476 ::rustango::core::Filter {
2477 column: #pk_column_lit,
2478 op: ::rustango::core::Op::Eq,
2479 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2480 ::core::clone::Clone::clone(&self.#pk_ident)
2481 ),
2482 }
2483 ),
2484 };
2485 let _affected = ::rustango::sql::delete_on(
2486 #executor_passes_to_data_write,
2487 &query,
2488 ).await?;
2489 #audit_delete_emit
2490 ::core::result::Result::Ok(_affected)
2491 }
2492
2493 pub async fn delete_on_with #executor_generics (
2499 &self,
2500 #executor_param,
2501 source: ::rustango::audit::AuditSource,
2502 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2503 #executor_where
2504 {
2505 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2506 }
2507 #pool_delete_method
2508 #pool_insert_method
2509 #pool_save_method
2510 #soft_delete_methods
2511 }
2512 });
2513
2514 let insert_method = if fields.has_auto {
2515 let pushes = &fields.insert_pushes;
2516 let returning_cols = &fields.returning_cols;
2517 let auto_assigns = &fields.auto_assigns;
2518 quote! {
2519 pub async fn insert(
2528 &mut self,
2529 pool: &::rustango::sql::sqlx::PgPool,
2530 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2531 #pool_to_insert_on
2532 }
2533
2534 pub async fn insert_on #executor_generics (
2540 &mut self,
2541 #executor_param,
2542 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2543 #executor_where
2544 {
2545 let mut _columns: ::std::vec::Vec<&'static str> =
2546 ::std::vec::Vec::new();
2547 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2548 ::std::vec::Vec::new();
2549 #( #pushes )*
2550 let query = ::rustango::core::InsertQuery {
2551 model: <Self as ::rustango::core::Model>::SCHEMA,
2552 columns: _columns,
2553 values: _values,
2554 returning: ::std::vec![ #( #returning_cols ),* ],
2555 on_conflict: ::core::option::Option::None,
2556 };
2557 let _returning_row_v = ::rustango::sql::insert_returning_on(
2558 #executor_passes_to_data_write,
2559 &query,
2560 ).await?;
2561 let _returning_row = &_returning_row_v;
2562 #( #auto_assigns )*
2563 #audit_insert_emit
2564 ::core::result::Result::Ok(())
2565 }
2566
2567 pub async fn insert_on_with #executor_generics (
2573 &mut self,
2574 #executor_param,
2575 source: ::rustango::audit::AuditSource,
2576 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2577 #executor_where
2578 {
2579 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2580 }
2581 }
2582 } else {
2583 let insert_columns = &fields.insert_columns;
2584 let insert_values = &fields.insert_values;
2585 quote! {
2586 pub async fn insert(
2592 &self,
2593 pool: &::rustango::sql::sqlx::PgPool,
2594 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2595 self.insert_on(pool).await
2596 }
2597
2598 pub async fn insert_on<'_c, _E>(
2604 &self,
2605 _executor: _E,
2606 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2607 where
2608 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2609 {
2610 let query = ::rustango::core::InsertQuery {
2611 model: <Self as ::rustango::core::Model>::SCHEMA,
2612 columns: ::std::vec![ #( #insert_columns ),* ],
2613 values: ::std::vec![ #( #insert_values ),* ],
2614 returning: ::std::vec::Vec::new(),
2615 on_conflict: ::core::option::Option::None,
2616 };
2617 ::rustango::sql::insert_on(_executor, &query).await
2618 }
2619 }
2620 };
2621
2622 let bulk_insert_method = if fields.has_auto {
2623 let cols_no_auto = &fields.bulk_columns_no_auto;
2624 let cols_all = &fields.bulk_columns_all;
2625 let pushes_no_auto = &fields.bulk_pushes_no_auto;
2626 let pushes_all = &fields.bulk_pushes_all;
2627 let returning_cols = &fields.returning_cols;
2628 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2629 let uniformity = &fields.bulk_auto_uniformity;
2630 let first_auto_ident = fields
2631 .first_auto_ident
2632 .as_ref()
2633 .expect("has_auto implies first_auto_ident is Some");
2634 quote! {
2635 pub async fn bulk_insert(
2649 rows: &mut [Self],
2650 pool: &::rustango::sql::sqlx::PgPool,
2651 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2652 #pool_to_bulk_insert_on
2653 }
2654
2655 pub async fn bulk_insert_on #executor_generics (
2661 rows: &mut [Self],
2662 #executor_param,
2663 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2664 #executor_where
2665 {
2666 if rows.is_empty() {
2667 return ::core::result::Result::Ok(());
2668 }
2669 let _first_unset = matches!(
2670 rows[0].#first_auto_ident,
2671 ::rustango::sql::Auto::Unset
2672 );
2673 #( #uniformity )*
2674
2675 let mut _all_rows: ::std::vec::Vec<
2676 ::std::vec::Vec<::rustango::core::SqlValue>,
2677 > = ::std::vec::Vec::with_capacity(rows.len());
2678 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2679 for _row in rows.iter() {
2680 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2681 ::std::vec::Vec::new();
2682 #( #pushes_no_auto )*
2683 _all_rows.push(_row_vals);
2684 }
2685 ::std::vec![ #( #cols_no_auto ),* ]
2686 } else {
2687 for _row in rows.iter() {
2688 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2689 ::std::vec::Vec::new();
2690 #( #pushes_all )*
2691 _all_rows.push(_row_vals);
2692 }
2693 ::std::vec![ #( #cols_all ),* ]
2694 };
2695
2696 let _query = ::rustango::core::BulkInsertQuery {
2697 model: <Self as ::rustango::core::Model>::SCHEMA,
2698 columns: _columns,
2699 rows: _all_rows,
2700 returning: ::std::vec![ #( #returning_cols ),* ],
2701 on_conflict: ::core::option::Option::None,
2702 };
2703 let _returned = ::rustango::sql::bulk_insert_on(
2704 #executor_passes_to_data_write,
2705 &_query,
2706 ).await?;
2707 if _returned.len() != rows.len() {
2708 return ::core::result::Result::Err(
2709 ::rustango::sql::ExecError::Sql(
2710 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2711 expected: rows.len(),
2712 actual: _returned.len(),
2713 }
2714 )
2715 );
2716 }
2717 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2718 #auto_assigns_for_row
2719 }
2720 #audit_bulk_insert_emit
2721 ::core::result::Result::Ok(())
2722 }
2723 }
2724 } else {
2725 let cols_all = &fields.bulk_columns_all;
2726 let pushes_all = &fields.bulk_pushes_all;
2727 quote! {
2728 pub async fn bulk_insert(
2738 rows: &[Self],
2739 pool: &::rustango::sql::sqlx::PgPool,
2740 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2741 Self::bulk_insert_on(rows, pool).await
2742 }
2743
2744 pub async fn bulk_insert_on<'_c, _E>(
2750 rows: &[Self],
2751 _executor: _E,
2752 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2753 where
2754 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2755 {
2756 if rows.is_empty() {
2757 return ::core::result::Result::Ok(());
2758 }
2759 let mut _all_rows: ::std::vec::Vec<
2760 ::std::vec::Vec<::rustango::core::SqlValue>,
2761 > = ::std::vec::Vec::with_capacity(rows.len());
2762 for _row in rows.iter() {
2763 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2764 ::std::vec::Vec::new();
2765 #( #pushes_all )*
2766 _all_rows.push(_row_vals);
2767 }
2768 let _query = ::rustango::core::BulkInsertQuery {
2769 model: <Self as ::rustango::core::Model>::SCHEMA,
2770 columns: ::std::vec![ #( #cols_all ),* ],
2771 rows: _all_rows,
2772 returning: ::std::vec::Vec::new(),
2773 on_conflict: ::core::option::Option::None,
2774 };
2775 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2776 ::core::result::Result::Ok(())
2777 }
2778 }
2779 };
2780
2781 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2782 quote! {
2783 #[doc(hidden)]
2788 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2789 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2790 ::core::clone::Clone::clone(&self.#pk_ident)
2791 )
2792 }
2793 }
2794 });
2795
2796 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2797 quote! {
2798 impl ::rustango::sql::HasPkValue for #struct_name {
2799 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2800 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2801 ::core::clone::Clone::clone(&self.#pk_ident)
2802 )
2803 }
2804 }
2805 }
2806 });
2807
2808 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2809
2810 let assign_auto_pk_pool_impl = {
2816 let auto_assigns = &fields.auto_assigns;
2817 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
2818 let value_ty = fields
2836 .first_auto_value_ty
2837 .as_ref()
2838 .expect("first_auto_value_ty set whenever first_auto_ident is");
2839 quote! {
2840 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
2841 ::rustango_from_mysql_auto_id(_id)?;
2842 self.#first = ::rustango::sql::Auto::Set(_converted);
2843 ::core::result::Result::Ok(())
2844 }
2845 } else {
2846 quote! {
2847 let _ = _id;
2848 ::core::result::Result::Ok(())
2849 }
2850 };
2851 quote! {
2852 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
2853 fn __rustango_assign_from_pg_row(
2854 &mut self,
2855 _returning_row: &::rustango::sql::PgReturningRow,
2856 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2857 #( #auto_assigns )*
2858 ::core::result::Result::Ok(())
2859 }
2860 fn __rustango_assign_from_mysql_id(
2861 &mut self,
2862 _id: i64,
2863 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2864 #mysql_body
2865 }
2866 }
2867 }
2868 };
2869
2870 let from_aliased_row_inits = &fields.from_aliased_row_inits;
2871 let aliased_row_helper = quote! {
2872 #[doc(hidden)]
2878 pub fn __rustango_from_aliased_row(
2879 row: &::rustango::sql::sqlx::postgres::PgRow,
2880 prefix: &str,
2881 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2882 ::core::result::Result::Ok(Self {
2883 #( #from_aliased_row_inits ),*
2884 })
2885 }
2886 };
2887 let aliased_row_helper_my = quote! {
2890 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
2891 #( #from_aliased_row_inits ),*
2892 });
2893 };
2894
2895 let load_related_impl =
2896 load_related_impl_tokens(struct_name, &fields.fk_relations);
2897 let load_related_impl_my =
2898 load_related_impl_my_tokens(struct_name, &fields.fk_relations);
2899
2900 quote! {
2901 impl #struct_name {
2902 #[must_use]
2904 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
2905 ::rustango::query::QuerySet::new()
2906 }
2907
2908 #insert_method
2909
2910 #bulk_insert_method
2911
2912 #save_method
2913
2914 #pk_methods
2915
2916 #pk_value_helper
2917
2918 #aliased_row_helper
2919
2920 #column_consts
2921 }
2922
2923 #aliased_row_helper_my
2924
2925 #load_related_impl
2926
2927 #load_related_impl_my
2928
2929 #has_pk_value_impl
2930
2931 #fk_pk_access_impl
2932
2933 #assign_auto_pk_pool_impl
2934 }
2935}
2936
2937fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
2941 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
2942 let col_lit = column.as_str();
2943 quote! {
2944 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
2945 _returning_row,
2946 #col_lit,
2947 )?;
2948 }
2949 });
2950 quote! { #( #lines )* }
2951}
2952
2953fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
2955 let lines = entries.iter().map(|e| {
2956 let ident = &e.ident;
2957 let col_ty = column_type_ident(ident);
2958 quote! {
2959 #[allow(non_upper_case_globals)]
2960 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
2961 }
2962 });
2963 quote! { #(#lines)* }
2964}
2965
2966fn column_module_tokens(
2969 module_ident: &syn::Ident,
2970 struct_name: &syn::Ident,
2971 entries: &[ColumnEntry],
2972) -> TokenStream2 {
2973 let items = entries.iter().map(|e| {
2974 let col_ty = column_type_ident(&e.ident);
2975 let value_ty = &e.value_ty;
2976 let name = &e.name;
2977 let column = &e.column;
2978 let field_type_tokens = &e.field_type_tokens;
2979 quote! {
2980 #[derive(::core::clone::Clone, ::core::marker::Copy)]
2981 pub struct #col_ty;
2982
2983 impl ::rustango::core::Column for #col_ty {
2984 type Model = super::#struct_name;
2985 type Value = #value_ty;
2986 const NAME: &'static str = #name;
2987 const COLUMN: &'static str = #column;
2988 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
2989 }
2990 }
2991 });
2992 quote! {
2993 #[doc(hidden)]
2994 #[allow(non_camel_case_types, non_snake_case)]
2995 pub mod #module_ident {
2996 #[allow(unused_imports)]
3001 use super::*;
3002 #(#items)*
3003 }
3004 }
3005}
3006
3007fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3008 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3009}
3010
3011fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3012 syn::Ident::new(
3013 &format!("__rustango_cols_{struct_name}"),
3014 struct_name.span(),
3015 )
3016}
3017
3018fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3019 quote! {
3030 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3031 for #struct_name
3032 {
3033 fn from_row(
3034 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3035 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3036 ::core::result::Result::Ok(Self {
3037 #( #from_row_inits ),*
3038 })
3039 }
3040 }
3041
3042 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3043 #( #from_row_inits ),*
3044 });
3045 }
3046}
3047
3048struct ContainerAttrs {
3049 table: Option<String>,
3050 display: Option<(String, proc_macro2::Span)>,
3051 app: Option<String>,
3058 admin: Option<AdminAttrs>,
3063 audit: Option<AuditAttrs>,
3069 permissions: bool,
3073 m2m: Vec<M2MAttr>,
3077 indexes: Vec<IndexAttr>,
3083 checks: Vec<CheckAttr>,
3086 composite_fks: Vec<CompositeFkAttr>,
3090 generic_fks: Vec<GenericFkAttr>,
3094 scope: Option<String>,
3100}
3101
3102struct IndexAttr {
3104 name: Option<String>,
3106 columns: Vec<String>,
3108 unique: bool,
3110}
3111
3112struct CheckAttr {
3114 name: String,
3115 expr: String,
3116}
3117
3118struct CompositeFkAttr {
3124 name: String,
3126 to: String,
3128 from: Vec<String>,
3130 on: Vec<String>,
3132}
3133
3134struct GenericFkAttr {
3139 name: String,
3141 ct_column: String,
3143 pk_column: String,
3145}
3146
3147struct M2MAttr {
3149 name: String,
3151 to: String,
3153 through: String,
3155 src: String,
3157 dst: String,
3159}
3160
3161#[derive(Default)]
3167struct AuditAttrs {
3168 track: Option<(Vec<String>, proc_macro2::Span)>,
3172}
3173
3174#[derive(Default)]
3179struct AdminAttrs {
3180 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3181 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3182 list_per_page: Option<usize>,
3183 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3184 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3185 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3186 actions: Option<(Vec<String>, proc_macro2::Span)>,
3189 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3193}
3194
3195fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3196 let mut out = ContainerAttrs {
3197 table: None,
3198 display: None,
3199 app: None,
3200 admin: None,
3201 audit: None,
3202 permissions: false,
3203 m2m: Vec::new(),
3204 indexes: Vec::new(),
3205 checks: Vec::new(),
3206 composite_fks: Vec::new(),
3207 generic_fks: Vec::new(),
3208 scope: None,
3209 };
3210 for attr in &input.attrs {
3211 if !attr.path().is_ident("rustango") {
3212 continue;
3213 }
3214 attr.parse_nested_meta(|meta| {
3215 if meta.path.is_ident("table") {
3216 let s: LitStr = meta.value()?.parse()?;
3217 out.table = Some(s.value());
3218 return Ok(());
3219 }
3220 if meta.path.is_ident("display") {
3221 let s: LitStr = meta.value()?.parse()?;
3222 out.display = Some((s.value(), s.span()));
3223 return Ok(());
3224 }
3225 if meta.path.is_ident("app") {
3226 let s: LitStr = meta.value()?.parse()?;
3227 out.app = Some(s.value());
3228 return Ok(());
3229 }
3230 if meta.path.is_ident("scope") {
3231 let s: LitStr = meta.value()?.parse()?;
3232 let val = s.value();
3233 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
3234 return Err(meta.error(format!(
3235 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
3236 )));
3237 }
3238 out.scope = Some(val);
3239 return Ok(());
3240 }
3241 if meta.path.is_ident("admin") {
3242 let mut admin = AdminAttrs::default();
3243 meta.parse_nested_meta(|inner| {
3244 if inner.path.is_ident("list_display") {
3245 let s: LitStr = inner.value()?.parse()?;
3246 admin.list_display =
3247 Some((split_field_list(&s.value()), s.span()));
3248 return Ok(());
3249 }
3250 if inner.path.is_ident("search_fields") {
3251 let s: LitStr = inner.value()?.parse()?;
3252 admin.search_fields =
3253 Some((split_field_list(&s.value()), s.span()));
3254 return Ok(());
3255 }
3256 if inner.path.is_ident("readonly_fields") {
3257 let s: LitStr = inner.value()?.parse()?;
3258 admin.readonly_fields =
3259 Some((split_field_list(&s.value()), s.span()));
3260 return Ok(());
3261 }
3262 if inner.path.is_ident("list_per_page") {
3263 let lit: syn::LitInt = inner.value()?.parse()?;
3264 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3265 return Ok(());
3266 }
3267 if inner.path.is_ident("ordering") {
3268 let s: LitStr = inner.value()?.parse()?;
3269 admin.ordering = Some((
3270 parse_ordering_list(&s.value()),
3271 s.span(),
3272 ));
3273 return Ok(());
3274 }
3275 if inner.path.is_ident("list_filter") {
3276 let s: LitStr = inner.value()?.parse()?;
3277 admin.list_filter =
3278 Some((split_field_list(&s.value()), s.span()));
3279 return Ok(());
3280 }
3281 if inner.path.is_ident("actions") {
3282 let s: LitStr = inner.value()?.parse()?;
3283 admin.actions =
3284 Some((split_field_list(&s.value()), s.span()));
3285 return Ok(());
3286 }
3287 if inner.path.is_ident("fieldsets") {
3288 let s: LitStr = inner.value()?.parse()?;
3289 admin.fieldsets =
3290 Some((parse_fieldset_list(&s.value()), s.span()));
3291 return Ok(());
3292 }
3293 Err(inner.error(
3294 "unknown admin attribute (supported: \
3295 `list_display`, `search_fields`, `readonly_fields`, \
3296 `list_filter`, `list_per_page`, `ordering`, `actions`, \
3297 `fieldsets`)",
3298 ))
3299 })?;
3300 out.admin = Some(admin);
3301 return Ok(());
3302 }
3303 if meta.path.is_ident("audit") {
3304 let mut audit = AuditAttrs::default();
3305 meta.parse_nested_meta(|inner| {
3306 if inner.path.is_ident("track") {
3307 let s: LitStr = inner.value()?.parse()?;
3308 audit.track =
3309 Some((split_field_list(&s.value()), s.span()));
3310 return Ok(());
3311 }
3312 Err(inner.error(
3313 "unknown audit attribute (supported: `track`)",
3314 ))
3315 })?;
3316 out.audit = Some(audit);
3317 return Ok(());
3318 }
3319 if meta.path.is_ident("permissions") {
3320 out.permissions = true;
3321 return Ok(());
3322 }
3323 if meta.path.is_ident("unique_together") {
3324 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3333 out.indexes.push(IndexAttr { name, columns, unique: true });
3334 return Ok(());
3335 }
3336 if meta.path.is_ident("index_together") {
3337 let (columns, name) = parse_together_attr(&meta, "index_together")?;
3343 out.indexes.push(IndexAttr { name, columns, unique: false });
3344 return Ok(());
3345 }
3346 if meta.path.is_ident("index") {
3347 let cols_lit: LitStr = meta.value()?.parse()?;
3355 let columns = split_field_list(&cols_lit.value());
3356 out.indexes.push(IndexAttr { name: None, columns, unique: false });
3357 return Ok(());
3358 }
3359 if meta.path.is_ident("check") {
3360 let mut name: Option<String> = None;
3362 let mut expr: Option<String> = None;
3363 meta.parse_nested_meta(|inner| {
3364 if inner.path.is_ident("name") {
3365 let s: LitStr = inner.value()?.parse()?;
3366 name = Some(s.value());
3367 return Ok(());
3368 }
3369 if inner.path.is_ident("expr") {
3370 let s: LitStr = inner.value()?.parse()?;
3371 expr = Some(s.value());
3372 return Ok(());
3373 }
3374 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3375 })?;
3376 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3377 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3378 out.checks.push(CheckAttr { name, expr });
3379 return Ok(());
3380 }
3381 if meta.path.is_ident("generic_fk") {
3382 let mut gfk = GenericFkAttr {
3383 name: String::new(),
3384 ct_column: String::new(),
3385 pk_column: String::new(),
3386 };
3387 meta.parse_nested_meta(|inner| {
3388 if inner.path.is_ident("name") {
3389 let s: LitStr = inner.value()?.parse()?;
3390 gfk.name = s.value();
3391 return Ok(());
3392 }
3393 if inner.path.is_ident("ct_column") {
3394 let s: LitStr = inner.value()?.parse()?;
3395 gfk.ct_column = s.value();
3396 return Ok(());
3397 }
3398 if inner.path.is_ident("pk_column") {
3399 let s: LitStr = inner.value()?.parse()?;
3400 gfk.pk_column = s.value();
3401 return Ok(());
3402 }
3403 Err(inner.error(
3404 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3405 ))
3406 })?;
3407 if gfk.name.is_empty() {
3408 return Err(meta.error("generic_fk requires `name = \"...\"`"));
3409 }
3410 if gfk.ct_column.is_empty() {
3411 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3412 }
3413 if gfk.pk_column.is_empty() {
3414 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3415 }
3416 out.generic_fks.push(gfk);
3417 return Ok(());
3418 }
3419 if meta.path.is_ident("fk_composite") {
3420 let mut fk = CompositeFkAttr {
3421 name: String::new(),
3422 to: String::new(),
3423 from: Vec::new(),
3424 on: Vec::new(),
3425 };
3426 meta.parse_nested_meta(|inner| {
3427 if inner.path.is_ident("name") {
3428 let s: LitStr = inner.value()?.parse()?;
3429 fk.name = s.value();
3430 return Ok(());
3431 }
3432 if inner.path.is_ident("to") {
3433 let s: LitStr = inner.value()?.parse()?;
3434 fk.to = s.value();
3435 return Ok(());
3436 }
3437 if inner.path.is_ident("on") || inner.path.is_ident("from") {
3440 let value = inner.value()?;
3441 let content;
3442 syn::parenthesized!(content in value);
3443 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3444 content.parse_terminated(
3445 |p| p.parse::<syn::LitStr>(),
3446 syn::Token![,],
3447 )?;
3448 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3449 if inner.path.is_ident("on") {
3450 fk.on = cols;
3451 } else {
3452 fk.from = cols;
3453 }
3454 return Ok(());
3455 }
3456 Err(inner.error(
3457 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3458 ))
3459 })?;
3460 if fk.name.is_empty() {
3461 return Err(meta.error("fk_composite requires `name = \"...\"`"));
3462 }
3463 if fk.to.is_empty() {
3464 return Err(meta.error("fk_composite requires `to = \"...\"`"));
3465 }
3466 if fk.from.is_empty() || fk.on.is_empty() {
3467 return Err(meta.error(
3468 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3469 ));
3470 }
3471 if fk.from.len() != fk.on.len() {
3472 return Err(meta.error(format!(
3473 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3474 fk.from.len(),
3475 fk.on.len(),
3476 )));
3477 }
3478 out.composite_fks.push(fk);
3479 return Ok(());
3480 }
3481 if meta.path.is_ident("m2m") {
3482 let mut m2m = M2MAttr {
3483 name: String::new(),
3484 to: String::new(),
3485 through: String::new(),
3486 src: String::new(),
3487 dst: String::new(),
3488 };
3489 meta.parse_nested_meta(|inner| {
3490 if inner.path.is_ident("name") {
3491 let s: LitStr = inner.value()?.parse()?;
3492 m2m.name = s.value();
3493 return Ok(());
3494 }
3495 if inner.path.is_ident("to") {
3496 let s: LitStr = inner.value()?.parse()?;
3497 m2m.to = s.value();
3498 return Ok(());
3499 }
3500 if inner.path.is_ident("through") {
3501 let s: LitStr = inner.value()?.parse()?;
3502 m2m.through = s.value();
3503 return Ok(());
3504 }
3505 if inner.path.is_ident("src") {
3506 let s: LitStr = inner.value()?.parse()?;
3507 m2m.src = s.value();
3508 return Ok(());
3509 }
3510 if inner.path.is_ident("dst") {
3511 let s: LitStr = inner.value()?.parse()?;
3512 m2m.dst = s.value();
3513 return Ok(());
3514 }
3515 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3516 })?;
3517 if m2m.name.is_empty() {
3518 return Err(meta.error("m2m requires `name = \"...\"`"));
3519 }
3520 if m2m.to.is_empty() {
3521 return Err(meta.error("m2m requires `to = \"...\"`"));
3522 }
3523 if m2m.through.is_empty() {
3524 return Err(meta.error("m2m requires `through = \"...\"`"));
3525 }
3526 if m2m.src.is_empty() {
3527 return Err(meta.error("m2m requires `src = \"...\"`"));
3528 }
3529 if m2m.dst.is_empty() {
3530 return Err(meta.error("m2m requires `dst = \"...\"`"));
3531 }
3532 out.m2m.push(m2m);
3533 return Ok(());
3534 }
3535 Err(meta.error("unknown rustango container attribute"))
3536 })?;
3537 }
3538 Ok(out)
3539}
3540
3541fn split_field_list(raw: &str) -> Vec<String> {
3545 raw.split(',')
3546 .map(str::trim)
3547 .filter(|s| !s.is_empty())
3548 .map(str::to_owned)
3549 .collect()
3550}
3551
3552fn parse_together_attr(
3560 meta: &syn::meta::ParseNestedMeta<'_>,
3561 attr: &str,
3562) -> syn::Result<(Vec<String>, Option<String>)> {
3563 if meta.input.peek(syn::Token![=]) {
3566 let cols_lit: LitStr = meta.value()?.parse()?;
3567 let columns = split_field_list(&cols_lit.value());
3568 check_together_columns(meta, attr, &columns)?;
3569 return Ok((columns, None));
3570 }
3571 let mut columns: Option<Vec<String>> = None;
3572 let mut name: Option<String> = None;
3573 meta.parse_nested_meta(|inner| {
3574 if inner.path.is_ident("columns") {
3575 let s: LitStr = inner.value()?.parse()?;
3576 columns = Some(split_field_list(&s.value()));
3577 return Ok(());
3578 }
3579 if inner.path.is_ident("name") {
3580 let s: LitStr = inner.value()?.parse()?;
3581 name = Some(s.value());
3582 return Ok(());
3583 }
3584 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3585 })?;
3586 let columns = columns.ok_or_else(|| meta.error(format!(
3587 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3588 )))?;
3589 check_together_columns(meta, attr, &columns)?;
3590 Ok((columns, name))
3591}
3592
3593fn check_together_columns(
3594 meta: &syn::meta::ParseNestedMeta<'_>,
3595 attr: &str,
3596 columns: &[String],
3597) -> syn::Result<()> {
3598 if columns.len() < 2 {
3599 let single = if attr == "unique_together" {
3600 "#[rustango(unique)] on the field"
3601 } else {
3602 "#[rustango(index)] on the field"
3603 };
3604 return Err(meta.error(format!(
3605 "{attr} expects two or more columns; for a single-column equivalent use {single}",
3606 )));
3607 }
3608 Ok(())
3609}
3610
3611fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3620 raw.split('|')
3621 .map(str::trim)
3622 .filter(|s| !s.is_empty())
3623 .map(|section| {
3624 let (title, rest) = match section.split_once(':') {
3626 Some((title, rest)) if !title.contains(',') => {
3627 (title.trim().to_owned(), rest)
3628 }
3629 _ => (String::new(), section),
3630 };
3631 let fields = split_field_list(rest);
3632 (title, fields)
3633 })
3634 .collect()
3635}
3636
3637fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3640 raw.split(',')
3641 .map(str::trim)
3642 .filter(|s| !s.is_empty())
3643 .map(|spec| {
3644 spec.strip_prefix('-')
3645 .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
3646 })
3647 .collect()
3648}
3649
3650struct FieldAttrs {
3651 column: Option<String>,
3652 primary_key: bool,
3653 fk: Option<String>,
3654 o2o: Option<String>,
3655 on: Option<String>,
3656 max_length: Option<u32>,
3657 min: Option<i64>,
3658 max: Option<i64>,
3659 default: Option<String>,
3660 auto_uuid: bool,
3666 auto_now_add: bool,
3671 auto_now: bool,
3677 soft_delete: bool,
3682 unique: bool,
3685 index: bool,
3689 index_unique: bool,
3690 index_name: Option<String>,
3691}
3692
3693fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3694 let mut out = FieldAttrs {
3695 column: None,
3696 primary_key: false,
3697 fk: None,
3698 o2o: None,
3699 on: None,
3700 max_length: None,
3701 min: None,
3702 max: None,
3703 default: None,
3704 auto_uuid: false,
3705 auto_now_add: false,
3706 auto_now: false,
3707 soft_delete: false,
3708 unique: false,
3709 index: false,
3710 index_unique: false,
3711 index_name: None,
3712 };
3713 for attr in &field.attrs {
3714 if !attr.path().is_ident("rustango") {
3715 continue;
3716 }
3717 attr.parse_nested_meta(|meta| {
3718 if meta.path.is_ident("column") {
3719 let s: LitStr = meta.value()?.parse()?;
3720 out.column = Some(s.value());
3721 return Ok(());
3722 }
3723 if meta.path.is_ident("primary_key") {
3724 out.primary_key = true;
3725 return Ok(());
3726 }
3727 if meta.path.is_ident("fk") {
3728 let s: LitStr = meta.value()?.parse()?;
3729 out.fk = Some(s.value());
3730 return Ok(());
3731 }
3732 if meta.path.is_ident("o2o") {
3733 let s: LitStr = meta.value()?.parse()?;
3734 out.o2o = Some(s.value());
3735 return Ok(());
3736 }
3737 if meta.path.is_ident("on") {
3738 let s: LitStr = meta.value()?.parse()?;
3739 out.on = Some(s.value());
3740 return Ok(());
3741 }
3742 if meta.path.is_ident("max_length") {
3743 let lit: syn::LitInt = meta.value()?.parse()?;
3744 out.max_length = Some(lit.base10_parse::<u32>()?);
3745 return Ok(());
3746 }
3747 if meta.path.is_ident("min") {
3748 out.min = Some(parse_signed_i64(&meta)?);
3749 return Ok(());
3750 }
3751 if meta.path.is_ident("max") {
3752 out.max = Some(parse_signed_i64(&meta)?);
3753 return Ok(());
3754 }
3755 if meta.path.is_ident("default") {
3756 let s: LitStr = meta.value()?.parse()?;
3757 out.default = Some(s.value());
3758 return Ok(());
3759 }
3760 if meta.path.is_ident("auto_uuid") {
3761 out.auto_uuid = true;
3762 out.primary_key = true;
3766 if out.default.is_none() {
3767 out.default = Some("gen_random_uuid()".into());
3768 }
3769 return Ok(());
3770 }
3771 if meta.path.is_ident("auto_now_add") {
3772 out.auto_now_add = true;
3773 if out.default.is_none() {
3774 out.default = Some("now()".into());
3775 }
3776 return Ok(());
3777 }
3778 if meta.path.is_ident("auto_now") {
3779 out.auto_now = true;
3780 if out.default.is_none() {
3781 out.default = Some("now()".into());
3782 }
3783 return Ok(());
3784 }
3785 if meta.path.is_ident("soft_delete") {
3786 out.soft_delete = true;
3787 return Ok(());
3788 }
3789 if meta.path.is_ident("unique") {
3790 out.unique = true;
3791 return Ok(());
3792 }
3793 if meta.path.is_ident("index") {
3794 out.index = true;
3795 if meta.input.peek(syn::token::Paren) {
3797 meta.parse_nested_meta(|inner| {
3798 if inner.path.is_ident("unique") {
3799 out.index_unique = true;
3800 return Ok(());
3801 }
3802 if inner.path.is_ident("name") {
3803 let s: LitStr = inner.value()?.parse()?;
3804 out.index_name = Some(s.value());
3805 return Ok(());
3806 }
3807 Err(inner.error("unknown index sub-attribute (supported: `unique`, `name`)"))
3808 })?;
3809 }
3810 return Ok(());
3811 }
3812 Err(meta.error("unknown rustango field attribute"))
3813 })?;
3814 }
3815 Ok(out)
3816}
3817
3818fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
3820 let expr: syn::Expr = meta.value()?.parse()?;
3821 match expr {
3822 syn::Expr::Lit(syn::ExprLit {
3823 lit: syn::Lit::Int(lit),
3824 ..
3825 }) => lit.base10_parse::<i64>(),
3826 syn::Expr::Unary(syn::ExprUnary {
3827 op: syn::UnOp::Neg(_),
3828 expr,
3829 ..
3830 }) => {
3831 if let syn::Expr::Lit(syn::ExprLit {
3832 lit: syn::Lit::Int(lit),
3833 ..
3834 }) = *expr
3835 {
3836 let v: i64 = lit.base10_parse()?;
3837 Ok(-v)
3838 } else {
3839 Err(syn::Error::new_spanned(expr, "expected integer literal"))
3840 }
3841 }
3842 other => Err(syn::Error::new_spanned(
3843 other,
3844 "expected integer literal (signed)",
3845 )),
3846 }
3847}
3848
3849struct FieldInfo<'a> {
3850 ident: &'a syn::Ident,
3851 column: String,
3852 primary_key: bool,
3853 auto: bool,
3857 value_ty: &'a Type,
3860 field_type_tokens: TokenStream2,
3862 schema: TokenStream2,
3863 from_row_init: TokenStream2,
3864 from_aliased_row_init: TokenStream2,
3870 fk_inner: Option<Type>,
3874 fk_pk_kind: DetectedKind,
3880 nullable: bool,
3888 auto_now: bool,
3894 auto_now_add: bool,
3900 soft_delete: bool,
3905}
3906
3907fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
3908 let attrs = parse_field_attrs(field)?;
3909 let ident = field
3910 .ident
3911 .as_ref()
3912 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
3913 let name = ident.to_string();
3914 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
3915 let primary_key = attrs.primary_key;
3916 let DetectedType {
3917 kind,
3918 nullable,
3919 auto: detected_auto,
3920 fk_inner,
3921 } = detect_type(&field.ty)?;
3922 check_bound_compatibility(field, &attrs, kind)?;
3923 let auto = detected_auto;
3924 if attrs.auto_uuid {
3930 if kind != DetectedKind::Uuid {
3931 return Err(syn::Error::new_spanned(
3932 field,
3933 "`#[rustango(auto_uuid)]` requires the field type to be \
3934 `Auto<uuid::Uuid>`",
3935 ));
3936 }
3937 if !detected_auto {
3938 return Err(syn::Error::new_spanned(
3939 field,
3940 "`#[rustango(auto_uuid)]` requires the field type to be \
3941 wrapped in `Auto<...>` so the macro skips the column on \
3942 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
3943 ));
3944 }
3945 }
3946 if attrs.auto_now_add || attrs.auto_now {
3947 if kind != DetectedKind::DateTime {
3948 return Err(syn::Error::new_spanned(
3949 field,
3950 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
3951 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
3952 ));
3953 }
3954 if !detected_auto {
3955 return Err(syn::Error::new_spanned(
3956 field,
3957 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
3958 the field type to be wrapped in `Auto<...>` so the macro skips \
3959 the column on INSERT and the DB DEFAULT (`now()`) fires",
3960 ));
3961 }
3962 }
3963 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
3964 return Err(syn::Error::new_spanned(
3965 field,
3966 "`#[rustango(soft_delete)]` requires the field type to be \
3967 `Option<chrono::DateTime<chrono::Utc>>`",
3968 ));
3969 }
3970 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
3971 if detected_auto && !primary_key && !is_mixin_auto {
3972 return Err(syn::Error::new_spanned(
3973 field,
3974 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
3975 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
3976 `auto_now`",
3977 ));
3978 }
3979 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
3980 return Err(syn::Error::new_spanned(
3981 field,
3982 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
3983 SERIAL / BIGSERIAL already supplies a default sequence.",
3984 ));
3985 }
3986 if fk_inner.is_some() && primary_key {
3987 return Err(syn::Error::new_spanned(
3988 field,
3989 "`ForeignKey<T>` is not allowed on a primary-key field — \
3990 a row's PK is its own identity, not a reference to a parent.",
3991 ));
3992 }
3993 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
3994 let column_lit = column.as_str();
3995 let field_type_tokens = kind.variant_tokens();
3996 let max_length = optional_u32(attrs.max_length);
3997 let min = optional_i64(attrs.min);
3998 let max = optional_i64(attrs.max);
3999 let default = optional_str(attrs.default.as_deref());
4000
4001 let unique = attrs.unique;
4002 let schema = quote! {
4003 ::rustango::core::FieldSchema {
4004 name: #name,
4005 column: #column_lit,
4006 ty: #field_type_tokens,
4007 nullable: #nullable,
4008 primary_key: #primary_key,
4009 relation: #relation,
4010 max_length: #max_length,
4011 min: #min,
4012 max: #max,
4013 default: #default,
4014 auto: #auto,
4015 unique: #unique,
4016 }
4017 };
4018
4019 let from_row_init = quote! {
4020 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4021 };
4022 let from_aliased_row_init = quote! {
4023 #ident: ::rustango::sql::sqlx::Row::try_get(
4024 row,
4025 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4026 )?
4027 };
4028
4029 Ok(FieldInfo {
4030 ident,
4031 column,
4032 primary_key,
4033 auto,
4034 value_ty: &field.ty,
4035 field_type_tokens,
4036 schema,
4037 from_row_init,
4038 from_aliased_row_init,
4039 fk_inner: fk_inner.cloned(),
4040 fk_pk_kind: kind,
4041 nullable,
4042 auto_now: attrs.auto_now,
4043 auto_now_add: attrs.auto_now_add,
4044 soft_delete: attrs.soft_delete,
4045 })
4046}
4047
4048fn check_bound_compatibility(
4049 field: &syn::Field,
4050 attrs: &FieldAttrs,
4051 kind: DetectedKind,
4052) -> syn::Result<()> {
4053 if attrs.max_length.is_some() && kind != DetectedKind::String {
4054 return Err(syn::Error::new_spanned(
4055 field,
4056 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4057 ));
4058 }
4059 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4060 return Err(syn::Error::new_spanned(
4061 field,
4062 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4063 ));
4064 }
4065 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4066 if min > max {
4067 return Err(syn::Error::new_spanned(
4068 field,
4069 format!("`min` ({min}) is greater than `max` ({max})"),
4070 ));
4071 }
4072 }
4073 Ok(())
4074}
4075
4076fn optional_u32(value: Option<u32>) -> TokenStream2 {
4077 if let Some(v) = value {
4078 quote!(::core::option::Option::Some(#v))
4079 } else {
4080 quote!(::core::option::Option::None)
4081 }
4082}
4083
4084fn optional_i64(value: Option<i64>) -> TokenStream2 {
4085 if let Some(v) = value {
4086 quote!(::core::option::Option::Some(#v))
4087 } else {
4088 quote!(::core::option::Option::None)
4089 }
4090}
4091
4092fn optional_str(value: Option<&str>) -> TokenStream2 {
4093 if let Some(v) = value {
4094 quote!(::core::option::Option::Some(#v))
4095 } else {
4096 quote!(::core::option::Option::None)
4097 }
4098}
4099
4100fn relation_tokens(
4101 field: &syn::Field,
4102 attrs: &FieldAttrs,
4103 fk_inner: Option<&syn::Type>,
4104 table: &str,
4105) -> syn::Result<TokenStream2> {
4106 if let Some(inner) = fk_inner {
4107 if attrs.fk.is_some() || attrs.o2o.is_some() {
4108 return Err(syn::Error::new_spanned(
4109 field,
4110 "`ForeignKey<T>` already declares the FK target via the type parameter — \
4111 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4112 ));
4113 }
4114 let on = attrs.on.as_deref().unwrap_or("id");
4115 return Ok(quote! {
4116 ::core::option::Option::Some(::rustango::core::Relation::Fk {
4117 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4118 on: #on,
4119 })
4120 });
4121 }
4122 match (&attrs.fk, &attrs.o2o) {
4123 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4124 field,
4125 "`fk` and `o2o` are mutually exclusive",
4126 )),
4127 (Some(to), None) => {
4128 let on = attrs.on.as_deref().unwrap_or("id");
4129 let resolved = if to == "self" { table } else { to };
4135 Ok(quote! {
4136 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4137 })
4138 }
4139 (None, Some(to)) => {
4140 let on = attrs.on.as_deref().unwrap_or("id");
4141 let resolved = if to == "self" { table } else { to };
4142 Ok(quote! {
4143 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4144 })
4145 }
4146 (None, None) => {
4147 if attrs.on.is_some() {
4148 return Err(syn::Error::new_spanned(
4149 field,
4150 "`on` requires `fk` or `o2o`",
4151 ));
4152 }
4153 Ok(quote!(::core::option::Option::None))
4154 }
4155 }
4156}
4157
4158#[derive(Clone, Copy, PartialEq, Eq)]
4162enum DetectedKind {
4163 I16,
4164 I32,
4165 I64,
4166 F32,
4167 F64,
4168 Bool,
4169 String,
4170 DateTime,
4171 Date,
4172 Uuid,
4173 Json,
4174}
4175
4176impl DetectedKind {
4177 fn variant_tokens(self) -> TokenStream2 {
4178 match self {
4179 Self::I16 => quote!(::rustango::core::FieldType::I16),
4180 Self::I32 => quote!(::rustango::core::FieldType::I32),
4181 Self::I64 => quote!(::rustango::core::FieldType::I64),
4182 Self::F32 => quote!(::rustango::core::FieldType::F32),
4183 Self::F64 => quote!(::rustango::core::FieldType::F64),
4184 Self::Bool => quote!(::rustango::core::FieldType::Bool),
4185 Self::String => quote!(::rustango::core::FieldType::String),
4186 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4187 Self::Date => quote!(::rustango::core::FieldType::Date),
4188 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4189 Self::Json => quote!(::rustango::core::FieldType::Json),
4190 }
4191 }
4192
4193 fn is_integer(self) -> bool {
4194 matches!(self, Self::I16 | Self::I32 | Self::I64)
4195 }
4196
4197 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4205 match self {
4206 Self::I16 => (quote!(I16), quote!(0i16)),
4207 Self::I32 => (quote!(I32), quote!(0i32)),
4208 Self::I64 => (quote!(I64), quote!(0i64)),
4209 Self::F32 => (quote!(F32), quote!(0f32)),
4210 Self::F64 => (quote!(F64), quote!(0f64)),
4211 Self::Bool => (quote!(Bool), quote!(false)),
4212 Self::String => (quote!(String), quote!(::std::string::String::new())),
4213 Self::DateTime => (
4214 quote!(DateTime),
4215 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4216 ),
4217 Self::Date => (
4218 quote!(Date),
4219 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4220 ),
4221 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4222 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4223 }
4224 }
4225}
4226
4227#[derive(Clone, Copy)]
4233struct DetectedType<'a> {
4234 kind: DetectedKind,
4235 nullable: bool,
4236 auto: bool,
4237 fk_inner: Option<&'a syn::Type>,
4238}
4239
4240fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4245 let Type::Path(TypePath { path, qself: None }) = ty else {
4246 return None;
4247 };
4248 let last = path.segments.last()?;
4249 if last.ident != "Auto" {
4250 return None;
4251 }
4252 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4253 return None;
4254 };
4255 args.args.iter().find_map(|a| match a {
4256 syn::GenericArgument::Type(t) => Some(t),
4257 _ => None,
4258 })
4259}
4260
4261fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4262 let Type::Path(TypePath { path, qself: None }) = ty else {
4263 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4264 };
4265 let last = path
4266 .segments
4267 .last()
4268 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4269
4270 if last.ident == "Option" {
4271 let inner = generic_inner(ty, &last.arguments, "Option")?;
4272 let inner_det = detect_type(inner)?;
4273 if inner_det.nullable {
4274 return Err(syn::Error::new_spanned(
4275 ty,
4276 "nested Option is not supported",
4277 ));
4278 }
4279 if inner_det.auto {
4280 return Err(syn::Error::new_spanned(
4281 ty,
4282 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4283 ));
4284 }
4285 return Ok(DetectedType {
4286 nullable: true,
4287 ..inner_det
4288 });
4289 }
4290
4291 if last.ident == "Auto" {
4292 let inner = generic_inner(ty, &last.arguments, "Auto")?;
4293 let inner_det = detect_type(inner)?;
4294 if inner_det.auto {
4295 return Err(syn::Error::new_spanned(
4296 ty,
4297 "nested Auto is not supported",
4298 ));
4299 }
4300 if inner_det.nullable {
4301 return Err(syn::Error::new_spanned(
4302 ty,
4303 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4304 ));
4305 }
4306 if inner_det.fk_inner.is_some() {
4307 return Err(syn::Error::new_spanned(
4308 ty,
4309 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4310 ));
4311 }
4312 if !matches!(
4313 inner_det.kind,
4314 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4315 ) {
4316 return Err(syn::Error::new_spanned(
4317 ty,
4318 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4319 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4320 (DEFAULT now())",
4321 ));
4322 }
4323 return Ok(DetectedType {
4324 auto: true,
4325 ..inner_det
4326 });
4327 }
4328
4329 if last.ident == "ForeignKey" {
4330 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4331 let kind = match key_ty {
4339 Some(k) => detect_type(k)?.kind,
4340 None => DetectedKind::I64,
4341 };
4342 return Ok(DetectedType {
4343 kind,
4344 nullable: false,
4345 auto: false,
4346 fk_inner: Some(inner),
4347 });
4348 }
4349
4350 let kind = match last.ident.to_string().as_str() {
4351 "i16" => DetectedKind::I16,
4352 "i32" => DetectedKind::I32,
4353 "i64" => DetectedKind::I64,
4354 "f32" => DetectedKind::F32,
4355 "f64" => DetectedKind::F64,
4356 "bool" => DetectedKind::Bool,
4357 "String" => DetectedKind::String,
4358 "DateTime" => DetectedKind::DateTime,
4359 "NaiveDate" => DetectedKind::Date,
4360 "Uuid" => DetectedKind::Uuid,
4361 "Value" => DetectedKind::Json,
4362 other => {
4363 return Err(syn::Error::new_spanned(
4364 ty,
4365 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)"),
4366 ));
4367 }
4368 };
4369 Ok(DetectedType {
4370 kind,
4371 nullable: false,
4372 auto: false,
4373 fk_inner: None,
4374 })
4375}
4376
4377fn generic_inner<'a>(
4378 ty: &'a Type,
4379 arguments: &'a PathArguments,
4380 wrapper: &str,
4381) -> syn::Result<&'a Type> {
4382 let PathArguments::AngleBracketed(args) = arguments else {
4383 return Err(syn::Error::new_spanned(
4384 ty,
4385 format!("{wrapper} requires a generic argument"),
4386 ));
4387 };
4388 args.args
4389 .iter()
4390 .find_map(|a| match a {
4391 GenericArgument::Type(t) => Some(t),
4392 _ => None,
4393 })
4394 .ok_or_else(|| {
4395 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4396 })
4397}
4398
4399fn generic_pair<'a>(
4403 ty: &'a Type,
4404 arguments: &'a PathArguments,
4405 wrapper: &str,
4406) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4407 let PathArguments::AngleBracketed(args) = arguments else {
4408 return Err(syn::Error::new_spanned(
4409 ty,
4410 format!("{wrapper} requires a generic argument"),
4411 ));
4412 };
4413 let mut types = args.args.iter().filter_map(|a| match a {
4414 GenericArgument::Type(t) => Some(t),
4415 _ => None,
4416 });
4417 let first = types.next().ok_or_else(|| {
4418 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4419 })?;
4420 let second = types.next();
4421 Ok((first, second))
4422}
4423
4424fn to_snake_case(s: &str) -> String {
4425 let mut out = String::with_capacity(s.len() + 4);
4426 for (i, ch) in s.chars().enumerate() {
4427 if ch.is_ascii_uppercase() {
4428 if i > 0 {
4429 out.push('_');
4430 }
4431 out.push(ch.to_ascii_lowercase());
4432 } else {
4433 out.push(ch);
4434 }
4435 }
4436 out
4437}
4438
4439#[derive(Default)]
4445struct FormFieldAttrs {
4446 min: Option<i64>,
4447 max: Option<i64>,
4448 min_length: Option<u32>,
4449 max_length: Option<u32>,
4450}
4451
4452#[derive(Clone, Copy)]
4454enum FormFieldKind {
4455 String,
4456 I16,
4457 I32,
4458 I64,
4459 F32,
4460 F64,
4461 Bool,
4462}
4463
4464impl FormFieldKind {
4465 fn parse_method(self) -> &'static str {
4466 match self {
4467 Self::I16 => "i16",
4468 Self::I32 => "i32",
4469 Self::I64 => "i64",
4470 Self::F32 => "f32",
4471 Self::F64 => "f64",
4472 Self::String | Self::Bool => "",
4475 }
4476 }
4477}
4478
4479fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4480 let struct_name = &input.ident;
4481
4482 let Data::Struct(data) = &input.data else {
4483 return Err(syn::Error::new_spanned(
4484 struct_name,
4485 "Form can only be derived on structs",
4486 ));
4487 };
4488 let Fields::Named(named) = &data.fields else {
4489 return Err(syn::Error::new_spanned(
4490 struct_name,
4491 "Form requires a struct with named fields",
4492 ));
4493 };
4494
4495 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4496 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4497
4498 for field in &named.named {
4499 let ident = field
4500 .ident
4501 .as_ref()
4502 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4503 let attrs = parse_form_field_attrs(field)?;
4504 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4505
4506 let name_lit = ident.to_string();
4507 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4508 field_blocks.push(parse_block);
4509 field_idents.push(ident);
4510 }
4511
4512 Ok(quote! {
4513 impl ::rustango::forms::Form for #struct_name {
4514 fn parse(
4515 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4516 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4517 let mut __errors = ::rustango::forms::FormErrors::default();
4518 #( #field_blocks )*
4519 if !__errors.is_empty() {
4520 return ::core::result::Result::Err(__errors);
4521 }
4522 ::core::result::Result::Ok(Self {
4523 #( #field_idents ),*
4524 })
4525 }
4526 }
4527 })
4528}
4529
4530fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4531 let mut out = FormFieldAttrs::default();
4532 for attr in &field.attrs {
4533 if !attr.path().is_ident("form") {
4534 continue;
4535 }
4536 attr.parse_nested_meta(|meta| {
4537 if meta.path.is_ident("min") {
4538 let lit: syn::LitInt = meta.value()?.parse()?;
4539 out.min = Some(lit.base10_parse::<i64>()?);
4540 return Ok(());
4541 }
4542 if meta.path.is_ident("max") {
4543 let lit: syn::LitInt = meta.value()?.parse()?;
4544 out.max = Some(lit.base10_parse::<i64>()?);
4545 return Ok(());
4546 }
4547 if meta.path.is_ident("min_length") {
4548 let lit: syn::LitInt = meta.value()?.parse()?;
4549 out.min_length = Some(lit.base10_parse::<u32>()?);
4550 return Ok(());
4551 }
4552 if meta.path.is_ident("max_length") {
4553 let lit: syn::LitInt = meta.value()?.parse()?;
4554 out.max_length = Some(lit.base10_parse::<u32>()?);
4555 return Ok(());
4556 }
4557 Err(meta.error(
4558 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4559 ))
4560 })?;
4561 }
4562 Ok(out)
4563}
4564
4565fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4566 let Type::Path(TypePath { path, qself: None }) = ty else {
4567 return Err(syn::Error::new(
4568 span,
4569 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4570 ));
4571 };
4572 let last = path
4573 .segments
4574 .last()
4575 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4576
4577 if last.ident == "Option" {
4578 let inner = generic_inner(ty, &last.arguments, "Option")?;
4579 let (kind, nested) = detect_form_field(inner, span)?;
4580 if nested {
4581 return Err(syn::Error::new(
4582 span,
4583 "nested Option in Form fields is not supported",
4584 ));
4585 }
4586 return Ok((kind, true));
4587 }
4588
4589 let kind = match last.ident.to_string().as_str() {
4590 "String" => FormFieldKind::String,
4591 "i16" => FormFieldKind::I16,
4592 "i32" => FormFieldKind::I32,
4593 "i64" => FormFieldKind::I64,
4594 "f32" => FormFieldKind::F32,
4595 "f64" => FormFieldKind::F64,
4596 "bool" => FormFieldKind::Bool,
4597 other => {
4598 return Err(syn::Error::new(
4599 span,
4600 format!(
4601 "Form field type `{other}` is not supported in v0.8 — use String / \
4602 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4603 ),
4604 ));
4605 }
4606 };
4607 Ok((kind, false))
4608}
4609
4610#[allow(clippy::too_many_lines)]
4611fn render_form_field_parse(
4612 ident: &syn::Ident,
4613 name_lit: &str,
4614 kind: FormFieldKind,
4615 nullable: bool,
4616 attrs: &FormFieldAttrs,
4617) -> TokenStream2 {
4618 let lookup = quote! {
4621 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4622 };
4623
4624 let parsed_value = match kind {
4625 FormFieldKind::Bool => quote! {
4626 let __v: bool = match __raw {
4627 ::core::option::Option::None => false,
4628 ::core::option::Option::Some(__s) => !matches!(
4629 __s.to_ascii_lowercase().as_str(),
4630 "" | "false" | "0" | "off" | "no"
4631 ),
4632 };
4633 },
4634 FormFieldKind::String => {
4635 if nullable {
4636 quote! {
4637 let __v: ::core::option::Option<::std::string::String> = match __raw {
4638 ::core::option::Option::None => ::core::option::Option::None,
4639 ::core::option::Option::Some(__s) if __s.is_empty() => {
4640 ::core::option::Option::None
4641 }
4642 ::core::option::Option::Some(__s) => {
4643 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4644 }
4645 };
4646 }
4647 } else {
4648 quote! {
4649 let __v: ::std::string::String = match __raw {
4650 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4651 ::core::clone::Clone::clone(__s)
4652 }
4653 _ => {
4654 __errors.add(#name_lit, "This field is required.");
4655 ::std::string::String::new()
4656 }
4657 };
4658 }
4659 }
4660 }
4661 FormFieldKind::I16
4662 | FormFieldKind::I32
4663 | FormFieldKind::I64
4664 | FormFieldKind::F32
4665 | FormFieldKind::F64 => {
4666 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
4667 let ty_lit = kind.parse_method();
4668 let default_val = match kind {
4669 FormFieldKind::I16 => quote! { 0i16 },
4670 FormFieldKind::I32 => quote! { 0i32 },
4671 FormFieldKind::I64 => quote! { 0i64 },
4672 FormFieldKind::F32 => quote! { 0f32 },
4673 FormFieldKind::F64 => quote! { 0f64 },
4674 _ => quote! { Default::default() },
4675 };
4676 if nullable {
4677 quote! {
4678 let __v: ::core::option::Option<#parse_ty> = match __raw {
4679 ::core::option::Option::None => ::core::option::Option::None,
4680 ::core::option::Option::Some(__s) if __s.is_empty() => {
4681 ::core::option::Option::None
4682 }
4683 ::core::option::Option::Some(__s) => {
4684 match __s.parse::<#parse_ty>() {
4685 ::core::result::Result::Ok(__n) => {
4686 ::core::option::Option::Some(__n)
4687 }
4688 ::core::result::Result::Err(__e) => {
4689 __errors.add(
4690 #name_lit,
4691 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4692 );
4693 ::core::option::Option::None
4694 }
4695 }
4696 }
4697 };
4698 }
4699 } else {
4700 quote! {
4701 let __v: #parse_ty = match __raw {
4702 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4703 match __s.parse::<#parse_ty>() {
4704 ::core::result::Result::Ok(__n) => __n,
4705 ::core::result::Result::Err(__e) => {
4706 __errors.add(
4707 #name_lit,
4708 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4709 );
4710 #default_val
4711 }
4712 }
4713 }
4714 _ => {
4715 __errors.add(#name_lit, "This field is required.");
4716 #default_val
4717 }
4718 };
4719 }
4720 }
4721 }
4722 };
4723
4724 let validators = render_form_validators(name_lit, kind, nullable, attrs);
4725
4726 quote! {
4727 let #ident = {
4728 #lookup
4729 #parsed_value
4730 #validators
4731 __v
4732 };
4733 }
4734}
4735
4736fn render_form_validators(
4737 name_lit: &str,
4738 kind: FormFieldKind,
4739 nullable: bool,
4740 attrs: &FormFieldAttrs,
4741) -> TokenStream2 {
4742 let mut checks: Vec<TokenStream2> = Vec::new();
4743
4744 let val_ref = if nullable {
4745 quote! { __v.as_ref() }
4746 } else {
4747 quote! { ::core::option::Option::Some(&__v) }
4748 };
4749
4750 let is_string = matches!(kind, FormFieldKind::String);
4751 let is_numeric = matches!(
4752 kind,
4753 FormFieldKind::I16
4754 | FormFieldKind::I32
4755 | FormFieldKind::I64
4756 | FormFieldKind::F32
4757 | FormFieldKind::F64
4758 );
4759
4760 if is_string {
4761 if let Some(min_len) = attrs.min_length {
4762 let min_len_usize = min_len as usize;
4763 checks.push(quote! {
4764 if let ::core::option::Option::Some(__s) = #val_ref {
4765 if __s.len() < #min_len_usize {
4766 __errors.add(
4767 #name_lit,
4768 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
4769 );
4770 }
4771 }
4772 });
4773 }
4774 if let Some(max_len) = attrs.max_length {
4775 let max_len_usize = max_len as usize;
4776 checks.push(quote! {
4777 if let ::core::option::Option::Some(__s) = #val_ref {
4778 if __s.len() > #max_len_usize {
4779 __errors.add(
4780 #name_lit,
4781 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
4782 );
4783 }
4784 }
4785 });
4786 }
4787 }
4788
4789 if is_numeric {
4790 if let Some(min) = attrs.min {
4791 checks.push(quote! {
4792 if let ::core::option::Option::Some(__n) = #val_ref {
4793 if (*__n as f64) < (#min as f64) {
4794 __errors.add(
4795 #name_lit,
4796 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
4797 );
4798 }
4799 }
4800 });
4801 }
4802 if let Some(max) = attrs.max {
4803 checks.push(quote! {
4804 if let ::core::option::Option::Some(__n) = #val_ref {
4805 if (*__n as f64) > (#max as f64) {
4806 __errors.add(
4807 #name_lit,
4808 ::std::format!("Ensure this value is less than or equal to {}.", #max),
4809 );
4810 }
4811 }
4812 });
4813 }
4814 }
4815
4816 quote! { #( #checks )* }
4817}
4818
4819struct ViewSetAttrs {
4824 model: syn::Path,
4825 fields: Option<Vec<String>>,
4826 filter_fields: Vec<String>,
4827 search_fields: Vec<String>,
4828 ordering: Vec<(String, bool)>,
4830 page_size: Option<usize>,
4831 read_only: bool,
4832 perms: ViewSetPermsAttrs,
4833}
4834
4835#[derive(Default)]
4836struct ViewSetPermsAttrs {
4837 list: Vec<String>,
4838 retrieve: Vec<String>,
4839 create: Vec<String>,
4840 update: Vec<String>,
4841 destroy: Vec<String>,
4842}
4843
4844fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
4845 let struct_name = &input.ident;
4846
4847 match &input.data {
4849 Data::Struct(s) => match &s.fields {
4850 Fields::Unit | Fields::Named(_) => {}
4851 Fields::Unnamed(_) => {
4852 return Err(syn::Error::new_spanned(
4853 struct_name,
4854 "ViewSet can only be derived on a unit struct or an empty named struct",
4855 ));
4856 }
4857 },
4858 _ => {
4859 return Err(syn::Error::new_spanned(
4860 struct_name,
4861 "ViewSet can only be derived on a struct",
4862 ));
4863 }
4864 }
4865
4866 let attrs = parse_viewset_attrs(input)?;
4867 let model_path = &attrs.model;
4868
4869 let fields_call = if let Some(ref fields) = attrs.fields {
4871 let lits = fields.iter().map(|f| f.as_str());
4872 quote!(.fields(&[ #(#lits),* ]))
4873 } else {
4874 quote!()
4875 };
4876
4877 let filter_fields_call = if attrs.filter_fields.is_empty() {
4878 quote!()
4879 } else {
4880 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
4881 quote!(.filter_fields(&[ #(#lits),* ]))
4882 };
4883
4884 let search_fields_call = if attrs.search_fields.is_empty() {
4885 quote!()
4886 } else {
4887 let lits = attrs.search_fields.iter().map(|f| f.as_str());
4888 quote!(.search_fields(&[ #(#lits),* ]))
4889 };
4890
4891 let ordering_call = if attrs.ordering.is_empty() {
4892 quote!()
4893 } else {
4894 let pairs = attrs.ordering.iter().map(|(f, desc)| {
4895 let f = f.as_str();
4896 quote!((#f, #desc))
4897 });
4898 quote!(.ordering(&[ #(#pairs),* ]))
4899 };
4900
4901 let page_size_call = if let Some(n) = attrs.page_size {
4902 quote!(.page_size(#n))
4903 } else {
4904 quote!()
4905 };
4906
4907 let read_only_call = if attrs.read_only {
4908 quote!(.read_only())
4909 } else {
4910 quote!()
4911 };
4912
4913 let perms = &attrs.perms;
4914 let perms_call = if perms.list.is_empty()
4915 && perms.retrieve.is_empty()
4916 && perms.create.is_empty()
4917 && perms.update.is_empty()
4918 && perms.destroy.is_empty()
4919 {
4920 quote!()
4921 } else {
4922 let list_lits = perms.list.iter().map(|s| s.as_str());
4923 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
4924 let create_lits = perms.create.iter().map(|s| s.as_str());
4925 let update_lits = perms.update.iter().map(|s| s.as_str());
4926 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
4927 quote! {
4928 .permissions(::rustango::viewset::ViewSetPerms {
4929 list: ::std::vec![ #(#list_lits.to_owned()),* ],
4930 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
4931 create: ::std::vec![ #(#create_lits.to_owned()),* ],
4932 update: ::std::vec![ #(#update_lits.to_owned()),* ],
4933 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
4934 })
4935 }
4936 };
4937
4938 Ok(quote! {
4939 impl #struct_name {
4940 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
4943 ::rustango::viewset::ViewSet::for_model(#model_path::SCHEMA)
4944 #fields_call
4945 #filter_fields_call
4946 #search_fields_call
4947 #ordering_call
4948 #page_size_call
4949 #perms_call
4950 #read_only_call
4951 .router(prefix, pool)
4952 }
4953 }
4954 })
4955}
4956
4957fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
4958 let mut model: Option<syn::Path> = None;
4959 let mut fields: Option<Vec<String>> = None;
4960 let mut filter_fields: Vec<String> = Vec::new();
4961 let mut search_fields: Vec<String> = Vec::new();
4962 let mut ordering: Vec<(String, bool)> = Vec::new();
4963 let mut page_size: Option<usize> = None;
4964 let mut read_only = false;
4965 let mut perms = ViewSetPermsAttrs::default();
4966
4967 for attr in &input.attrs {
4968 if !attr.path().is_ident("viewset") {
4969 continue;
4970 }
4971 attr.parse_nested_meta(|meta| {
4972 if meta.path.is_ident("model") {
4973 let path: syn::Path = meta.value()?.parse()?;
4974 model = Some(path);
4975 return Ok(());
4976 }
4977 if meta.path.is_ident("fields") {
4978 let s: LitStr = meta.value()?.parse()?;
4979 fields = Some(split_field_list(&s.value()));
4980 return Ok(());
4981 }
4982 if meta.path.is_ident("filter_fields") {
4983 let s: LitStr = meta.value()?.parse()?;
4984 filter_fields = split_field_list(&s.value());
4985 return Ok(());
4986 }
4987 if meta.path.is_ident("search_fields") {
4988 let s: LitStr = meta.value()?.parse()?;
4989 search_fields = split_field_list(&s.value());
4990 return Ok(());
4991 }
4992 if meta.path.is_ident("ordering") {
4993 let s: LitStr = meta.value()?.parse()?;
4994 ordering = parse_ordering_list(&s.value());
4995 return Ok(());
4996 }
4997 if meta.path.is_ident("page_size") {
4998 let lit: syn::LitInt = meta.value()?.parse()?;
4999 page_size = Some(lit.base10_parse::<usize>()?);
5000 return Ok(());
5001 }
5002 if meta.path.is_ident("read_only") {
5003 read_only = true;
5004 return Ok(());
5005 }
5006 if meta.path.is_ident("permissions") {
5007 meta.parse_nested_meta(|inner| {
5008 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5009 let s: LitStr = inner.value()?.parse()?;
5010 Ok(split_field_list(&s.value()))
5011 };
5012 if inner.path.is_ident("list") {
5013 perms.list = parse_codenames(&inner)?;
5014 } else if inner.path.is_ident("retrieve") {
5015 perms.retrieve = parse_codenames(&inner)?;
5016 } else if inner.path.is_ident("create") {
5017 perms.create = parse_codenames(&inner)?;
5018 } else if inner.path.is_ident("update") {
5019 perms.update = parse_codenames(&inner)?;
5020 } else if inner.path.is_ident("destroy") {
5021 perms.destroy = parse_codenames(&inner)?;
5022 } else {
5023 return Err(inner.error(
5024 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5025 ));
5026 }
5027 Ok(())
5028 })?;
5029 return Ok(());
5030 }
5031 Err(meta.error(
5032 "unknown viewset attribute (supported: model, fields, filter_fields, \
5033 search_fields, ordering, page_size, read_only, permissions(...))",
5034 ))
5035 })?;
5036 }
5037
5038 let model = model.ok_or_else(|| {
5039 syn::Error::new_spanned(
5040 &input.ident,
5041 "`#[viewset(model = SomeModel)]` is required",
5042 )
5043 })?;
5044
5045 Ok(ViewSetAttrs {
5046 model,
5047 fields,
5048 filter_fields,
5049 search_fields,
5050 ordering,
5051 page_size,
5052 read_only,
5053 perms,
5054 })
5055}
5056
5057struct SerializerContainerAttrs {
5060 model: syn::Path,
5061}
5062
5063#[derive(Default)]
5064struct SerializerFieldAttrs {
5065 read_only: bool,
5066 write_only: bool,
5067 source: Option<String>,
5068 skip: bool,
5069 method: Option<String>,
5073 validate: Option<String>,
5078 nested: bool,
5088 nested_strict: bool,
5093 many: Option<syn::Type>,
5102}
5103
5104fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5105 let mut model: Option<syn::Path> = None;
5106 for attr in &input.attrs {
5107 if !attr.path().is_ident("serializer") {
5108 continue;
5109 }
5110 attr.parse_nested_meta(|meta| {
5111 if meta.path.is_ident("model") {
5112 let _eq: syn::Token![=] = meta.input.parse()?;
5113 model = Some(meta.input.parse()?);
5114 return Ok(());
5115 }
5116 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5117 })?;
5118 }
5119 let model = model.ok_or_else(|| {
5120 syn::Error::new_spanned(
5121 &input.ident,
5122 "`#[serializer(model = SomeModel)]` is required",
5123 )
5124 })?;
5125 Ok(SerializerContainerAttrs { model })
5126}
5127
5128fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5129 let mut out = SerializerFieldAttrs::default();
5130 for attr in &field.attrs {
5131 if !attr.path().is_ident("serializer") {
5132 continue;
5133 }
5134 attr.parse_nested_meta(|meta| {
5135 if meta.path.is_ident("read_only") {
5136 out.read_only = true;
5137 return Ok(());
5138 }
5139 if meta.path.is_ident("write_only") {
5140 out.write_only = true;
5141 return Ok(());
5142 }
5143 if meta.path.is_ident("skip") {
5144 out.skip = true;
5145 return Ok(());
5146 }
5147 if meta.path.is_ident("source") {
5148 let s: LitStr = meta.value()?.parse()?;
5149 out.source = Some(s.value());
5150 return Ok(());
5151 }
5152 if meta.path.is_ident("method") {
5153 let s: LitStr = meta.value()?.parse()?;
5154 out.method = Some(s.value());
5155 return Ok(());
5156 }
5157 if meta.path.is_ident("validate") {
5158 let s: LitStr = meta.value()?.parse()?;
5159 out.validate = Some(s.value());
5160 return Ok(());
5161 }
5162 if meta.path.is_ident("many") {
5163 let _eq: syn::Token![=] = meta.input.parse()?;
5164 out.many = Some(meta.input.parse()?);
5165 return Ok(());
5166 }
5167 if meta.path.is_ident("nested") {
5168 out.nested = true;
5169 if meta.input.peek(syn::token::Paren) {
5172 meta.parse_nested_meta(|inner| {
5173 if inner.path.is_ident("strict") {
5174 out.nested_strict = true;
5175 return Ok(());
5176 }
5177 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5178 })?;
5179 }
5180 return Ok(());
5181 }
5182 Err(meta.error(
5183 "unknown serializer field attribute (supported: \
5184 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5185 ))
5186 })?;
5187 }
5188 if out.read_only && out.write_only {
5190 return Err(syn::Error::new_spanned(
5191 field,
5192 "a field cannot be both `read_only` and `write_only`",
5193 ));
5194 }
5195 if out.method.is_some() && out.source.is_some() {
5196 return Err(syn::Error::new_spanned(
5197 field,
5198 "`method` and `source` are mutually exclusive — `method` computes \
5199 the value from a method, `source` reads it from a different model field",
5200 ));
5201 }
5202 Ok(out)
5203}
5204
5205fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5206 let struct_name = &input.ident;
5207 let struct_name_lit = struct_name.to_string();
5208
5209 let Data::Struct(data) = &input.data else {
5210 return Err(syn::Error::new_spanned(
5211 struct_name,
5212 "Serializer can only be derived on structs",
5213 ));
5214 };
5215 let Fields::Named(named) = &data.fields else {
5216 return Err(syn::Error::new_spanned(
5217 struct_name,
5218 "Serializer requires a struct with named fields",
5219 ));
5220 };
5221
5222 let container = parse_serializer_container_attrs(input)?;
5223 let model_path = &container.model;
5224
5225 #[allow(dead_code)]
5229 struct FieldInfo {
5230 ident: syn::Ident,
5231 ty: syn::Type,
5232 attrs: SerializerFieldAttrs,
5233 }
5234 let mut fields_info: Vec<FieldInfo> = Vec::new();
5235 for field in &named.named {
5236 let ident = field.ident.clone().expect("named field has ident");
5237 let attrs = parse_serializer_field_attrs(field)?;
5238 fields_info.push(FieldInfo {
5239 ident,
5240 ty: field.ty.clone(),
5241 attrs,
5242 });
5243 }
5244
5245 let from_model_fields = fields_info.iter().map(|fi| {
5247 let ident = &fi.ident;
5248 let ty = &fi.ty;
5249 if let Some(_inner) = &fi.attrs.many {
5250 quote! { #ident: ::std::vec::Vec::new() }
5254 } else if let Some(method) = &fi.attrs.method {
5255 let method_ident = syn::Ident::new(method, ident.span());
5259 quote! { #ident: Self::#method_ident(model) }
5260 } else if fi.attrs.nested {
5261 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5277 let src_ident = syn::Ident::new(&src_name, ident.span());
5278 if fi.attrs.nested_strict {
5279 let panic_msg = format!(
5280 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5281 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5282 );
5283 quote! {
5284 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5285 model.#src_ident.value().expect(#panic_msg),
5286 )
5287 }
5288 } else {
5289 quote! {
5290 #ident: match model.#src_ident.value() {
5291 ::core::option::Option::Some(__loaded) =>
5292 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5293 ::core::option::Option::None =>
5294 ::core::default::Default::default(),
5295 }
5296 }
5297 }
5298 } else if fi.attrs.write_only || fi.attrs.skip {
5299 quote! { #ident: ::core::default::Default::default() }
5301 } else if let Some(src) = &fi.attrs.source {
5302 let src_ident = syn::Ident::new(src, ident.span());
5303 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5304 } else {
5305 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5306 }
5307 });
5308
5309 let validator_calls: Vec<_> = fields_info.iter().filter_map(|fi| {
5313 let ident = &fi.ident;
5314 let name_lit = ident.to_string();
5315 let method = fi.attrs.validate.as_ref()?;
5316 let method_ident = syn::Ident::new(method, ident.span());
5317 Some(quote! {
5318 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5319 __errors.add(#name_lit.to_owned(), __e);
5320 }
5321 })
5322 }).collect();
5323 let validate_method = if validator_calls.is_empty() {
5324 quote! {}
5325 } else {
5326 quote! {
5327 impl #struct_name {
5328 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5332 let mut __errors = ::rustango::forms::FormErrors::default();
5333 #( #validator_calls )*
5334 if __errors.is_empty() {
5335 ::core::result::Result::Ok(())
5336 } else {
5337 ::core::result::Result::Err(__errors)
5338 }
5339 }
5340 }
5341 }
5342 };
5343
5344 let many_setters: Vec<_> = fields_info.iter().filter_map(|fi| {
5348 let many_ty = fi.attrs.many.as_ref()?;
5349 let ident = &fi.ident;
5350 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5351 Some(quote! {
5352 pub fn #setter(
5357 &mut self,
5358 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5359 ) -> &mut Self {
5360 self.#ident = models.iter()
5361 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5362 .collect();
5363 self
5364 }
5365 })
5366 }).collect();
5367 let many_setters_impl = if many_setters.is_empty() {
5368 quote! {}
5369 } else {
5370 quote! {
5371 impl #struct_name {
5372 #( #many_setters )*
5373 }
5374 }
5375 };
5376
5377 let output_fields: Vec<_> = fields_info
5379 .iter()
5380 .filter(|fi| !fi.attrs.write_only)
5381 .collect();
5382 let output_field_count = output_fields.len();
5383 let serialize_fields = output_fields.iter().map(|fi| {
5384 let ident = &fi.ident;
5385 let name_lit = ident.to_string();
5386 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5387 });
5388
5389 let writable_lits: Vec<_> = fields_info
5391 .iter()
5392 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5393 .map(|fi| fi.ident.to_string())
5394 .collect();
5395
5396 let openapi_impl = {
5400 #[cfg(feature = "openapi")]
5401 {
5402 let property_calls = output_fields.iter().map(|fi| {
5403 let ident = &fi.ident;
5404 let name_lit = ident.to_string();
5405 let ty = &fi.ty;
5406 let nullable_call = if is_option(ty) {
5407 quote! { .nullable() }
5408 } else {
5409 quote! {}
5410 };
5411 quote! {
5412 .property(
5413 #name_lit,
5414 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5415 #nullable_call,
5416 )
5417 }
5418 });
5419 let required_lits: Vec<_> = output_fields
5420 .iter()
5421 .filter(|fi| !is_option(&fi.ty))
5422 .map(|fi| fi.ident.to_string())
5423 .collect();
5424 quote! {
5425 impl ::rustango::openapi::OpenApiSchema for #struct_name {
5426 fn openapi_schema() -> ::rustango::openapi::Schema {
5427 ::rustango::openapi::Schema::object()
5428 #( #property_calls )*
5429 .required([ #( #required_lits ),* ])
5430 }
5431 }
5432 }
5433 }
5434 #[cfg(not(feature = "openapi"))]
5435 {
5436 quote! {}
5437 }
5438 };
5439
5440 Ok(quote! {
5441 impl ::rustango::serializer::ModelSerializer for #struct_name {
5442 type Model = #model_path;
5443
5444 fn from_model(model: &Self::Model) -> Self {
5445 Self {
5446 #( #from_model_fields ),*
5447 }
5448 }
5449
5450 fn writable_fields() -> &'static [&'static str] {
5451 &[ #( #writable_lits ),* ]
5452 }
5453 }
5454
5455 impl ::serde::Serialize for #struct_name {
5456 fn serialize<S>(&self, serializer: S)
5457 -> ::core::result::Result<S::Ok, S::Error>
5458 where
5459 S: ::serde::Serializer,
5460 {
5461 use ::serde::ser::SerializeStruct;
5462 let mut __state = serializer.serialize_struct(
5463 #struct_name_lit,
5464 #output_field_count,
5465 )?;
5466 #( #serialize_fields )*
5467 __state.end()
5468 }
5469 }
5470
5471 #openapi_impl
5472
5473 #validate_method
5474
5475 #many_setters_impl
5476 })
5477}
5478
5479#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5483fn is_option(ty: &syn::Type) -> bool {
5484 if let syn::Type::Path(p) = ty {
5485 if let Some(last) = p.path.segments.last() {
5486 return last.ident == "Option";
5487 }
5488 }
5489 false
5490}