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 );
520 let module_ident = column_module_ident(struct_name);
521 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
522 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
523 let track_set: Option<std::collections::HashSet<&str>> = audit
524 .track
525 .as_ref()
526 .map(|(names, _)| names.iter().map(String::as_str).collect());
527 collected
528 .column_entries
529 .iter()
530 .filter(|c| {
531 track_set
532 .as_ref()
533 .map_or(true, |s| s.contains(c.name.as_str()))
534 })
535 .collect()
536 });
537 let inherent_impl = inherent_impl_tokens(
538 struct_name,
539 &collected,
540 collected.primary_key.as_ref(),
541 &column_consts,
542 audited_fields.as_deref(),
543 );
544 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
545 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
546 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
547 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
548
549 Ok(quote! {
550 #model_impl
551 #inherent_impl
552 #from_row_impl
553 #column_module
554 #reverse_helpers
555 #m2m_accessors
556
557 ::rustango::core::inventory::submit! {
558 ::rustango::core::ModelEntry {
559 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
560 module_path: ::core::module_path!(),
565 }
566 }
567 })
568}
569
570fn load_related_impl_tokens(
581 struct_name: &syn::Ident,
582 fk_relations: &[FkRelation],
583) -> TokenStream2 {
584 let arms = fk_relations.iter().map(|rel| {
585 let parent_ty = &rel.parent_type;
586 let fk_col = rel.fk_column.as_str();
587 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
590 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
591 let assign = if rel.nullable {
592 quote! {
593 self.#field_ident = ::core::option::Option::Some(
594 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
595 );
596 }
597 } else {
598 quote! {
599 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
600 }
601 };
602 quote! {
603 #fk_col => {
604 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
605 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
612 ::rustango::core::SqlValue::#variant_ident(v) => v,
613 _other => {
614 ::core::debug_assert!(
615 false,
616 "rustango macro bug: load_related on FK `{}` expected \
617 SqlValue::{} from parent's __rustango_pk_value but got \
618 {:?} — file a bug at https://github.com/ujeenet/rustango",
619 #fk_col,
620 ::core::stringify!(#variant_ident),
621 _other,
622 );
623 #default_expr
624 }
625 };
626 #assign
627 ::core::result::Result::Ok(true)
628 }
629 }
630 });
631 quote! {
632 impl ::rustango::sql::LoadRelated for #struct_name {
633 #[allow(unused_variables)]
634 fn __rustango_load_related(
635 &mut self,
636 row: &::rustango::sql::sqlx::postgres::PgRow,
637 field_name: &str,
638 alias: &str,
639 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
640 match field_name {
641 #( #arms )*
642 _ => ::core::result::Result::Ok(false),
643 }
644 }
645 }
646 }
647}
648
649fn load_related_impl_my_tokens(
657 struct_name: &syn::Ident,
658 fk_relations: &[FkRelation],
659) -> TokenStream2 {
660 let arms = fk_relations.iter().map(|rel| {
661 let parent_ty = &rel.parent_type;
662 let fk_col = rel.fk_column.as_str();
663 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
664 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
665 let assign = if rel.nullable {
666 quote! {
667 __self.#field_ident = ::core::option::Option::Some(
668 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
669 );
670 }
671 } else {
672 quote! {
673 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
674 }
675 };
676 quote! {
681 #fk_col => {
682 let _parent: #parent_ty =
683 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
684 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
687 ::rustango::core::SqlValue::#variant_ident(v) => v,
688 _other => {
689 ::core::debug_assert!(
690 false,
691 "rustango macro bug: load_related on FK `{}` expected \
692 SqlValue::{} from parent's __rustango_pk_value but got \
693 {:?} — file a bug at https://github.com/ujeenet/rustango",
694 #fk_col,
695 ::core::stringify!(#variant_ident),
696 _other,
697 );
698 #default_expr
699 }
700 };
701 #assign
702 ::core::result::Result::Ok(true)
703 }
704 }
705 });
706 quote! {
707 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
708 #( #arms )*
709 });
710 }
711}
712
713fn fk_pk_access_impl_tokens(
721 struct_name: &syn::Ident,
722 fk_relations: &[FkRelation],
723) -> TokenStream2 {
724 let arms = fk_relations.iter().map(|rel| {
725 let fk_col = rel.fk_column.as_str();
726 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
727 if rel.pk_kind == DetectedKind::I64 {
728 if rel.nullable {
734 quote! {
735 #fk_col => self.#field_ident
736 .as_ref()
737 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
738 }
739 } else {
740 quote! {
741 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
742 }
743 }
744 } else {
745 quote! {
753 #fk_col => ::core::option::Option::None,
754 }
755 }
756 });
757 quote! {
758 impl ::rustango::sql::FkPkAccess for #struct_name {
759 #[allow(unused_variables)]
760 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
761 match field_name {
762 #( #arms )*
763 _ => ::core::option::Option::None,
764 }
765 }
766 }
767 }
768}
769
770fn reverse_helper_tokens(
776 child_ident: &syn::Ident,
777 fk_relations: &[FkRelation],
778) -> TokenStream2 {
779 if fk_relations.is_empty() {
780 return TokenStream2::new();
781 }
782 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
786 let method_ident = syn::Ident::new(&suffix, child_ident.span());
787 let impls = fk_relations.iter().map(|rel| {
788 let parent_ty = &rel.parent_type;
789 let fk_col = rel.fk_column.as_str();
790 let doc = format!(
791 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
792 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
793 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
794 further `{child_ident}::objects()` filters via direct queryset use."
795 );
796 quote! {
797 impl #parent_ty {
798 #[doc = #doc]
799 pub async fn #method_ident<'_c, _E>(
804 &self,
805 _executor: _E,
806 ) -> ::core::result::Result<
807 ::std::vec::Vec<#child_ident>,
808 ::rustango::sql::ExecError,
809 >
810 where
811 _E: ::rustango::sql::sqlx::Executor<
812 '_c,
813 Database = ::rustango::sql::sqlx::Postgres,
814 >,
815 {
816 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
817 ::rustango::query::QuerySet::<#child_ident>::new()
818 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
819 .fetch_on(_executor)
820 .await
821 }
822 }
823 }
824 });
825 quote! { #( #impls )* }
826}
827
828fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
831 if m2m_relations.is_empty() {
832 return TokenStream2::new();
833 }
834 let methods = m2m_relations.iter().map(|rel| {
835 let method_name = format!("{}_m2m", rel.name);
836 let method_ident = syn::Ident::new(&method_name, struct_name.span());
837 let through = rel.through.as_str();
838 let src_col = rel.src.as_str();
839 let dst_col = rel.dst.as_str();
840 quote! {
841 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
842 ::rustango::sql::M2MManager {
843 src_pk: self.__rustango_pk_value(),
844 through: #through,
845 src_col: #src_col,
846 dst_col: #dst_col,
847 }
848 }
849 }
850 });
851 quote! {
852 impl #struct_name {
853 #( #methods )*
854 }
855 }
856}
857
858struct ColumnEntry {
859 ident: syn::Ident,
862 value_ty: Type,
864 name: String,
866 column: String,
868 field_type_tokens: TokenStream2,
870}
871
872struct CollectedFields {
873 field_schemas: Vec<TokenStream2>,
874 from_row_inits: Vec<TokenStream2>,
875 from_aliased_row_inits: Vec<TokenStream2>,
879 insert_columns: Vec<TokenStream2>,
882 insert_values: Vec<TokenStream2>,
885 insert_pushes: Vec<TokenStream2>,
890 returning_cols: Vec<TokenStream2>,
893 auto_assigns: Vec<TokenStream2>,
896 auto_field_idents: Vec<(syn::Ident, String)>,
900 first_auto_value_ty: Option<Type>,
903 bulk_pushes_no_auto: Vec<TokenStream2>,
907 bulk_pushes_all: Vec<TokenStream2>,
911 bulk_columns_no_auto: Vec<TokenStream2>,
914 bulk_columns_all: Vec<TokenStream2>,
917 bulk_auto_uniformity: Vec<TokenStream2>,
921 first_auto_ident: Option<syn::Ident>,
924 has_auto: bool,
926 pk_is_auto: bool,
930 update_assignments: Vec<TokenStream2>,
933 upsert_update_columns: Vec<TokenStream2>,
936 primary_key: Option<(syn::Ident, String)>,
937 column_entries: Vec<ColumnEntry>,
938 field_names: Vec<String>,
941 fk_relations: Vec<FkRelation>,
946 soft_delete_column: Option<String>,
951}
952
953#[derive(Clone)]
954struct FkRelation {
955 parent_type: Type,
958 fk_column: String,
961 pk_kind: DetectedKind,
966 nullable: bool,
971}
972
973fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
974 let cap = named.named.len();
975 let mut out = CollectedFields {
976 field_schemas: Vec::with_capacity(cap),
977 from_row_inits: Vec::with_capacity(cap),
978 from_aliased_row_inits: Vec::with_capacity(cap),
979 insert_columns: Vec::with_capacity(cap),
980 insert_values: Vec::with_capacity(cap),
981 insert_pushes: Vec::with_capacity(cap),
982 returning_cols: Vec::new(),
983 auto_assigns: Vec::new(),
984 auto_field_idents: Vec::new(),
985 first_auto_value_ty: None,
986 bulk_pushes_no_auto: Vec::with_capacity(cap),
987 bulk_pushes_all: Vec::with_capacity(cap),
988 bulk_columns_no_auto: Vec::with_capacity(cap),
989 bulk_columns_all: Vec::with_capacity(cap),
990 bulk_auto_uniformity: Vec::new(),
991 first_auto_ident: None,
992 has_auto: false,
993 pk_is_auto: false,
994 update_assignments: Vec::with_capacity(cap),
995 upsert_update_columns: Vec::with_capacity(cap),
996 primary_key: None,
997 column_entries: Vec::with_capacity(cap),
998 field_names: Vec::with_capacity(cap),
999 fk_relations: Vec::new(),
1000 soft_delete_column: None,
1001 };
1002
1003 for field in &named.named {
1004 let info = process_field(field, table)?;
1005 out.field_names.push(info.ident.to_string());
1006 out.field_schemas.push(info.schema);
1007 out.from_row_inits.push(info.from_row_init);
1008 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1009 if let Some(parent_ty) = info.fk_inner.clone() {
1010 out.fk_relations.push(FkRelation {
1011 parent_type: parent_ty,
1012 fk_column: info.column.clone(),
1013 pk_kind: info.fk_pk_kind,
1014 nullable: info.nullable,
1015 });
1016 }
1017 if info.soft_delete {
1018 if out.soft_delete_column.is_some() {
1019 return Err(syn::Error::new_spanned(
1020 field,
1021 "only one field may be marked `#[rustango(soft_delete)]`",
1022 ));
1023 }
1024 out.soft_delete_column = Some(info.column.clone());
1025 }
1026 let column = info.column.as_str();
1027 let ident = info.ident;
1028 out.insert_columns.push(quote!(#column));
1029 out.insert_values.push(quote! {
1030 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1031 ::core::clone::Clone::clone(&self.#ident)
1032 )
1033 });
1034 if info.auto {
1035 out.has_auto = true;
1036 if out.first_auto_ident.is_none() {
1037 out.first_auto_ident = Some(ident.clone());
1038 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1039 }
1040 out.returning_cols.push(quote!(#column));
1041 out.auto_field_idents
1042 .push((ident.clone(), info.column.clone()));
1043 out.auto_assigns.push(quote! {
1044 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1045 });
1046 out.insert_pushes.push(quote! {
1047 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1048 _columns.push(#column);
1049 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1050 ::core::clone::Clone::clone(_v)
1051 ));
1052 }
1053 });
1054 out.bulk_columns_all.push(quote!(#column));
1057 out.bulk_pushes_all.push(quote! {
1058 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1059 ::core::clone::Clone::clone(&_row.#ident)
1060 ));
1061 });
1062 let ident_clone = ident.clone();
1066 out.bulk_auto_uniformity.push(quote! {
1067 for _r in rows.iter().skip(1) {
1068 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1069 return ::core::result::Result::Err(
1070 ::rustango::sql::ExecError::Sql(
1071 ::rustango::sql::SqlError::BulkAutoMixed
1072 )
1073 );
1074 }
1075 }
1076 });
1077 } else {
1078 out.insert_pushes.push(quote! {
1079 _columns.push(#column);
1080 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1081 ::core::clone::Clone::clone(&self.#ident)
1082 ));
1083 });
1084 out.bulk_columns_no_auto.push(quote!(#column));
1086 out.bulk_columns_all.push(quote!(#column));
1087 let push_expr = quote! {
1088 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1089 ::core::clone::Clone::clone(&_row.#ident)
1090 ));
1091 };
1092 out.bulk_pushes_no_auto.push(push_expr.clone());
1093 out.bulk_pushes_all.push(push_expr);
1094 }
1095 if info.primary_key {
1096 if out.primary_key.is_some() {
1097 return Err(syn::Error::new_spanned(
1098 field,
1099 "only one field may be marked `#[rustango(primary_key)]`",
1100 ));
1101 }
1102 out.primary_key = Some((ident.clone(), info.column.clone()));
1103 if info.auto {
1104 out.pk_is_auto = true;
1105 }
1106 } else if info.auto_now_add {
1107 } else if info.auto_now {
1109 out.update_assignments.push(quote! {
1114 ::rustango::core::Assignment {
1115 column: #column,
1116 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1117 ::chrono::Utc::now()
1118 ),
1119 }
1120 });
1121 out.upsert_update_columns.push(quote!(#column));
1122 } else {
1123 out.update_assignments.push(quote! {
1124 ::rustango::core::Assignment {
1125 column: #column,
1126 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1127 ::core::clone::Clone::clone(&self.#ident)
1128 ),
1129 }
1130 });
1131 out.upsert_update_columns.push(quote!(#column));
1132 }
1133 out.column_entries.push(ColumnEntry {
1134 ident: ident.clone(),
1135 value_ty: info.value_ty.clone(),
1136 name: ident.to_string(),
1137 column: info.column.clone(),
1138 field_type_tokens: info.field_type_tokens,
1139 });
1140 }
1141 Ok(out)
1142}
1143
1144fn model_impl_tokens(
1145 struct_name: &syn::Ident,
1146 model_name: &str,
1147 table: &str,
1148 display: Option<&str>,
1149 app_label: Option<&str>,
1150 admin: Option<&AdminAttrs>,
1151 field_schemas: &[TokenStream2],
1152 soft_delete_column: Option<&str>,
1153 permissions: bool,
1154 audit_track: Option<&[String]>,
1155 m2m_relations: &[M2MAttr],
1156 indexes: &[IndexAttr],
1157 checks: &[CheckAttr],
1158 composite_fks: &[CompositeFkAttr],
1159 generic_fks: &[GenericFkAttr],
1160) -> TokenStream2 {
1161 let display_tokens = if let Some(name) = display {
1162 quote!(::core::option::Option::Some(#name))
1163 } else {
1164 quote!(::core::option::Option::None)
1165 };
1166 let app_label_tokens = if let Some(name) = app_label {
1167 quote!(::core::option::Option::Some(#name))
1168 } else {
1169 quote!(::core::option::Option::None)
1170 };
1171 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1172 quote!(::core::option::Option::Some(#col))
1173 } else {
1174 quote!(::core::option::Option::None)
1175 };
1176 let audit_track_tokens = match audit_track {
1177 None => quote!(::core::option::Option::None),
1178 Some(names) => {
1179 let lits = names.iter().map(|n| n.as_str());
1180 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1181 }
1182 };
1183 let admin_tokens = admin_config_tokens(admin);
1184 let indexes_tokens = indexes.iter().map(|idx| {
1185 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1186 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1187 let unique = idx.unique;
1188 quote! {
1189 ::rustango::core::IndexSchema {
1190 name: #name,
1191 columns: &[ #(#cols),* ],
1192 unique: #unique,
1193 }
1194 }
1195 });
1196 let checks_tokens = checks.iter().map(|c| {
1197 let name = c.name.as_str();
1198 let expr = c.expr.as_str();
1199 quote! {
1200 ::rustango::core::CheckConstraint {
1201 name: #name,
1202 expr: #expr,
1203 }
1204 }
1205 });
1206 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1207 let name = rel.name.as_str();
1208 let to = rel.to.as_str();
1209 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1210 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1211 quote! {
1212 ::rustango::core::CompositeFkRelation {
1213 name: #name,
1214 to: #to,
1215 from: &[ #(#from_cols),* ],
1216 on: &[ #(#on_cols),* ],
1217 }
1218 }
1219 });
1220 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1221 let name = rel.name.as_str();
1222 let ct_col = rel.ct_column.as_str();
1223 let pk_col = rel.pk_column.as_str();
1224 quote! {
1225 ::rustango::core::GenericRelation {
1226 name: #name,
1227 ct_column: #ct_col,
1228 pk_column: #pk_col,
1229 }
1230 }
1231 });
1232 let m2m_tokens = m2m_relations.iter().map(|rel| {
1233 let name = rel.name.as_str();
1234 let to = rel.to.as_str();
1235 let through = rel.through.as_str();
1236 let src = rel.src.as_str();
1237 let dst = rel.dst.as_str();
1238 quote! {
1239 ::rustango::core::M2MRelation {
1240 name: #name,
1241 to: #to,
1242 through: #through,
1243 src_col: #src,
1244 dst_col: #dst,
1245 }
1246 }
1247 });
1248 quote! {
1249 impl ::rustango::core::Model for #struct_name {
1250 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1251 name: #model_name,
1252 table: #table,
1253 fields: &[ #(#field_schemas),* ],
1254 display: #display_tokens,
1255 app_label: #app_label_tokens,
1256 admin: #admin_tokens,
1257 soft_delete_column: #soft_delete_tokens,
1258 permissions: #permissions,
1259 audit_track: #audit_track_tokens,
1260 m2m: &[ #(#m2m_tokens),* ],
1261 indexes: &[ #(#indexes_tokens),* ],
1262 check_constraints: &[ #(#checks_tokens),* ],
1263 composite_relations: &[ #(#composite_fk_tokens),* ],
1264 generic_relations: &[ #(#generic_fk_tokens),* ],
1265 };
1266 }
1267 }
1268}
1269
1270fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1274 let Some(admin) = admin else {
1275 return quote!(::core::option::Option::None);
1276 };
1277
1278 let list_display = admin
1279 .list_display
1280 .as_ref()
1281 .map(|(v, _)| v.as_slice())
1282 .unwrap_or(&[]);
1283 let list_display_lits = list_display.iter().map(|s| s.as_str());
1284
1285 let search_fields = admin
1286 .search_fields
1287 .as_ref()
1288 .map(|(v, _)| v.as_slice())
1289 .unwrap_or(&[]);
1290 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1291
1292 let readonly_fields = admin
1293 .readonly_fields
1294 .as_ref()
1295 .map(|(v, _)| v.as_slice())
1296 .unwrap_or(&[]);
1297 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1298
1299 let list_filter = admin
1300 .list_filter
1301 .as_ref()
1302 .map(|(v, _)| v.as_slice())
1303 .unwrap_or(&[]);
1304 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1305
1306 let actions = admin
1307 .actions
1308 .as_ref()
1309 .map(|(v, _)| v.as_slice())
1310 .unwrap_or(&[]);
1311 let actions_lits = actions.iter().map(|s| s.as_str());
1312
1313 let fieldsets = admin
1314 .fieldsets
1315 .as_ref()
1316 .map(|(v, _)| v.as_slice())
1317 .unwrap_or(&[]);
1318 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1319 let title = title.as_str();
1320 let field_lits = fields.iter().map(|s| s.as_str());
1321 quote!(::rustango::core::Fieldset {
1322 title: #title,
1323 fields: &[ #( #field_lits ),* ],
1324 })
1325 });
1326
1327 let list_per_page = admin.list_per_page.unwrap_or(0);
1328
1329 let ordering_pairs = admin
1330 .ordering
1331 .as_ref()
1332 .map(|(v, _)| v.as_slice())
1333 .unwrap_or(&[]);
1334 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1335 let name = name.as_str();
1336 let desc = *desc;
1337 quote!((#name, #desc))
1338 });
1339
1340 quote! {
1341 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1342 list_display: &[ #( #list_display_lits ),* ],
1343 search_fields: &[ #( #search_fields_lits ),* ],
1344 list_per_page: #list_per_page,
1345 ordering: &[ #( #ordering_tokens ),* ],
1346 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1347 list_filter: &[ #( #list_filter_lits ),* ],
1348 actions: &[ #( #actions_lits ),* ],
1349 fieldsets: &[ #( #fieldset_tokens ),* ],
1350 })
1351 }
1352}
1353
1354fn inherent_impl_tokens(
1355 struct_name: &syn::Ident,
1356 fields: &CollectedFields,
1357 primary_key: Option<&(syn::Ident, String)>,
1358 column_consts: &TokenStream2,
1359 audited_fields: Option<&[&ColumnEntry]>,
1360) -> TokenStream2 {
1361 let executor_passes_to_data_write = if audited_fields.is_some() {
1367 quote!(&mut *_executor)
1368 } else {
1369 quote!(_executor)
1370 };
1371 let executor_param = if audited_fields.is_some() {
1372 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1373 } else {
1374 quote!(_executor: _E)
1375 };
1376 let executor_generics = if audited_fields.is_some() {
1377 quote!()
1378 } else {
1379 quote!(<'_c, _E>)
1380 };
1381 let executor_where = if audited_fields.is_some() {
1382 quote!()
1383 } else {
1384 quote! {
1385 where
1386 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1387 }
1388 };
1389 let pool_to_save_on = if audited_fields.is_some() {
1394 quote! {
1395 let mut _conn = pool.acquire().await?;
1396 self.save_on(&mut *_conn).await
1397 }
1398 } else {
1399 quote!(self.save_on(pool).await)
1400 };
1401 let pool_to_insert_on = if audited_fields.is_some() {
1402 quote! {
1403 let mut _conn = pool.acquire().await?;
1404 self.insert_on(&mut *_conn).await
1405 }
1406 } else {
1407 quote!(self.insert_on(pool).await)
1408 };
1409 let pool_to_delete_on = if audited_fields.is_some() {
1410 quote! {
1411 let mut _conn = pool.acquire().await?;
1412 self.delete_on(&mut *_conn).await
1413 }
1414 } else {
1415 quote!(self.delete_on(pool).await)
1416 };
1417 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1418 quote! {
1419 let mut _conn = pool.acquire().await?;
1420 Self::bulk_insert_on(rows, &mut *_conn).await
1421 }
1422 } else {
1423 quote!(Self::bulk_insert_on(rows, pool).await)
1424 };
1425 let pool_to_upsert_on = if audited_fields.is_some() {
1432 quote! {
1433 let mut _conn = pool.acquire().await?;
1434 self.upsert_on(&mut *_conn).await
1435 }
1436 } else {
1437 quote!(self.upsert_on(pool).await)
1438 };
1439
1440 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1458 quote!()
1467 } else if audited_fields.is_some() && fields.has_auto {
1468 quote!()
1471 } else if fields.has_auto {
1472 let pushes = &fields.insert_pushes;
1473 let returning_cols = &fields.returning_cols;
1474 quote! {
1475 pub async fn insert_pool(
1481 &mut self,
1482 pool: &::rustango::sql::Pool,
1483 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1484 let mut _columns: ::std::vec::Vec<&'static str> =
1485 ::std::vec::Vec::new();
1486 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1487 ::std::vec::Vec::new();
1488 #( #pushes )*
1489 let _query = ::rustango::core::InsertQuery {
1490 model: <Self as ::rustango::core::Model>::SCHEMA,
1491 columns: _columns,
1492 values: _values,
1493 returning: ::std::vec![ #( #returning_cols ),* ],
1494 on_conflict: ::core::option::Option::None,
1495 };
1496 let _result = ::rustango::sql::insert_returning_pool(
1497 pool, &_query,
1498 ).await?;
1499 ::rustango::sql::apply_auto_pk_pool(_result, self)
1500 }
1501 }
1502 } else {
1503 let insert_columns = &fields.insert_columns;
1504 let insert_values = &fields.insert_values;
1505 quote! {
1506 pub async fn insert_pool(
1513 &self,
1514 pool: &::rustango::sql::Pool,
1515 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1516 let _query = ::rustango::core::InsertQuery {
1517 model: <Self as ::rustango::core::Model>::SCHEMA,
1518 columns: ::std::vec![ #( #insert_columns ),* ],
1519 values: ::std::vec![ #( #insert_values ),* ],
1520 returning: ::std::vec::Vec::new(),
1521 on_conflict: ::core::option::Option::None,
1522 };
1523 ::rustango::sql::insert_pool(pool, &_query).await
1524 }
1525 }
1526 };
1527
1528 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1541 .map(|tracked| {
1542 tracked
1543 .iter()
1544 .map(|c| {
1545 let column_lit = c.column.as_str();
1546 let ident = &c.ident;
1547 quote! {
1548 (
1549 #column_lit,
1550 ::serde_json::to_value(&self.#ident)
1551 .unwrap_or(::serde_json::Value::Null),
1552 )
1553 }
1554 })
1555 .collect()
1556 })
1557 .unwrap_or_default();
1558 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1559 if fields.pk_is_auto {
1560 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1561 } else {
1562 quote!(::std::format!("{}", &self.#pk_ident))
1563 }
1564 } else {
1565 quote!(::std::string::String::new())
1566 };
1567 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1568 if audited_fields.is_some() {
1569 let pairs = audit_pair_tokens.iter();
1570 let pk_str = audit_pk_to_string.clone();
1571 quote! {
1572 let _audit_entry = ::rustango::audit::PendingEntry {
1573 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1574 entity_pk: #pk_str,
1575 operation: #op_path,
1576 source: ::rustango::audit::current_source(),
1577 changes: ::rustango::audit::snapshot_changes(&[
1578 #( #pairs ),*
1579 ]),
1580 };
1581 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1582 }
1583 } else {
1584 quote!()
1585 }
1586 };
1587 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1588 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1589 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1590 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1591
1592 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1608 let pk_column_lit = pk_col.as_str();
1609 let assignments = &fields.update_assignments;
1610 if audited_fields.is_some() {
1611 if fields.pk_is_auto {
1612 quote!()
1616 } else {
1617 let pairs = audit_pair_tokens.iter();
1618 let pk_str = audit_pk_to_string.clone();
1619 quote! {
1620 pub async fn save_pool(
1634 &mut self,
1635 pool: &::rustango::sql::Pool,
1636 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1637 let _query = ::rustango::core::UpdateQuery {
1638 model: <Self as ::rustango::core::Model>::SCHEMA,
1639 set: ::std::vec![ #( #assignments ),* ],
1640 where_clause: ::rustango::core::WhereExpr::Predicate(
1641 ::rustango::core::Filter {
1642 column: #pk_column_lit,
1643 op: ::rustango::core::Op::Eq,
1644 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1645 ::core::clone::Clone::clone(&self.#pk_ident)
1646 ),
1647 }
1648 ),
1649 };
1650 let _audit_entry = ::rustango::audit::PendingEntry {
1651 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1652 entity_pk: #pk_str,
1653 operation: ::rustango::audit::AuditOp::Update,
1654 source: ::rustango::audit::current_source(),
1655 changes: ::rustango::audit::snapshot_changes(&[
1656 #( #pairs ),*
1657 ]),
1658 };
1659 let _ = ::rustango::audit::save_one_with_audit_pool(
1660 pool, &_query, &_audit_entry,
1661 ).await?;
1662 ::core::result::Result::Ok(())
1663 }
1664 }
1665 }
1666 } else {
1667 let dispatch_unset = if fields.pk_is_auto {
1668 quote! {
1669 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1670 return self.insert_pool(pool).await;
1671 }
1672 }
1673 } else {
1674 quote!()
1675 };
1676 quote! {
1677 pub async fn save_pool(
1684 &mut self,
1685 pool: &::rustango::sql::Pool,
1686 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1687 #dispatch_unset
1688 let _query = ::rustango::core::UpdateQuery {
1689 model: <Self as ::rustango::core::Model>::SCHEMA,
1690 set: ::std::vec![ #( #assignments ),* ],
1691 where_clause: ::rustango::core::WhereExpr::Predicate(
1692 ::rustango::core::Filter {
1693 column: #pk_column_lit,
1694 op: ::rustango::core::Op::Eq,
1695 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1696 ::core::clone::Clone::clone(&self.#pk_ident)
1697 ),
1698 }
1699 ),
1700 };
1701 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1702 ::core::result::Result::Ok(())
1703 }
1704 }
1705 }
1706 } else {
1707 quote!()
1708 };
1709
1710 let pool_insert_method = if audited_fields.is_some() {
1717 if let Some(_) = primary_key {
1718 let pushes = if fields.has_auto {
1719 fields.insert_pushes.clone()
1720 } else {
1721 fields
1726 .insert_columns
1727 .iter()
1728 .zip(&fields.insert_values)
1729 .map(|(col, val)| {
1730 quote! {
1731 _columns.push(#col);
1732 _values.push(#val);
1733 }
1734 })
1735 .collect()
1736 };
1737 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1738 fields.returning_cols.clone()
1739 } else {
1740 primary_key
1747 .map(|(_, col)| {
1748 let lit = col.as_str();
1749 vec![quote!(#lit)]
1750 })
1751 .unwrap_or_default()
1752 };
1753 let pairs = audit_pair_tokens.iter();
1754 let pk_str = audit_pk_to_string.clone();
1755 quote! {
1756 pub async fn insert_pool(
1765 &mut self,
1766 pool: &::rustango::sql::Pool,
1767 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1768 let mut _columns: ::std::vec::Vec<&'static str> =
1769 ::std::vec::Vec::new();
1770 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1771 ::std::vec::Vec::new();
1772 #( #pushes )*
1773 let _query = ::rustango::core::InsertQuery {
1774 model: <Self as ::rustango::core::Model>::SCHEMA,
1775 columns: _columns,
1776 values: _values,
1777 returning: ::std::vec![ #( #returning_cols ),* ],
1778 on_conflict: ::core::option::Option::None,
1779 };
1780 let _audit_entry = ::rustango::audit::PendingEntry {
1781 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1782 entity_pk: #pk_str,
1783 operation: ::rustango::audit::AuditOp::Create,
1784 source: ::rustango::audit::current_source(),
1785 changes: ::rustango::audit::snapshot_changes(&[
1786 #( #pairs ),*
1787 ]),
1788 };
1789 let _result = ::rustango::audit::insert_one_with_audit_pool(
1790 pool, &_query, &_audit_entry,
1791 ).await?;
1792 ::rustango::sql::apply_auto_pk_pool(_result, self)
1793 }
1794 }
1795 } else {
1796 quote!()
1797 }
1798 } else {
1799 pool_insert_method
1801 };
1802
1803 let pool_save_method = if let Some(tracked) = audited_fields {
1824 if let Some((pk_ident, pk_col)) = primary_key {
1825 let pk_column_lit = pk_col.as_str();
1826 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1830 let pk_str = audit_pk_to_string.clone();
1831 let mk_before_pairs = |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1836 tracked
1837 .iter()
1838 .map(|c| {
1839 let column_lit = c.column.as_str();
1840 let value_ty = &c.value_ty;
1841 quote! {
1842 (
1843 #column_lit,
1844 match #getter::<#value_ty>(
1845 _audit_before_row, #column_lit,
1846 ) {
1847 ::core::result::Result::Ok(v) => {
1848 ::serde_json::to_value(&v)
1849 .unwrap_or(::serde_json::Value::Null)
1850 }
1851 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1852 },
1853 )
1854 }
1855 })
1856 .collect()
1857 };
1858 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
1859 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
1860 let before_pairs_my: Vec<proc_macro2::TokenStream> =
1861 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
1862 let pg_select_cols: String = tracked
1863 .iter()
1864 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1865 .collect::<Vec<_>>()
1866 .join(", ");
1867 let my_select_cols: String = tracked
1868 .iter()
1869 .map(|c| format!("`{}`", c.column.replace('`', "``")))
1870 .collect::<Vec<_>>()
1871 .join(", ");
1872 let pk_value_for_bind = if fields.pk_is_auto {
1873 quote!(self.#pk_ident.get().copied().unwrap_or_default())
1874 } else {
1875 quote!(::core::clone::Clone::clone(&self.#pk_ident))
1876 };
1877 let assignments = &fields.update_assignments;
1878 let unset_dispatch = if fields.has_auto {
1879 quote! {
1880 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1881 return self.insert_pool(pool).await;
1882 }
1883 }
1884 } else {
1885 quote!()
1886 };
1887 quote! {
1888 pub async fn save_pool(
1902 &mut self,
1903 pool: &::rustango::sql::Pool,
1904 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1905 #unset_dispatch
1906 let _query = ::rustango::core::UpdateQuery {
1907 model: <Self as ::rustango::core::Model>::SCHEMA,
1908 set: ::std::vec![ #( #assignments ),* ],
1909 where_clause: ::rustango::core::WhereExpr::Predicate(
1910 ::rustango::core::Filter {
1911 column: #pk_column_lit,
1912 op: ::rustango::core::Op::Eq,
1913 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1914 ::core::clone::Clone::clone(&self.#pk_ident)
1915 ),
1916 }
1917 ),
1918 };
1919 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1920 ::std::vec![ #( #after_pairs_pg ),* ];
1921 ::rustango::audit::save_one_with_diff_pool(
1922 pool,
1923 &_query,
1924 #pk_column_lit,
1925 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1926 #pk_value_for_bind,
1927 ),
1928 <Self as ::rustango::core::Model>::SCHEMA.table,
1929 #pk_str,
1930 _after_pairs,
1931 #pg_select_cols,
1932 #my_select_cols,
1933 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
1934 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
1935 ).await
1936 }
1937 }
1938 } else {
1939 quote!()
1940 }
1941 } else {
1942 pool_save_method
1943 };
1944
1945 let pool_delete_method = {
1952 let pk_column_lit = primary_key
1953 .map(|(_, col)| col.as_str())
1954 .unwrap_or("id");
1955 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
1956 if let Some(pk_ident) = pk_ident_for_pool {
1957 if audited_fields.is_some() {
1958 let pairs = audit_pair_tokens.iter();
1959 let pk_str = audit_pk_to_string.clone();
1960 quote! {
1961 pub async fn delete_pool(
1968 &self,
1969 pool: &::rustango::sql::Pool,
1970 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1971 let _query = ::rustango::core::DeleteQuery {
1972 model: <Self as ::rustango::core::Model>::SCHEMA,
1973 where_clause: ::rustango::core::WhereExpr::Predicate(
1974 ::rustango::core::Filter {
1975 column: #pk_column_lit,
1976 op: ::rustango::core::Op::Eq,
1977 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1978 ::core::clone::Clone::clone(&self.#pk_ident)
1979 ),
1980 }
1981 ),
1982 };
1983 let _audit_entry = ::rustango::audit::PendingEntry {
1984 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1985 entity_pk: #pk_str,
1986 operation: ::rustango::audit::AuditOp::Delete,
1987 source: ::rustango::audit::current_source(),
1988 changes: ::rustango::audit::snapshot_changes(&[
1989 #( #pairs ),*
1990 ]),
1991 };
1992 ::rustango::audit::delete_one_with_audit_pool(
1993 pool, &_query, &_audit_entry,
1994 ).await
1995 }
1996 }
1997 } else {
1998 quote! {
1999 pub async fn delete_pool(
2006 &self,
2007 pool: &::rustango::sql::Pool,
2008 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2009 let _query = ::rustango::core::DeleteQuery {
2010 model: <Self as ::rustango::core::Model>::SCHEMA,
2011 where_clause: ::rustango::core::WhereExpr::Predicate(
2012 ::rustango::core::Filter {
2013 column: #pk_column_lit,
2014 op: ::rustango::core::Op::Eq,
2015 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2016 ::core::clone::Clone::clone(&self.#pk_ident)
2017 ),
2018 }
2019 ),
2020 };
2021 ::rustango::sql::delete_pool(pool, &_query).await
2022 }
2023 }
2024 }
2025 } else {
2026 quote!()
2027 }
2028 };
2029
2030 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
2040 if let Some(tracked) = audited_fields {
2041 if tracked.is_empty() {
2042 (quote!(), quote!())
2043 } else {
2044 let select_cols: String = tracked
2045 .iter()
2046 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2047 .collect::<Vec<_>>()
2048 .join(", ");
2049 let pk_column_for_select = primary_key
2050 .map(|(_, col)| col.clone())
2051 .unwrap_or_default();
2052 let select_cols_lit = select_cols;
2053 let pk_column_lit_for_select = pk_column_for_select;
2054 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2055 if fields.pk_is_auto {
2056 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2057 } else {
2058 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2059 }
2060 } else {
2061 quote!(0_i64)
2062 };
2063 let before_pairs = tracked.iter().map(|c| {
2064 let column_lit = c.column.as_str();
2065 let value_ty = &c.value_ty;
2066 quote! {
2067 (
2068 #column_lit,
2069 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2070 &_audit_before_row, #column_lit,
2071 ) {
2072 ::core::result::Result::Ok(v) => {
2073 ::serde_json::to_value(&v)
2074 .unwrap_or(::serde_json::Value::Null)
2075 }
2076 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2077 },
2078 )
2079 }
2080 });
2081 let after_pairs = tracked.iter().map(|c| {
2082 let column_lit = c.column.as_str();
2083 let ident = &c.ident;
2084 quote! {
2085 (
2086 #column_lit,
2087 ::serde_json::to_value(&self.#ident)
2088 .unwrap_or(::serde_json::Value::Null),
2089 )
2090 }
2091 });
2092 let pk_str = audit_pk_to_string.clone();
2093 let pre = quote! {
2094 let _audit_select_sql = ::std::format!(
2095 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2096 #select_cols_lit,
2097 <Self as ::rustango::core::Model>::SCHEMA.table,
2098 #pk_column_lit_for_select,
2099 );
2100 let _audit_before_pairs:
2101 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2102 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2103 .bind(#pk_value_for_bind)
2104 .fetch_optional(&mut *_executor)
2105 .await
2106 {
2107 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2108 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2109 }
2110 _ => ::core::option::Option::None,
2111 };
2112 };
2113 let post = quote! {
2114 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2115 let _audit_after:
2116 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2117 ::std::vec![ #( #after_pairs ),* ];
2118 let _audit_entry = ::rustango::audit::PendingEntry {
2119 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2120 entity_pk: #pk_str,
2121 operation: ::rustango::audit::AuditOp::Update,
2122 source: ::rustango::audit::current_source(),
2123 changes: ::rustango::audit::diff_changes(
2124 &_audit_before,
2125 &_audit_after,
2126 ),
2127 };
2128 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2129 }
2130 };
2131 (pre, post)
2132 }
2133 } else {
2134 (quote!(), quote!())
2135 };
2136
2137 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2141 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2142 if fields.pk_is_auto {
2143 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2144 } else {
2145 quote!(::std::format!("{}", &_row.#pk_ident))
2146 }
2147 } else {
2148 quote!(::std::string::String::new())
2149 };
2150 let row_pairs = audited_fields
2151 .unwrap_or(&[])
2152 .iter()
2153 .map(|c| {
2154 let column_lit = c.column.as_str();
2155 let ident = &c.ident;
2156 quote! {
2157 (
2158 #column_lit,
2159 ::serde_json::to_value(&_row.#ident)
2160 .unwrap_or(::serde_json::Value::Null),
2161 )
2162 }
2163 });
2164 quote! {
2165 let _audit_source = ::rustango::audit::current_source();
2166 let mut _audit_entries:
2167 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2168 ::std::vec::Vec::with_capacity(rows.len());
2169 for _row in rows.iter() {
2170 _audit_entries.push(::rustango::audit::PendingEntry {
2171 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2172 entity_pk: #row_pk_str,
2173 operation: ::rustango::audit::AuditOp::Create,
2174 source: _audit_source.clone(),
2175 changes: ::rustango::audit::snapshot_changes(&[
2176 #( #row_pairs ),*
2177 ]),
2178 });
2179 }
2180 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2181 }
2182 } else {
2183 quote!()
2184 };
2185
2186 let save_method = if fields.pk_is_auto {
2187 let (pk_ident, pk_column) = primary_key
2188 .expect("pk_is_auto implies primary_key is Some");
2189 let pk_column_lit = pk_column.as_str();
2190 let assignments = &fields.update_assignments;
2191 let upsert_cols = &fields.upsert_update_columns;
2192 let upsert_pushes = &fields.insert_pushes;
2193 let upsert_returning = &fields.returning_cols;
2194 let upsert_auto_assigns = &fields.auto_assigns;
2195 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2196 quote!(::rustango::core::ConflictClause::DoNothing)
2197 } else {
2198 quote!(::rustango::core::ConflictClause::DoUpdate {
2199 target: ::std::vec![#pk_column_lit],
2200 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2201 })
2202 };
2203 Some(quote! {
2204 pub async fn save(
2222 &mut self,
2223 pool: &::rustango::sql::sqlx::PgPool,
2224 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2225 #pool_to_save_on
2226 }
2227
2228 pub async fn save_on #executor_generics (
2239 &mut self,
2240 #executor_param,
2241 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2242 #executor_where
2243 {
2244 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2245 return self.insert_on(#executor_passes_to_data_write).await;
2246 }
2247 #audit_update_pre
2248 let _query = ::rustango::core::UpdateQuery {
2249 model: <Self as ::rustango::core::Model>::SCHEMA,
2250 set: ::std::vec![ #( #assignments ),* ],
2251 where_clause: ::rustango::core::WhereExpr::Predicate(
2252 ::rustango::core::Filter {
2253 column: #pk_column_lit,
2254 op: ::rustango::core::Op::Eq,
2255 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2256 ::core::clone::Clone::clone(&self.#pk_ident)
2257 ),
2258 }
2259 ),
2260 };
2261 let _ = ::rustango::sql::update_on(
2262 #executor_passes_to_data_write,
2263 &_query,
2264 ).await?;
2265 #audit_update_post
2266 ::core::result::Result::Ok(())
2267 }
2268
2269 pub async fn save_on_with #executor_generics (
2280 &mut self,
2281 #executor_param,
2282 source: ::rustango::audit::AuditSource,
2283 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2284 #executor_where
2285 {
2286 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2287 }
2288
2289 pub async fn upsert(
2299 &mut self,
2300 pool: &::rustango::sql::sqlx::PgPool,
2301 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2302 #pool_to_upsert_on
2303 }
2304
2305 pub async fn upsert_on #executor_generics (
2311 &mut self,
2312 #executor_param,
2313 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2314 #executor_where
2315 {
2316 let mut _columns: ::std::vec::Vec<&'static str> =
2317 ::std::vec::Vec::new();
2318 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2319 ::std::vec::Vec::new();
2320 #( #upsert_pushes )*
2321 let query = ::rustango::core::InsertQuery {
2322 model: <Self as ::rustango::core::Model>::SCHEMA,
2323 columns: _columns,
2324 values: _values,
2325 returning: ::std::vec![ #( #upsert_returning ),* ],
2326 on_conflict: ::core::option::Option::Some(#conflict_clause),
2327 };
2328 let _returning_row_v = ::rustango::sql::insert_returning_on(
2329 #executor_passes_to_data_write,
2330 &query,
2331 ).await?;
2332 let _returning_row = &_returning_row_v;
2333 #( #upsert_auto_assigns )*
2334 ::core::result::Result::Ok(())
2335 }
2336 })
2337 } else {
2338 None
2339 };
2340
2341 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2342 let pk_column_lit = pk_column.as_str();
2343 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2350 let col_lit = col;
2351 quote! {
2352 pub async fn soft_delete_on #executor_generics (
2362 &self,
2363 #executor_param,
2364 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2365 #executor_where
2366 {
2367 let _query = ::rustango::core::UpdateQuery {
2368 model: <Self as ::rustango::core::Model>::SCHEMA,
2369 set: ::std::vec![
2370 ::rustango::core::Assignment {
2371 column: #col_lit,
2372 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2373 ::chrono::Utc::now()
2374 ),
2375 },
2376 ],
2377 where_clause: ::rustango::core::WhereExpr::Predicate(
2378 ::rustango::core::Filter {
2379 column: #pk_column_lit,
2380 op: ::rustango::core::Op::Eq,
2381 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2382 ::core::clone::Clone::clone(&self.#pk_ident)
2383 ),
2384 }
2385 ),
2386 };
2387 let _affected = ::rustango::sql::update_on(
2388 #executor_passes_to_data_write,
2389 &_query,
2390 ).await?;
2391 #audit_softdelete_emit
2392 ::core::result::Result::Ok(_affected)
2393 }
2394
2395 pub async fn restore_on #executor_generics (
2402 &self,
2403 #executor_param,
2404 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2405 #executor_where
2406 {
2407 let _query = ::rustango::core::UpdateQuery {
2408 model: <Self as ::rustango::core::Model>::SCHEMA,
2409 set: ::std::vec![
2410 ::rustango::core::Assignment {
2411 column: #col_lit,
2412 value: ::rustango::core::SqlValue::Null,
2413 },
2414 ],
2415 where_clause: ::rustango::core::WhereExpr::Predicate(
2416 ::rustango::core::Filter {
2417 column: #pk_column_lit,
2418 op: ::rustango::core::Op::Eq,
2419 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2420 ::core::clone::Clone::clone(&self.#pk_ident)
2421 ),
2422 }
2423 ),
2424 };
2425 let _affected = ::rustango::sql::update_on(
2426 #executor_passes_to_data_write,
2427 &_query,
2428 ).await?;
2429 #audit_restore_emit
2430 ::core::result::Result::Ok(_affected)
2431 }
2432 }
2433 } else {
2434 quote!()
2435 };
2436 quote! {
2437 pub async fn delete(
2445 &self,
2446 pool: &::rustango::sql::sqlx::PgPool,
2447 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2448 #pool_to_delete_on
2449 }
2450
2451 pub async fn delete_on #executor_generics (
2458 &self,
2459 #executor_param,
2460 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2461 #executor_where
2462 {
2463 let query = ::rustango::core::DeleteQuery {
2464 model: <Self as ::rustango::core::Model>::SCHEMA,
2465 where_clause: ::rustango::core::WhereExpr::Predicate(
2466 ::rustango::core::Filter {
2467 column: #pk_column_lit,
2468 op: ::rustango::core::Op::Eq,
2469 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2470 ::core::clone::Clone::clone(&self.#pk_ident)
2471 ),
2472 }
2473 ),
2474 };
2475 let _affected = ::rustango::sql::delete_on(
2476 #executor_passes_to_data_write,
2477 &query,
2478 ).await?;
2479 #audit_delete_emit
2480 ::core::result::Result::Ok(_affected)
2481 }
2482
2483 pub async fn delete_on_with #executor_generics (
2489 &self,
2490 #executor_param,
2491 source: ::rustango::audit::AuditSource,
2492 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2493 #executor_where
2494 {
2495 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2496 }
2497 #pool_delete_method
2498 #pool_insert_method
2499 #pool_save_method
2500 #soft_delete_methods
2501 }
2502 });
2503
2504 let insert_method = if fields.has_auto {
2505 let pushes = &fields.insert_pushes;
2506 let returning_cols = &fields.returning_cols;
2507 let auto_assigns = &fields.auto_assigns;
2508 quote! {
2509 pub async fn insert(
2518 &mut self,
2519 pool: &::rustango::sql::sqlx::PgPool,
2520 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2521 #pool_to_insert_on
2522 }
2523
2524 pub async fn insert_on #executor_generics (
2530 &mut self,
2531 #executor_param,
2532 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2533 #executor_where
2534 {
2535 let mut _columns: ::std::vec::Vec<&'static str> =
2536 ::std::vec::Vec::new();
2537 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2538 ::std::vec::Vec::new();
2539 #( #pushes )*
2540 let query = ::rustango::core::InsertQuery {
2541 model: <Self as ::rustango::core::Model>::SCHEMA,
2542 columns: _columns,
2543 values: _values,
2544 returning: ::std::vec![ #( #returning_cols ),* ],
2545 on_conflict: ::core::option::Option::None,
2546 };
2547 let _returning_row_v = ::rustango::sql::insert_returning_on(
2548 #executor_passes_to_data_write,
2549 &query,
2550 ).await?;
2551 let _returning_row = &_returning_row_v;
2552 #( #auto_assigns )*
2553 #audit_insert_emit
2554 ::core::result::Result::Ok(())
2555 }
2556
2557 pub async fn insert_on_with #executor_generics (
2563 &mut self,
2564 #executor_param,
2565 source: ::rustango::audit::AuditSource,
2566 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2567 #executor_where
2568 {
2569 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2570 }
2571 }
2572 } else {
2573 let insert_columns = &fields.insert_columns;
2574 let insert_values = &fields.insert_values;
2575 quote! {
2576 pub async fn insert(
2582 &self,
2583 pool: &::rustango::sql::sqlx::PgPool,
2584 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2585 self.insert_on(pool).await
2586 }
2587
2588 pub async fn insert_on<'_c, _E>(
2594 &self,
2595 _executor: _E,
2596 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2597 where
2598 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2599 {
2600 let query = ::rustango::core::InsertQuery {
2601 model: <Self as ::rustango::core::Model>::SCHEMA,
2602 columns: ::std::vec![ #( #insert_columns ),* ],
2603 values: ::std::vec![ #( #insert_values ),* ],
2604 returning: ::std::vec::Vec::new(),
2605 on_conflict: ::core::option::Option::None,
2606 };
2607 ::rustango::sql::insert_on(_executor, &query).await
2608 }
2609 }
2610 };
2611
2612 let bulk_insert_method = if fields.has_auto {
2613 let cols_no_auto = &fields.bulk_columns_no_auto;
2614 let cols_all = &fields.bulk_columns_all;
2615 let pushes_no_auto = &fields.bulk_pushes_no_auto;
2616 let pushes_all = &fields.bulk_pushes_all;
2617 let returning_cols = &fields.returning_cols;
2618 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2619 let uniformity = &fields.bulk_auto_uniformity;
2620 let first_auto_ident = fields
2621 .first_auto_ident
2622 .as_ref()
2623 .expect("has_auto implies first_auto_ident is Some");
2624 quote! {
2625 pub async fn bulk_insert(
2639 rows: &mut [Self],
2640 pool: &::rustango::sql::sqlx::PgPool,
2641 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2642 #pool_to_bulk_insert_on
2643 }
2644
2645 pub async fn bulk_insert_on #executor_generics (
2651 rows: &mut [Self],
2652 #executor_param,
2653 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2654 #executor_where
2655 {
2656 if rows.is_empty() {
2657 return ::core::result::Result::Ok(());
2658 }
2659 let _first_unset = matches!(
2660 rows[0].#first_auto_ident,
2661 ::rustango::sql::Auto::Unset
2662 );
2663 #( #uniformity )*
2664
2665 let mut _all_rows: ::std::vec::Vec<
2666 ::std::vec::Vec<::rustango::core::SqlValue>,
2667 > = ::std::vec::Vec::with_capacity(rows.len());
2668 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2669 for _row in rows.iter() {
2670 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2671 ::std::vec::Vec::new();
2672 #( #pushes_no_auto )*
2673 _all_rows.push(_row_vals);
2674 }
2675 ::std::vec![ #( #cols_no_auto ),* ]
2676 } else {
2677 for _row in rows.iter() {
2678 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2679 ::std::vec::Vec::new();
2680 #( #pushes_all )*
2681 _all_rows.push(_row_vals);
2682 }
2683 ::std::vec![ #( #cols_all ),* ]
2684 };
2685
2686 let _query = ::rustango::core::BulkInsertQuery {
2687 model: <Self as ::rustango::core::Model>::SCHEMA,
2688 columns: _columns,
2689 rows: _all_rows,
2690 returning: ::std::vec![ #( #returning_cols ),* ],
2691 on_conflict: ::core::option::Option::None,
2692 };
2693 let _returned = ::rustango::sql::bulk_insert_on(
2694 #executor_passes_to_data_write,
2695 &_query,
2696 ).await?;
2697 if _returned.len() != rows.len() {
2698 return ::core::result::Result::Err(
2699 ::rustango::sql::ExecError::Sql(
2700 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2701 expected: rows.len(),
2702 actual: _returned.len(),
2703 }
2704 )
2705 );
2706 }
2707 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2708 #auto_assigns_for_row
2709 }
2710 #audit_bulk_insert_emit
2711 ::core::result::Result::Ok(())
2712 }
2713 }
2714 } else {
2715 let cols_all = &fields.bulk_columns_all;
2716 let pushes_all = &fields.bulk_pushes_all;
2717 quote! {
2718 pub async fn bulk_insert(
2728 rows: &[Self],
2729 pool: &::rustango::sql::sqlx::PgPool,
2730 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2731 Self::bulk_insert_on(rows, pool).await
2732 }
2733
2734 pub async fn bulk_insert_on<'_c, _E>(
2740 rows: &[Self],
2741 _executor: _E,
2742 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2743 where
2744 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2745 {
2746 if rows.is_empty() {
2747 return ::core::result::Result::Ok(());
2748 }
2749 let mut _all_rows: ::std::vec::Vec<
2750 ::std::vec::Vec<::rustango::core::SqlValue>,
2751 > = ::std::vec::Vec::with_capacity(rows.len());
2752 for _row in rows.iter() {
2753 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2754 ::std::vec::Vec::new();
2755 #( #pushes_all )*
2756 _all_rows.push(_row_vals);
2757 }
2758 let _query = ::rustango::core::BulkInsertQuery {
2759 model: <Self as ::rustango::core::Model>::SCHEMA,
2760 columns: ::std::vec![ #( #cols_all ),* ],
2761 rows: _all_rows,
2762 returning: ::std::vec::Vec::new(),
2763 on_conflict: ::core::option::Option::None,
2764 };
2765 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2766 ::core::result::Result::Ok(())
2767 }
2768 }
2769 };
2770
2771 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2772 quote! {
2773 #[doc(hidden)]
2778 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2779 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2780 ::core::clone::Clone::clone(&self.#pk_ident)
2781 )
2782 }
2783 }
2784 });
2785
2786 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2787 quote! {
2788 impl ::rustango::sql::HasPkValue for #struct_name {
2789 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2790 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2791 ::core::clone::Clone::clone(&self.#pk_ident)
2792 )
2793 }
2794 }
2795 }
2796 });
2797
2798 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2799
2800 let assign_auto_pk_pool_impl = {
2806 let auto_assigns = &fields.auto_assigns;
2807 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
2808 let value_ty = fields
2826 .first_auto_value_ty
2827 .as_ref()
2828 .expect("first_auto_value_ty set whenever first_auto_ident is");
2829 quote! {
2830 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
2831 ::rustango_from_mysql_auto_id(_id)?;
2832 self.#first = ::rustango::sql::Auto::Set(_converted);
2833 ::core::result::Result::Ok(())
2834 }
2835 } else {
2836 quote! {
2837 let _ = _id;
2838 ::core::result::Result::Ok(())
2839 }
2840 };
2841 quote! {
2842 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
2843 fn __rustango_assign_from_pg_row(
2844 &mut self,
2845 _returning_row: &::rustango::sql::PgReturningRow,
2846 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2847 #( #auto_assigns )*
2848 ::core::result::Result::Ok(())
2849 }
2850 fn __rustango_assign_from_mysql_id(
2851 &mut self,
2852 _id: i64,
2853 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2854 #mysql_body
2855 }
2856 }
2857 }
2858 };
2859
2860 let from_aliased_row_inits = &fields.from_aliased_row_inits;
2861 let aliased_row_helper = quote! {
2862 #[doc(hidden)]
2868 pub fn __rustango_from_aliased_row(
2869 row: &::rustango::sql::sqlx::postgres::PgRow,
2870 prefix: &str,
2871 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2872 ::core::result::Result::Ok(Self {
2873 #( #from_aliased_row_inits ),*
2874 })
2875 }
2876 };
2877 let aliased_row_helper_my = quote! {
2880 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
2881 #( #from_aliased_row_inits ),*
2882 });
2883 };
2884
2885 let load_related_impl =
2886 load_related_impl_tokens(struct_name, &fields.fk_relations);
2887 let load_related_impl_my =
2888 load_related_impl_my_tokens(struct_name, &fields.fk_relations);
2889
2890 quote! {
2891 impl #struct_name {
2892 #[must_use]
2894 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
2895 ::rustango::query::QuerySet::new()
2896 }
2897
2898 #insert_method
2899
2900 #bulk_insert_method
2901
2902 #save_method
2903
2904 #pk_methods
2905
2906 #pk_value_helper
2907
2908 #aliased_row_helper
2909
2910 #column_consts
2911 }
2912
2913 #aliased_row_helper_my
2914
2915 #load_related_impl
2916
2917 #load_related_impl_my
2918
2919 #has_pk_value_impl
2920
2921 #fk_pk_access_impl
2922
2923 #assign_auto_pk_pool_impl
2924 }
2925}
2926
2927fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
2931 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
2932 let col_lit = column.as_str();
2933 quote! {
2934 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
2935 _returning_row,
2936 #col_lit,
2937 )?;
2938 }
2939 });
2940 quote! { #( #lines )* }
2941}
2942
2943fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
2945 let lines = entries.iter().map(|e| {
2946 let ident = &e.ident;
2947 let col_ty = column_type_ident(ident);
2948 quote! {
2949 #[allow(non_upper_case_globals)]
2950 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
2951 }
2952 });
2953 quote! { #(#lines)* }
2954}
2955
2956fn column_module_tokens(
2959 module_ident: &syn::Ident,
2960 struct_name: &syn::Ident,
2961 entries: &[ColumnEntry],
2962) -> TokenStream2 {
2963 let items = entries.iter().map(|e| {
2964 let col_ty = column_type_ident(&e.ident);
2965 let value_ty = &e.value_ty;
2966 let name = &e.name;
2967 let column = &e.column;
2968 let field_type_tokens = &e.field_type_tokens;
2969 quote! {
2970 #[derive(::core::clone::Clone, ::core::marker::Copy)]
2971 pub struct #col_ty;
2972
2973 impl ::rustango::core::Column for #col_ty {
2974 type Model = super::#struct_name;
2975 type Value = #value_ty;
2976 const NAME: &'static str = #name;
2977 const COLUMN: &'static str = #column;
2978 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
2979 }
2980 }
2981 });
2982 quote! {
2983 #[doc(hidden)]
2984 #[allow(non_camel_case_types, non_snake_case)]
2985 pub mod #module_ident {
2986 #[allow(unused_imports)]
2991 use super::*;
2992 #(#items)*
2993 }
2994 }
2995}
2996
2997fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
2998 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
2999}
3000
3001fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3002 syn::Ident::new(
3003 &format!("__rustango_cols_{struct_name}"),
3004 struct_name.span(),
3005 )
3006}
3007
3008fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3009 quote! {
3020 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3021 for #struct_name
3022 {
3023 fn from_row(
3024 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3025 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3026 ::core::result::Result::Ok(Self {
3027 #( #from_row_inits ),*
3028 })
3029 }
3030 }
3031
3032 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3033 #( #from_row_inits ),*
3034 });
3035 }
3036}
3037
3038struct ContainerAttrs {
3039 table: Option<String>,
3040 display: Option<(String, proc_macro2::Span)>,
3041 app: Option<String>,
3048 admin: Option<AdminAttrs>,
3053 audit: Option<AuditAttrs>,
3059 permissions: bool,
3063 m2m: Vec<M2MAttr>,
3067 indexes: Vec<IndexAttr>,
3073 checks: Vec<CheckAttr>,
3076 composite_fks: Vec<CompositeFkAttr>,
3080 generic_fks: Vec<GenericFkAttr>,
3084}
3085
3086struct IndexAttr {
3088 name: Option<String>,
3090 columns: Vec<String>,
3092 unique: bool,
3094}
3095
3096struct CheckAttr {
3098 name: String,
3099 expr: String,
3100}
3101
3102struct CompositeFkAttr {
3108 name: String,
3110 to: String,
3112 from: Vec<String>,
3114 on: Vec<String>,
3116}
3117
3118struct GenericFkAttr {
3123 name: String,
3125 ct_column: String,
3127 pk_column: String,
3129}
3130
3131struct M2MAttr {
3133 name: String,
3135 to: String,
3137 through: String,
3139 src: String,
3141 dst: String,
3143}
3144
3145#[derive(Default)]
3151struct AuditAttrs {
3152 track: Option<(Vec<String>, proc_macro2::Span)>,
3156}
3157
3158#[derive(Default)]
3163struct AdminAttrs {
3164 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3165 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3166 list_per_page: Option<usize>,
3167 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3168 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3169 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3170 actions: Option<(Vec<String>, proc_macro2::Span)>,
3173 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3177}
3178
3179fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3180 let mut out = ContainerAttrs {
3181 table: None,
3182 display: None,
3183 app: None,
3184 admin: None,
3185 audit: None,
3186 permissions: false,
3187 m2m: Vec::new(),
3188 indexes: Vec::new(),
3189 checks: Vec::new(),
3190 composite_fks: Vec::new(),
3191 generic_fks: Vec::new(),
3192 };
3193 for attr in &input.attrs {
3194 if !attr.path().is_ident("rustango") {
3195 continue;
3196 }
3197 attr.parse_nested_meta(|meta| {
3198 if meta.path.is_ident("table") {
3199 let s: LitStr = meta.value()?.parse()?;
3200 out.table = Some(s.value());
3201 return Ok(());
3202 }
3203 if meta.path.is_ident("display") {
3204 let s: LitStr = meta.value()?.parse()?;
3205 out.display = Some((s.value(), s.span()));
3206 return Ok(());
3207 }
3208 if meta.path.is_ident("app") {
3209 let s: LitStr = meta.value()?.parse()?;
3210 out.app = Some(s.value());
3211 return Ok(());
3212 }
3213 if meta.path.is_ident("admin") {
3214 let mut admin = AdminAttrs::default();
3215 meta.parse_nested_meta(|inner| {
3216 if inner.path.is_ident("list_display") {
3217 let s: LitStr = inner.value()?.parse()?;
3218 admin.list_display =
3219 Some((split_field_list(&s.value()), s.span()));
3220 return Ok(());
3221 }
3222 if inner.path.is_ident("search_fields") {
3223 let s: LitStr = inner.value()?.parse()?;
3224 admin.search_fields =
3225 Some((split_field_list(&s.value()), s.span()));
3226 return Ok(());
3227 }
3228 if inner.path.is_ident("readonly_fields") {
3229 let s: LitStr = inner.value()?.parse()?;
3230 admin.readonly_fields =
3231 Some((split_field_list(&s.value()), s.span()));
3232 return Ok(());
3233 }
3234 if inner.path.is_ident("list_per_page") {
3235 let lit: syn::LitInt = inner.value()?.parse()?;
3236 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3237 return Ok(());
3238 }
3239 if inner.path.is_ident("ordering") {
3240 let s: LitStr = inner.value()?.parse()?;
3241 admin.ordering = Some((
3242 parse_ordering_list(&s.value()),
3243 s.span(),
3244 ));
3245 return Ok(());
3246 }
3247 if inner.path.is_ident("list_filter") {
3248 let s: LitStr = inner.value()?.parse()?;
3249 admin.list_filter =
3250 Some((split_field_list(&s.value()), s.span()));
3251 return Ok(());
3252 }
3253 if inner.path.is_ident("actions") {
3254 let s: LitStr = inner.value()?.parse()?;
3255 admin.actions =
3256 Some((split_field_list(&s.value()), s.span()));
3257 return Ok(());
3258 }
3259 if inner.path.is_ident("fieldsets") {
3260 let s: LitStr = inner.value()?.parse()?;
3261 admin.fieldsets =
3262 Some((parse_fieldset_list(&s.value()), s.span()));
3263 return Ok(());
3264 }
3265 Err(inner.error(
3266 "unknown admin attribute (supported: \
3267 `list_display`, `search_fields`, `readonly_fields`, \
3268 `list_filter`, `list_per_page`, `ordering`, `actions`, \
3269 `fieldsets`)",
3270 ))
3271 })?;
3272 out.admin = Some(admin);
3273 return Ok(());
3274 }
3275 if meta.path.is_ident("audit") {
3276 let mut audit = AuditAttrs::default();
3277 meta.parse_nested_meta(|inner| {
3278 if inner.path.is_ident("track") {
3279 let s: LitStr = inner.value()?.parse()?;
3280 audit.track =
3281 Some((split_field_list(&s.value()), s.span()));
3282 return Ok(());
3283 }
3284 Err(inner.error(
3285 "unknown audit attribute (supported: `track`)",
3286 ))
3287 })?;
3288 out.audit = Some(audit);
3289 return Ok(());
3290 }
3291 if meta.path.is_ident("permissions") {
3292 out.permissions = true;
3293 return Ok(());
3294 }
3295 if meta.path.is_ident("unique_together") {
3296 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3305 out.indexes.push(IndexAttr { name, columns, unique: true });
3306 return Ok(());
3307 }
3308 if meta.path.is_ident("index_together") {
3309 let (columns, name) = parse_together_attr(&meta, "index_together")?;
3315 out.indexes.push(IndexAttr { name, columns, unique: false });
3316 return Ok(());
3317 }
3318 if meta.path.is_ident("index") {
3319 let cols_lit: LitStr = meta.value()?.parse()?;
3327 let columns = split_field_list(&cols_lit.value());
3328 out.indexes.push(IndexAttr { name: None, columns, unique: false });
3329 return Ok(());
3330 }
3331 if meta.path.is_ident("check") {
3332 let mut name: Option<String> = None;
3334 let mut expr: Option<String> = None;
3335 meta.parse_nested_meta(|inner| {
3336 if inner.path.is_ident("name") {
3337 let s: LitStr = inner.value()?.parse()?;
3338 name = Some(s.value());
3339 return Ok(());
3340 }
3341 if inner.path.is_ident("expr") {
3342 let s: LitStr = inner.value()?.parse()?;
3343 expr = Some(s.value());
3344 return Ok(());
3345 }
3346 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3347 })?;
3348 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3349 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3350 out.checks.push(CheckAttr { name, expr });
3351 return Ok(());
3352 }
3353 if meta.path.is_ident("generic_fk") {
3354 let mut gfk = GenericFkAttr {
3355 name: String::new(),
3356 ct_column: String::new(),
3357 pk_column: String::new(),
3358 };
3359 meta.parse_nested_meta(|inner| {
3360 if inner.path.is_ident("name") {
3361 let s: LitStr = inner.value()?.parse()?;
3362 gfk.name = s.value();
3363 return Ok(());
3364 }
3365 if inner.path.is_ident("ct_column") {
3366 let s: LitStr = inner.value()?.parse()?;
3367 gfk.ct_column = s.value();
3368 return Ok(());
3369 }
3370 if inner.path.is_ident("pk_column") {
3371 let s: LitStr = inner.value()?.parse()?;
3372 gfk.pk_column = s.value();
3373 return Ok(());
3374 }
3375 Err(inner.error(
3376 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3377 ))
3378 })?;
3379 if gfk.name.is_empty() {
3380 return Err(meta.error("generic_fk requires `name = \"...\"`"));
3381 }
3382 if gfk.ct_column.is_empty() {
3383 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3384 }
3385 if gfk.pk_column.is_empty() {
3386 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3387 }
3388 out.generic_fks.push(gfk);
3389 return Ok(());
3390 }
3391 if meta.path.is_ident("fk_composite") {
3392 let mut fk = CompositeFkAttr {
3393 name: String::new(),
3394 to: String::new(),
3395 from: Vec::new(),
3396 on: Vec::new(),
3397 };
3398 meta.parse_nested_meta(|inner| {
3399 if inner.path.is_ident("name") {
3400 let s: LitStr = inner.value()?.parse()?;
3401 fk.name = s.value();
3402 return Ok(());
3403 }
3404 if inner.path.is_ident("to") {
3405 let s: LitStr = inner.value()?.parse()?;
3406 fk.to = s.value();
3407 return Ok(());
3408 }
3409 if inner.path.is_ident("on") || inner.path.is_ident("from") {
3412 let value = inner.value()?;
3413 let content;
3414 syn::parenthesized!(content in value);
3415 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3416 content.parse_terminated(
3417 |p| p.parse::<syn::LitStr>(),
3418 syn::Token![,],
3419 )?;
3420 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3421 if inner.path.is_ident("on") {
3422 fk.on = cols;
3423 } else {
3424 fk.from = cols;
3425 }
3426 return Ok(());
3427 }
3428 Err(inner.error(
3429 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3430 ))
3431 })?;
3432 if fk.name.is_empty() {
3433 return Err(meta.error("fk_composite requires `name = \"...\"`"));
3434 }
3435 if fk.to.is_empty() {
3436 return Err(meta.error("fk_composite requires `to = \"...\"`"));
3437 }
3438 if fk.from.is_empty() || fk.on.is_empty() {
3439 return Err(meta.error(
3440 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3441 ));
3442 }
3443 if fk.from.len() != fk.on.len() {
3444 return Err(meta.error(format!(
3445 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3446 fk.from.len(),
3447 fk.on.len(),
3448 )));
3449 }
3450 out.composite_fks.push(fk);
3451 return Ok(());
3452 }
3453 if meta.path.is_ident("m2m") {
3454 let mut m2m = M2MAttr {
3455 name: String::new(),
3456 to: String::new(),
3457 through: String::new(),
3458 src: String::new(),
3459 dst: String::new(),
3460 };
3461 meta.parse_nested_meta(|inner| {
3462 if inner.path.is_ident("name") {
3463 let s: LitStr = inner.value()?.parse()?;
3464 m2m.name = s.value();
3465 return Ok(());
3466 }
3467 if inner.path.is_ident("to") {
3468 let s: LitStr = inner.value()?.parse()?;
3469 m2m.to = s.value();
3470 return Ok(());
3471 }
3472 if inner.path.is_ident("through") {
3473 let s: LitStr = inner.value()?.parse()?;
3474 m2m.through = s.value();
3475 return Ok(());
3476 }
3477 if inner.path.is_ident("src") {
3478 let s: LitStr = inner.value()?.parse()?;
3479 m2m.src = s.value();
3480 return Ok(());
3481 }
3482 if inner.path.is_ident("dst") {
3483 let s: LitStr = inner.value()?.parse()?;
3484 m2m.dst = s.value();
3485 return Ok(());
3486 }
3487 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3488 })?;
3489 if m2m.name.is_empty() {
3490 return Err(meta.error("m2m requires `name = \"...\"`"));
3491 }
3492 if m2m.to.is_empty() {
3493 return Err(meta.error("m2m requires `to = \"...\"`"));
3494 }
3495 if m2m.through.is_empty() {
3496 return Err(meta.error("m2m requires `through = \"...\"`"));
3497 }
3498 if m2m.src.is_empty() {
3499 return Err(meta.error("m2m requires `src = \"...\"`"));
3500 }
3501 if m2m.dst.is_empty() {
3502 return Err(meta.error("m2m requires `dst = \"...\"`"));
3503 }
3504 out.m2m.push(m2m);
3505 return Ok(());
3506 }
3507 Err(meta.error("unknown rustango container attribute"))
3508 })?;
3509 }
3510 Ok(out)
3511}
3512
3513fn split_field_list(raw: &str) -> Vec<String> {
3517 raw.split(',')
3518 .map(str::trim)
3519 .filter(|s| !s.is_empty())
3520 .map(str::to_owned)
3521 .collect()
3522}
3523
3524fn parse_together_attr(
3532 meta: &syn::meta::ParseNestedMeta<'_>,
3533 attr: &str,
3534) -> syn::Result<(Vec<String>, Option<String>)> {
3535 if meta.input.peek(syn::Token![=]) {
3538 let cols_lit: LitStr = meta.value()?.parse()?;
3539 let columns = split_field_list(&cols_lit.value());
3540 check_together_columns(meta, attr, &columns)?;
3541 return Ok((columns, None));
3542 }
3543 let mut columns: Option<Vec<String>> = None;
3544 let mut name: Option<String> = None;
3545 meta.parse_nested_meta(|inner| {
3546 if inner.path.is_ident("columns") {
3547 let s: LitStr = inner.value()?.parse()?;
3548 columns = Some(split_field_list(&s.value()));
3549 return Ok(());
3550 }
3551 if inner.path.is_ident("name") {
3552 let s: LitStr = inner.value()?.parse()?;
3553 name = Some(s.value());
3554 return Ok(());
3555 }
3556 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3557 })?;
3558 let columns = columns.ok_or_else(|| meta.error(format!(
3559 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3560 )))?;
3561 check_together_columns(meta, attr, &columns)?;
3562 Ok((columns, name))
3563}
3564
3565fn check_together_columns(
3566 meta: &syn::meta::ParseNestedMeta<'_>,
3567 attr: &str,
3568 columns: &[String],
3569) -> syn::Result<()> {
3570 if columns.len() < 2 {
3571 let single = if attr == "unique_together" {
3572 "#[rustango(unique)] on the field"
3573 } else {
3574 "#[rustango(index)] on the field"
3575 };
3576 return Err(meta.error(format!(
3577 "{attr} expects two or more columns; for a single-column equivalent use {single}",
3578 )));
3579 }
3580 Ok(())
3581}
3582
3583fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3592 raw.split('|')
3593 .map(str::trim)
3594 .filter(|s| !s.is_empty())
3595 .map(|section| {
3596 let (title, rest) = match section.split_once(':') {
3598 Some((title, rest)) if !title.contains(',') => {
3599 (title.trim().to_owned(), rest)
3600 }
3601 _ => (String::new(), section),
3602 };
3603 let fields = split_field_list(rest);
3604 (title, fields)
3605 })
3606 .collect()
3607}
3608
3609fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3612 raw.split(',')
3613 .map(str::trim)
3614 .filter(|s| !s.is_empty())
3615 .map(|spec| {
3616 spec.strip_prefix('-')
3617 .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
3618 })
3619 .collect()
3620}
3621
3622struct FieldAttrs {
3623 column: Option<String>,
3624 primary_key: bool,
3625 fk: Option<String>,
3626 o2o: Option<String>,
3627 on: Option<String>,
3628 max_length: Option<u32>,
3629 min: Option<i64>,
3630 max: Option<i64>,
3631 default: Option<String>,
3632 auto_uuid: bool,
3638 auto_now_add: bool,
3643 auto_now: bool,
3649 soft_delete: bool,
3654 unique: bool,
3657 index: bool,
3661 index_unique: bool,
3662 index_name: Option<String>,
3663}
3664
3665fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3666 let mut out = FieldAttrs {
3667 column: None,
3668 primary_key: false,
3669 fk: None,
3670 o2o: None,
3671 on: None,
3672 max_length: None,
3673 min: None,
3674 max: None,
3675 default: None,
3676 auto_uuid: false,
3677 auto_now_add: false,
3678 auto_now: false,
3679 soft_delete: false,
3680 unique: false,
3681 index: false,
3682 index_unique: false,
3683 index_name: None,
3684 };
3685 for attr in &field.attrs {
3686 if !attr.path().is_ident("rustango") {
3687 continue;
3688 }
3689 attr.parse_nested_meta(|meta| {
3690 if meta.path.is_ident("column") {
3691 let s: LitStr = meta.value()?.parse()?;
3692 out.column = Some(s.value());
3693 return Ok(());
3694 }
3695 if meta.path.is_ident("primary_key") {
3696 out.primary_key = true;
3697 return Ok(());
3698 }
3699 if meta.path.is_ident("fk") {
3700 let s: LitStr = meta.value()?.parse()?;
3701 out.fk = Some(s.value());
3702 return Ok(());
3703 }
3704 if meta.path.is_ident("o2o") {
3705 let s: LitStr = meta.value()?.parse()?;
3706 out.o2o = Some(s.value());
3707 return Ok(());
3708 }
3709 if meta.path.is_ident("on") {
3710 let s: LitStr = meta.value()?.parse()?;
3711 out.on = Some(s.value());
3712 return Ok(());
3713 }
3714 if meta.path.is_ident("max_length") {
3715 let lit: syn::LitInt = meta.value()?.parse()?;
3716 out.max_length = Some(lit.base10_parse::<u32>()?);
3717 return Ok(());
3718 }
3719 if meta.path.is_ident("min") {
3720 out.min = Some(parse_signed_i64(&meta)?);
3721 return Ok(());
3722 }
3723 if meta.path.is_ident("max") {
3724 out.max = Some(parse_signed_i64(&meta)?);
3725 return Ok(());
3726 }
3727 if meta.path.is_ident("default") {
3728 let s: LitStr = meta.value()?.parse()?;
3729 out.default = Some(s.value());
3730 return Ok(());
3731 }
3732 if meta.path.is_ident("auto_uuid") {
3733 out.auto_uuid = true;
3734 out.primary_key = true;
3738 if out.default.is_none() {
3739 out.default = Some("gen_random_uuid()".into());
3740 }
3741 return Ok(());
3742 }
3743 if meta.path.is_ident("auto_now_add") {
3744 out.auto_now_add = true;
3745 if out.default.is_none() {
3746 out.default = Some("now()".into());
3747 }
3748 return Ok(());
3749 }
3750 if meta.path.is_ident("auto_now") {
3751 out.auto_now = true;
3752 if out.default.is_none() {
3753 out.default = Some("now()".into());
3754 }
3755 return Ok(());
3756 }
3757 if meta.path.is_ident("soft_delete") {
3758 out.soft_delete = true;
3759 return Ok(());
3760 }
3761 if meta.path.is_ident("unique") {
3762 out.unique = true;
3763 return Ok(());
3764 }
3765 if meta.path.is_ident("index") {
3766 out.index = true;
3767 if meta.input.peek(syn::token::Paren) {
3769 meta.parse_nested_meta(|inner| {
3770 if inner.path.is_ident("unique") {
3771 out.index_unique = true;
3772 return Ok(());
3773 }
3774 if inner.path.is_ident("name") {
3775 let s: LitStr = inner.value()?.parse()?;
3776 out.index_name = Some(s.value());
3777 return Ok(());
3778 }
3779 Err(inner.error("unknown index sub-attribute (supported: `unique`, `name`)"))
3780 })?;
3781 }
3782 return Ok(());
3783 }
3784 Err(meta.error("unknown rustango field attribute"))
3785 })?;
3786 }
3787 Ok(out)
3788}
3789
3790fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
3792 let expr: syn::Expr = meta.value()?.parse()?;
3793 match expr {
3794 syn::Expr::Lit(syn::ExprLit {
3795 lit: syn::Lit::Int(lit),
3796 ..
3797 }) => lit.base10_parse::<i64>(),
3798 syn::Expr::Unary(syn::ExprUnary {
3799 op: syn::UnOp::Neg(_),
3800 expr,
3801 ..
3802 }) => {
3803 if let syn::Expr::Lit(syn::ExprLit {
3804 lit: syn::Lit::Int(lit),
3805 ..
3806 }) = *expr
3807 {
3808 let v: i64 = lit.base10_parse()?;
3809 Ok(-v)
3810 } else {
3811 Err(syn::Error::new_spanned(expr, "expected integer literal"))
3812 }
3813 }
3814 other => Err(syn::Error::new_spanned(
3815 other,
3816 "expected integer literal (signed)",
3817 )),
3818 }
3819}
3820
3821struct FieldInfo<'a> {
3822 ident: &'a syn::Ident,
3823 column: String,
3824 primary_key: bool,
3825 auto: bool,
3829 value_ty: &'a Type,
3832 field_type_tokens: TokenStream2,
3834 schema: TokenStream2,
3835 from_row_init: TokenStream2,
3836 from_aliased_row_init: TokenStream2,
3842 fk_inner: Option<Type>,
3846 fk_pk_kind: DetectedKind,
3852 nullable: bool,
3860 auto_now: bool,
3866 auto_now_add: bool,
3872 soft_delete: bool,
3877}
3878
3879fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
3880 let attrs = parse_field_attrs(field)?;
3881 let ident = field
3882 .ident
3883 .as_ref()
3884 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
3885 let name = ident.to_string();
3886 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
3887 let primary_key = attrs.primary_key;
3888 let DetectedType {
3889 kind,
3890 nullable,
3891 auto: detected_auto,
3892 fk_inner,
3893 } = detect_type(&field.ty)?;
3894 check_bound_compatibility(field, &attrs, kind)?;
3895 let auto = detected_auto;
3896 if attrs.auto_uuid {
3902 if kind != DetectedKind::Uuid {
3903 return Err(syn::Error::new_spanned(
3904 field,
3905 "`#[rustango(auto_uuid)]` requires the field type to be \
3906 `Auto<uuid::Uuid>`",
3907 ));
3908 }
3909 if !detected_auto {
3910 return Err(syn::Error::new_spanned(
3911 field,
3912 "`#[rustango(auto_uuid)]` requires the field type to be \
3913 wrapped in `Auto<...>` so the macro skips the column on \
3914 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
3915 ));
3916 }
3917 }
3918 if attrs.auto_now_add || attrs.auto_now {
3919 if kind != DetectedKind::DateTime {
3920 return Err(syn::Error::new_spanned(
3921 field,
3922 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
3923 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
3924 ));
3925 }
3926 if !detected_auto {
3927 return Err(syn::Error::new_spanned(
3928 field,
3929 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
3930 the field type to be wrapped in `Auto<...>` so the macro skips \
3931 the column on INSERT and the DB DEFAULT (`now()`) fires",
3932 ));
3933 }
3934 }
3935 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
3936 return Err(syn::Error::new_spanned(
3937 field,
3938 "`#[rustango(soft_delete)]` requires the field type to be \
3939 `Option<chrono::DateTime<chrono::Utc>>`",
3940 ));
3941 }
3942 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
3943 if detected_auto && !primary_key && !is_mixin_auto {
3944 return Err(syn::Error::new_spanned(
3945 field,
3946 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
3947 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
3948 `auto_now`",
3949 ));
3950 }
3951 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
3952 return Err(syn::Error::new_spanned(
3953 field,
3954 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
3955 SERIAL / BIGSERIAL already supplies a default sequence.",
3956 ));
3957 }
3958 if fk_inner.is_some() && primary_key {
3959 return Err(syn::Error::new_spanned(
3960 field,
3961 "`ForeignKey<T>` is not allowed on a primary-key field — \
3962 a row's PK is its own identity, not a reference to a parent.",
3963 ));
3964 }
3965 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
3966 let column_lit = column.as_str();
3967 let field_type_tokens = kind.variant_tokens();
3968 let max_length = optional_u32(attrs.max_length);
3969 let min = optional_i64(attrs.min);
3970 let max = optional_i64(attrs.max);
3971 let default = optional_str(attrs.default.as_deref());
3972
3973 let unique = attrs.unique;
3974 let schema = quote! {
3975 ::rustango::core::FieldSchema {
3976 name: #name,
3977 column: #column_lit,
3978 ty: #field_type_tokens,
3979 nullable: #nullable,
3980 primary_key: #primary_key,
3981 relation: #relation,
3982 max_length: #max_length,
3983 min: #min,
3984 max: #max,
3985 default: #default,
3986 auto: #auto,
3987 unique: #unique,
3988 }
3989 };
3990
3991 let from_row_init = quote! {
3992 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
3993 };
3994 let from_aliased_row_init = quote! {
3995 #ident: ::rustango::sql::sqlx::Row::try_get(
3996 row,
3997 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
3998 )?
3999 };
4000
4001 Ok(FieldInfo {
4002 ident,
4003 column,
4004 primary_key,
4005 auto,
4006 value_ty: &field.ty,
4007 field_type_tokens,
4008 schema,
4009 from_row_init,
4010 from_aliased_row_init,
4011 fk_inner: fk_inner.cloned(),
4012 fk_pk_kind: kind,
4013 nullable,
4014 auto_now: attrs.auto_now,
4015 auto_now_add: attrs.auto_now_add,
4016 soft_delete: attrs.soft_delete,
4017 })
4018}
4019
4020fn check_bound_compatibility(
4021 field: &syn::Field,
4022 attrs: &FieldAttrs,
4023 kind: DetectedKind,
4024) -> syn::Result<()> {
4025 if attrs.max_length.is_some() && kind != DetectedKind::String {
4026 return Err(syn::Error::new_spanned(
4027 field,
4028 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4029 ));
4030 }
4031 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4032 return Err(syn::Error::new_spanned(
4033 field,
4034 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4035 ));
4036 }
4037 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4038 if min > max {
4039 return Err(syn::Error::new_spanned(
4040 field,
4041 format!("`min` ({min}) is greater than `max` ({max})"),
4042 ));
4043 }
4044 }
4045 Ok(())
4046}
4047
4048fn optional_u32(value: Option<u32>) -> TokenStream2 {
4049 if let Some(v) = value {
4050 quote!(::core::option::Option::Some(#v))
4051 } else {
4052 quote!(::core::option::Option::None)
4053 }
4054}
4055
4056fn optional_i64(value: Option<i64>) -> TokenStream2 {
4057 if let Some(v) = value {
4058 quote!(::core::option::Option::Some(#v))
4059 } else {
4060 quote!(::core::option::Option::None)
4061 }
4062}
4063
4064fn optional_str(value: Option<&str>) -> TokenStream2 {
4065 if let Some(v) = value {
4066 quote!(::core::option::Option::Some(#v))
4067 } else {
4068 quote!(::core::option::Option::None)
4069 }
4070}
4071
4072fn relation_tokens(
4073 field: &syn::Field,
4074 attrs: &FieldAttrs,
4075 fk_inner: Option<&syn::Type>,
4076 table: &str,
4077) -> syn::Result<TokenStream2> {
4078 if let Some(inner) = fk_inner {
4079 if attrs.fk.is_some() || attrs.o2o.is_some() {
4080 return Err(syn::Error::new_spanned(
4081 field,
4082 "`ForeignKey<T>` already declares the FK target via the type parameter — \
4083 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4084 ));
4085 }
4086 let on = attrs.on.as_deref().unwrap_or("id");
4087 return Ok(quote! {
4088 ::core::option::Option::Some(::rustango::core::Relation::Fk {
4089 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4090 on: #on,
4091 })
4092 });
4093 }
4094 match (&attrs.fk, &attrs.o2o) {
4095 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4096 field,
4097 "`fk` and `o2o` are mutually exclusive",
4098 )),
4099 (Some(to), None) => {
4100 let on = attrs.on.as_deref().unwrap_or("id");
4101 let resolved = if to == "self" { table } else { to };
4107 Ok(quote! {
4108 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4109 })
4110 }
4111 (None, Some(to)) => {
4112 let on = attrs.on.as_deref().unwrap_or("id");
4113 let resolved = if to == "self" { table } else { to };
4114 Ok(quote! {
4115 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4116 })
4117 }
4118 (None, None) => {
4119 if attrs.on.is_some() {
4120 return Err(syn::Error::new_spanned(
4121 field,
4122 "`on` requires `fk` or `o2o`",
4123 ));
4124 }
4125 Ok(quote!(::core::option::Option::None))
4126 }
4127 }
4128}
4129
4130#[derive(Clone, Copy, PartialEq, Eq)]
4134enum DetectedKind {
4135 I16,
4136 I32,
4137 I64,
4138 F32,
4139 F64,
4140 Bool,
4141 String,
4142 DateTime,
4143 Date,
4144 Uuid,
4145 Json,
4146}
4147
4148impl DetectedKind {
4149 fn variant_tokens(self) -> TokenStream2 {
4150 match self {
4151 Self::I16 => quote!(::rustango::core::FieldType::I16),
4152 Self::I32 => quote!(::rustango::core::FieldType::I32),
4153 Self::I64 => quote!(::rustango::core::FieldType::I64),
4154 Self::F32 => quote!(::rustango::core::FieldType::F32),
4155 Self::F64 => quote!(::rustango::core::FieldType::F64),
4156 Self::Bool => quote!(::rustango::core::FieldType::Bool),
4157 Self::String => quote!(::rustango::core::FieldType::String),
4158 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4159 Self::Date => quote!(::rustango::core::FieldType::Date),
4160 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4161 Self::Json => quote!(::rustango::core::FieldType::Json),
4162 }
4163 }
4164
4165 fn is_integer(self) -> bool {
4166 matches!(self, Self::I16 | Self::I32 | Self::I64)
4167 }
4168
4169 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4177 match self {
4178 Self::I16 => (quote!(I16), quote!(0i16)),
4179 Self::I32 => (quote!(I32), quote!(0i32)),
4180 Self::I64 => (quote!(I64), quote!(0i64)),
4181 Self::F32 => (quote!(F32), quote!(0f32)),
4182 Self::F64 => (quote!(F64), quote!(0f64)),
4183 Self::Bool => (quote!(Bool), quote!(false)),
4184 Self::String => (quote!(String), quote!(::std::string::String::new())),
4185 Self::DateTime => (
4186 quote!(DateTime),
4187 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4188 ),
4189 Self::Date => (
4190 quote!(Date),
4191 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4192 ),
4193 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4194 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4195 }
4196 }
4197}
4198
4199#[derive(Clone, Copy)]
4205struct DetectedType<'a> {
4206 kind: DetectedKind,
4207 nullable: bool,
4208 auto: bool,
4209 fk_inner: Option<&'a syn::Type>,
4210}
4211
4212fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4217 let Type::Path(TypePath { path, qself: None }) = ty else {
4218 return None;
4219 };
4220 let last = path.segments.last()?;
4221 if last.ident != "Auto" {
4222 return None;
4223 }
4224 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4225 return None;
4226 };
4227 args.args.iter().find_map(|a| match a {
4228 syn::GenericArgument::Type(t) => Some(t),
4229 _ => None,
4230 })
4231}
4232
4233fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4234 let Type::Path(TypePath { path, qself: None }) = ty else {
4235 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4236 };
4237 let last = path
4238 .segments
4239 .last()
4240 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4241
4242 if last.ident == "Option" {
4243 let inner = generic_inner(ty, &last.arguments, "Option")?;
4244 let inner_det = detect_type(inner)?;
4245 if inner_det.nullable {
4246 return Err(syn::Error::new_spanned(
4247 ty,
4248 "nested Option is not supported",
4249 ));
4250 }
4251 if inner_det.auto {
4252 return Err(syn::Error::new_spanned(
4253 ty,
4254 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4255 ));
4256 }
4257 return Ok(DetectedType {
4258 nullable: true,
4259 ..inner_det
4260 });
4261 }
4262
4263 if last.ident == "Auto" {
4264 let inner = generic_inner(ty, &last.arguments, "Auto")?;
4265 let inner_det = detect_type(inner)?;
4266 if inner_det.auto {
4267 return Err(syn::Error::new_spanned(
4268 ty,
4269 "nested Auto is not supported",
4270 ));
4271 }
4272 if inner_det.nullable {
4273 return Err(syn::Error::new_spanned(
4274 ty,
4275 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4276 ));
4277 }
4278 if inner_det.fk_inner.is_some() {
4279 return Err(syn::Error::new_spanned(
4280 ty,
4281 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4282 ));
4283 }
4284 if !matches!(
4285 inner_det.kind,
4286 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4287 ) {
4288 return Err(syn::Error::new_spanned(
4289 ty,
4290 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4291 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4292 (DEFAULT now())",
4293 ));
4294 }
4295 return Ok(DetectedType {
4296 auto: true,
4297 ..inner_det
4298 });
4299 }
4300
4301 if last.ident == "ForeignKey" {
4302 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4303 let kind = match key_ty {
4311 Some(k) => detect_type(k)?.kind,
4312 None => DetectedKind::I64,
4313 };
4314 return Ok(DetectedType {
4315 kind,
4316 nullable: false,
4317 auto: false,
4318 fk_inner: Some(inner),
4319 });
4320 }
4321
4322 let kind = match last.ident.to_string().as_str() {
4323 "i16" => DetectedKind::I16,
4324 "i32" => DetectedKind::I32,
4325 "i64" => DetectedKind::I64,
4326 "f32" => DetectedKind::F32,
4327 "f64" => DetectedKind::F64,
4328 "bool" => DetectedKind::Bool,
4329 "String" => DetectedKind::String,
4330 "DateTime" => DetectedKind::DateTime,
4331 "NaiveDate" => DetectedKind::Date,
4332 "Uuid" => DetectedKind::Uuid,
4333 "Value" => DetectedKind::Json,
4334 other => {
4335 return Err(syn::Error::new_spanned(
4336 ty,
4337 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)"),
4338 ));
4339 }
4340 };
4341 Ok(DetectedType {
4342 kind,
4343 nullable: false,
4344 auto: false,
4345 fk_inner: None,
4346 })
4347}
4348
4349fn generic_inner<'a>(
4350 ty: &'a Type,
4351 arguments: &'a PathArguments,
4352 wrapper: &str,
4353) -> syn::Result<&'a Type> {
4354 let PathArguments::AngleBracketed(args) = arguments else {
4355 return Err(syn::Error::new_spanned(
4356 ty,
4357 format!("{wrapper} requires a generic argument"),
4358 ));
4359 };
4360 args.args
4361 .iter()
4362 .find_map(|a| match a {
4363 GenericArgument::Type(t) => Some(t),
4364 _ => None,
4365 })
4366 .ok_or_else(|| {
4367 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4368 })
4369}
4370
4371fn generic_pair<'a>(
4375 ty: &'a Type,
4376 arguments: &'a PathArguments,
4377 wrapper: &str,
4378) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4379 let PathArguments::AngleBracketed(args) = arguments else {
4380 return Err(syn::Error::new_spanned(
4381 ty,
4382 format!("{wrapper} requires a generic argument"),
4383 ));
4384 };
4385 let mut types = args.args.iter().filter_map(|a| match a {
4386 GenericArgument::Type(t) => Some(t),
4387 _ => None,
4388 });
4389 let first = types.next().ok_or_else(|| {
4390 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4391 })?;
4392 let second = types.next();
4393 Ok((first, second))
4394}
4395
4396fn to_snake_case(s: &str) -> String {
4397 let mut out = String::with_capacity(s.len() + 4);
4398 for (i, ch) in s.chars().enumerate() {
4399 if ch.is_ascii_uppercase() {
4400 if i > 0 {
4401 out.push('_');
4402 }
4403 out.push(ch.to_ascii_lowercase());
4404 } else {
4405 out.push(ch);
4406 }
4407 }
4408 out
4409}
4410
4411#[derive(Default)]
4417struct FormFieldAttrs {
4418 min: Option<i64>,
4419 max: Option<i64>,
4420 min_length: Option<u32>,
4421 max_length: Option<u32>,
4422}
4423
4424#[derive(Clone, Copy)]
4426enum FormFieldKind {
4427 String,
4428 I16,
4429 I32,
4430 I64,
4431 F32,
4432 F64,
4433 Bool,
4434}
4435
4436impl FormFieldKind {
4437 fn parse_method(self) -> &'static str {
4438 match self {
4439 Self::I16 => "i16",
4440 Self::I32 => "i32",
4441 Self::I64 => "i64",
4442 Self::F32 => "f32",
4443 Self::F64 => "f64",
4444 Self::String | Self::Bool => "",
4447 }
4448 }
4449}
4450
4451fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4452 let struct_name = &input.ident;
4453
4454 let Data::Struct(data) = &input.data else {
4455 return Err(syn::Error::new_spanned(
4456 struct_name,
4457 "Form can only be derived on structs",
4458 ));
4459 };
4460 let Fields::Named(named) = &data.fields else {
4461 return Err(syn::Error::new_spanned(
4462 struct_name,
4463 "Form requires a struct with named fields",
4464 ));
4465 };
4466
4467 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4468 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4469
4470 for field in &named.named {
4471 let ident = field
4472 .ident
4473 .as_ref()
4474 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4475 let attrs = parse_form_field_attrs(field)?;
4476 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4477
4478 let name_lit = ident.to_string();
4479 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4480 field_blocks.push(parse_block);
4481 field_idents.push(ident);
4482 }
4483
4484 Ok(quote! {
4485 impl ::rustango::forms::Form for #struct_name {
4486 fn parse(
4487 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4488 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4489 let mut __errors = ::rustango::forms::FormErrors::default();
4490 #( #field_blocks )*
4491 if !__errors.is_empty() {
4492 return ::core::result::Result::Err(__errors);
4493 }
4494 ::core::result::Result::Ok(Self {
4495 #( #field_idents ),*
4496 })
4497 }
4498 }
4499 })
4500}
4501
4502fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4503 let mut out = FormFieldAttrs::default();
4504 for attr in &field.attrs {
4505 if !attr.path().is_ident("form") {
4506 continue;
4507 }
4508 attr.parse_nested_meta(|meta| {
4509 if meta.path.is_ident("min") {
4510 let lit: syn::LitInt = meta.value()?.parse()?;
4511 out.min = Some(lit.base10_parse::<i64>()?);
4512 return Ok(());
4513 }
4514 if meta.path.is_ident("max") {
4515 let lit: syn::LitInt = meta.value()?.parse()?;
4516 out.max = Some(lit.base10_parse::<i64>()?);
4517 return Ok(());
4518 }
4519 if meta.path.is_ident("min_length") {
4520 let lit: syn::LitInt = meta.value()?.parse()?;
4521 out.min_length = Some(lit.base10_parse::<u32>()?);
4522 return Ok(());
4523 }
4524 if meta.path.is_ident("max_length") {
4525 let lit: syn::LitInt = meta.value()?.parse()?;
4526 out.max_length = Some(lit.base10_parse::<u32>()?);
4527 return Ok(());
4528 }
4529 Err(meta.error(
4530 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4531 ))
4532 })?;
4533 }
4534 Ok(out)
4535}
4536
4537fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4538 let Type::Path(TypePath { path, qself: None }) = ty else {
4539 return Err(syn::Error::new(
4540 span,
4541 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4542 ));
4543 };
4544 let last = path
4545 .segments
4546 .last()
4547 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4548
4549 if last.ident == "Option" {
4550 let inner = generic_inner(ty, &last.arguments, "Option")?;
4551 let (kind, nested) = detect_form_field(inner, span)?;
4552 if nested {
4553 return Err(syn::Error::new(
4554 span,
4555 "nested Option in Form fields is not supported",
4556 ));
4557 }
4558 return Ok((kind, true));
4559 }
4560
4561 let kind = match last.ident.to_string().as_str() {
4562 "String" => FormFieldKind::String,
4563 "i16" => FormFieldKind::I16,
4564 "i32" => FormFieldKind::I32,
4565 "i64" => FormFieldKind::I64,
4566 "f32" => FormFieldKind::F32,
4567 "f64" => FormFieldKind::F64,
4568 "bool" => FormFieldKind::Bool,
4569 other => {
4570 return Err(syn::Error::new(
4571 span,
4572 format!(
4573 "Form field type `{other}` is not supported in v0.8 — use String / \
4574 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4575 ),
4576 ));
4577 }
4578 };
4579 Ok((kind, false))
4580}
4581
4582#[allow(clippy::too_many_lines)]
4583fn render_form_field_parse(
4584 ident: &syn::Ident,
4585 name_lit: &str,
4586 kind: FormFieldKind,
4587 nullable: bool,
4588 attrs: &FormFieldAttrs,
4589) -> TokenStream2 {
4590 let lookup = quote! {
4593 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4594 };
4595
4596 let parsed_value = match kind {
4597 FormFieldKind::Bool => quote! {
4598 let __v: bool = match __raw {
4599 ::core::option::Option::None => false,
4600 ::core::option::Option::Some(__s) => !matches!(
4601 __s.to_ascii_lowercase().as_str(),
4602 "" | "false" | "0" | "off" | "no"
4603 ),
4604 };
4605 },
4606 FormFieldKind::String => {
4607 if nullable {
4608 quote! {
4609 let __v: ::core::option::Option<::std::string::String> = match __raw {
4610 ::core::option::Option::None => ::core::option::Option::None,
4611 ::core::option::Option::Some(__s) if __s.is_empty() => {
4612 ::core::option::Option::None
4613 }
4614 ::core::option::Option::Some(__s) => {
4615 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4616 }
4617 };
4618 }
4619 } else {
4620 quote! {
4621 let __v: ::std::string::String = match __raw {
4622 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4623 ::core::clone::Clone::clone(__s)
4624 }
4625 _ => {
4626 __errors.add(#name_lit, "This field is required.");
4627 ::std::string::String::new()
4628 }
4629 };
4630 }
4631 }
4632 }
4633 FormFieldKind::I16
4634 | FormFieldKind::I32
4635 | FormFieldKind::I64
4636 | FormFieldKind::F32
4637 | FormFieldKind::F64 => {
4638 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
4639 let ty_lit = kind.parse_method();
4640 let default_val = match kind {
4641 FormFieldKind::I16 => quote! { 0i16 },
4642 FormFieldKind::I32 => quote! { 0i32 },
4643 FormFieldKind::I64 => quote! { 0i64 },
4644 FormFieldKind::F32 => quote! { 0f32 },
4645 FormFieldKind::F64 => quote! { 0f64 },
4646 _ => quote! { Default::default() },
4647 };
4648 if nullable {
4649 quote! {
4650 let __v: ::core::option::Option<#parse_ty> = match __raw {
4651 ::core::option::Option::None => ::core::option::Option::None,
4652 ::core::option::Option::Some(__s) if __s.is_empty() => {
4653 ::core::option::Option::None
4654 }
4655 ::core::option::Option::Some(__s) => {
4656 match __s.parse::<#parse_ty>() {
4657 ::core::result::Result::Ok(__n) => {
4658 ::core::option::Option::Some(__n)
4659 }
4660 ::core::result::Result::Err(__e) => {
4661 __errors.add(
4662 #name_lit,
4663 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4664 );
4665 ::core::option::Option::None
4666 }
4667 }
4668 }
4669 };
4670 }
4671 } else {
4672 quote! {
4673 let __v: #parse_ty = match __raw {
4674 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4675 match __s.parse::<#parse_ty>() {
4676 ::core::result::Result::Ok(__n) => __n,
4677 ::core::result::Result::Err(__e) => {
4678 __errors.add(
4679 #name_lit,
4680 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4681 );
4682 #default_val
4683 }
4684 }
4685 }
4686 _ => {
4687 __errors.add(#name_lit, "This field is required.");
4688 #default_val
4689 }
4690 };
4691 }
4692 }
4693 }
4694 };
4695
4696 let validators = render_form_validators(name_lit, kind, nullable, attrs);
4697
4698 quote! {
4699 let #ident = {
4700 #lookup
4701 #parsed_value
4702 #validators
4703 __v
4704 };
4705 }
4706}
4707
4708fn render_form_validators(
4709 name_lit: &str,
4710 kind: FormFieldKind,
4711 nullable: bool,
4712 attrs: &FormFieldAttrs,
4713) -> TokenStream2 {
4714 let mut checks: Vec<TokenStream2> = Vec::new();
4715
4716 let val_ref = if nullable {
4717 quote! { __v.as_ref() }
4718 } else {
4719 quote! { ::core::option::Option::Some(&__v) }
4720 };
4721
4722 let is_string = matches!(kind, FormFieldKind::String);
4723 let is_numeric = matches!(
4724 kind,
4725 FormFieldKind::I16
4726 | FormFieldKind::I32
4727 | FormFieldKind::I64
4728 | FormFieldKind::F32
4729 | FormFieldKind::F64
4730 );
4731
4732 if is_string {
4733 if let Some(min_len) = attrs.min_length {
4734 let min_len_usize = min_len as usize;
4735 checks.push(quote! {
4736 if let ::core::option::Option::Some(__s) = #val_ref {
4737 if __s.len() < #min_len_usize {
4738 __errors.add(
4739 #name_lit,
4740 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
4741 );
4742 }
4743 }
4744 });
4745 }
4746 if let Some(max_len) = attrs.max_length {
4747 let max_len_usize = max_len as usize;
4748 checks.push(quote! {
4749 if let ::core::option::Option::Some(__s) = #val_ref {
4750 if __s.len() > #max_len_usize {
4751 __errors.add(
4752 #name_lit,
4753 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
4754 );
4755 }
4756 }
4757 });
4758 }
4759 }
4760
4761 if is_numeric {
4762 if let Some(min) = attrs.min {
4763 checks.push(quote! {
4764 if let ::core::option::Option::Some(__n) = #val_ref {
4765 if (*__n as f64) < (#min as f64) {
4766 __errors.add(
4767 #name_lit,
4768 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
4769 );
4770 }
4771 }
4772 });
4773 }
4774 if let Some(max) = attrs.max {
4775 checks.push(quote! {
4776 if let ::core::option::Option::Some(__n) = #val_ref {
4777 if (*__n as f64) > (#max as f64) {
4778 __errors.add(
4779 #name_lit,
4780 ::std::format!("Ensure this value is less than or equal to {}.", #max),
4781 );
4782 }
4783 }
4784 });
4785 }
4786 }
4787
4788 quote! { #( #checks )* }
4789}
4790
4791struct ViewSetAttrs {
4796 model: syn::Path,
4797 fields: Option<Vec<String>>,
4798 filter_fields: Vec<String>,
4799 search_fields: Vec<String>,
4800 ordering: Vec<(String, bool)>,
4802 page_size: Option<usize>,
4803 read_only: bool,
4804 perms: ViewSetPermsAttrs,
4805}
4806
4807#[derive(Default)]
4808struct ViewSetPermsAttrs {
4809 list: Vec<String>,
4810 retrieve: Vec<String>,
4811 create: Vec<String>,
4812 update: Vec<String>,
4813 destroy: Vec<String>,
4814}
4815
4816fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
4817 let struct_name = &input.ident;
4818
4819 match &input.data {
4821 Data::Struct(s) => match &s.fields {
4822 Fields::Unit | Fields::Named(_) => {}
4823 Fields::Unnamed(_) => {
4824 return Err(syn::Error::new_spanned(
4825 struct_name,
4826 "ViewSet can only be derived on a unit struct or an empty named struct",
4827 ));
4828 }
4829 },
4830 _ => {
4831 return Err(syn::Error::new_spanned(
4832 struct_name,
4833 "ViewSet can only be derived on a struct",
4834 ));
4835 }
4836 }
4837
4838 let attrs = parse_viewset_attrs(input)?;
4839 let model_path = &attrs.model;
4840
4841 let fields_call = if let Some(ref fields) = attrs.fields {
4843 let lits = fields.iter().map(|f| f.as_str());
4844 quote!(.fields(&[ #(#lits),* ]))
4845 } else {
4846 quote!()
4847 };
4848
4849 let filter_fields_call = if attrs.filter_fields.is_empty() {
4850 quote!()
4851 } else {
4852 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
4853 quote!(.filter_fields(&[ #(#lits),* ]))
4854 };
4855
4856 let search_fields_call = if attrs.search_fields.is_empty() {
4857 quote!()
4858 } else {
4859 let lits = attrs.search_fields.iter().map(|f| f.as_str());
4860 quote!(.search_fields(&[ #(#lits),* ]))
4861 };
4862
4863 let ordering_call = if attrs.ordering.is_empty() {
4864 quote!()
4865 } else {
4866 let pairs = attrs.ordering.iter().map(|(f, desc)| {
4867 let f = f.as_str();
4868 quote!((#f, #desc))
4869 });
4870 quote!(.ordering(&[ #(#pairs),* ]))
4871 };
4872
4873 let page_size_call = if let Some(n) = attrs.page_size {
4874 quote!(.page_size(#n))
4875 } else {
4876 quote!()
4877 };
4878
4879 let read_only_call = if attrs.read_only {
4880 quote!(.read_only())
4881 } else {
4882 quote!()
4883 };
4884
4885 let perms = &attrs.perms;
4886 let perms_call = if perms.list.is_empty()
4887 && perms.retrieve.is_empty()
4888 && perms.create.is_empty()
4889 && perms.update.is_empty()
4890 && perms.destroy.is_empty()
4891 {
4892 quote!()
4893 } else {
4894 let list_lits = perms.list.iter().map(|s| s.as_str());
4895 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
4896 let create_lits = perms.create.iter().map(|s| s.as_str());
4897 let update_lits = perms.update.iter().map(|s| s.as_str());
4898 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
4899 quote! {
4900 .permissions(::rustango::viewset::ViewSetPerms {
4901 list: ::std::vec![ #(#list_lits.to_owned()),* ],
4902 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
4903 create: ::std::vec![ #(#create_lits.to_owned()),* ],
4904 update: ::std::vec![ #(#update_lits.to_owned()),* ],
4905 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
4906 })
4907 }
4908 };
4909
4910 Ok(quote! {
4911 impl #struct_name {
4912 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
4915 ::rustango::viewset::ViewSet::for_model(#model_path::SCHEMA)
4916 #fields_call
4917 #filter_fields_call
4918 #search_fields_call
4919 #ordering_call
4920 #page_size_call
4921 #perms_call
4922 #read_only_call
4923 .router(prefix, pool)
4924 }
4925 }
4926 })
4927}
4928
4929fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
4930 let mut model: Option<syn::Path> = None;
4931 let mut fields: Option<Vec<String>> = None;
4932 let mut filter_fields: Vec<String> = Vec::new();
4933 let mut search_fields: Vec<String> = Vec::new();
4934 let mut ordering: Vec<(String, bool)> = Vec::new();
4935 let mut page_size: Option<usize> = None;
4936 let mut read_only = false;
4937 let mut perms = ViewSetPermsAttrs::default();
4938
4939 for attr in &input.attrs {
4940 if !attr.path().is_ident("viewset") {
4941 continue;
4942 }
4943 attr.parse_nested_meta(|meta| {
4944 if meta.path.is_ident("model") {
4945 let path: syn::Path = meta.value()?.parse()?;
4946 model = Some(path);
4947 return Ok(());
4948 }
4949 if meta.path.is_ident("fields") {
4950 let s: LitStr = meta.value()?.parse()?;
4951 fields = Some(split_field_list(&s.value()));
4952 return Ok(());
4953 }
4954 if meta.path.is_ident("filter_fields") {
4955 let s: LitStr = meta.value()?.parse()?;
4956 filter_fields = split_field_list(&s.value());
4957 return Ok(());
4958 }
4959 if meta.path.is_ident("search_fields") {
4960 let s: LitStr = meta.value()?.parse()?;
4961 search_fields = split_field_list(&s.value());
4962 return Ok(());
4963 }
4964 if meta.path.is_ident("ordering") {
4965 let s: LitStr = meta.value()?.parse()?;
4966 ordering = parse_ordering_list(&s.value());
4967 return Ok(());
4968 }
4969 if meta.path.is_ident("page_size") {
4970 let lit: syn::LitInt = meta.value()?.parse()?;
4971 page_size = Some(lit.base10_parse::<usize>()?);
4972 return Ok(());
4973 }
4974 if meta.path.is_ident("read_only") {
4975 read_only = true;
4976 return Ok(());
4977 }
4978 if meta.path.is_ident("permissions") {
4979 meta.parse_nested_meta(|inner| {
4980 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
4981 let s: LitStr = inner.value()?.parse()?;
4982 Ok(split_field_list(&s.value()))
4983 };
4984 if inner.path.is_ident("list") {
4985 perms.list = parse_codenames(&inner)?;
4986 } else if inner.path.is_ident("retrieve") {
4987 perms.retrieve = parse_codenames(&inner)?;
4988 } else if inner.path.is_ident("create") {
4989 perms.create = parse_codenames(&inner)?;
4990 } else if inner.path.is_ident("update") {
4991 perms.update = parse_codenames(&inner)?;
4992 } else if inner.path.is_ident("destroy") {
4993 perms.destroy = parse_codenames(&inner)?;
4994 } else {
4995 return Err(inner.error(
4996 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
4997 ));
4998 }
4999 Ok(())
5000 })?;
5001 return Ok(());
5002 }
5003 Err(meta.error(
5004 "unknown viewset attribute (supported: model, fields, filter_fields, \
5005 search_fields, ordering, page_size, read_only, permissions(...))",
5006 ))
5007 })?;
5008 }
5009
5010 let model = model.ok_or_else(|| {
5011 syn::Error::new_spanned(
5012 &input.ident,
5013 "`#[viewset(model = SomeModel)]` is required",
5014 )
5015 })?;
5016
5017 Ok(ViewSetAttrs {
5018 model,
5019 fields,
5020 filter_fields,
5021 search_fields,
5022 ordering,
5023 page_size,
5024 read_only,
5025 perms,
5026 })
5027}
5028
5029struct SerializerContainerAttrs {
5032 model: syn::Path,
5033}
5034
5035#[derive(Default)]
5036struct SerializerFieldAttrs {
5037 read_only: bool,
5038 write_only: bool,
5039 source: Option<String>,
5040 skip: bool,
5041 method: Option<String>,
5045 validate: Option<String>,
5050 nested: bool,
5060 nested_strict: bool,
5065 many: Option<syn::Type>,
5074}
5075
5076fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5077 let mut model: Option<syn::Path> = None;
5078 for attr in &input.attrs {
5079 if !attr.path().is_ident("serializer") {
5080 continue;
5081 }
5082 attr.parse_nested_meta(|meta| {
5083 if meta.path.is_ident("model") {
5084 let _eq: syn::Token![=] = meta.input.parse()?;
5085 model = Some(meta.input.parse()?);
5086 return Ok(());
5087 }
5088 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5089 })?;
5090 }
5091 let model = model.ok_or_else(|| {
5092 syn::Error::new_spanned(
5093 &input.ident,
5094 "`#[serializer(model = SomeModel)]` is required",
5095 )
5096 })?;
5097 Ok(SerializerContainerAttrs { model })
5098}
5099
5100fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5101 let mut out = SerializerFieldAttrs::default();
5102 for attr in &field.attrs {
5103 if !attr.path().is_ident("serializer") {
5104 continue;
5105 }
5106 attr.parse_nested_meta(|meta| {
5107 if meta.path.is_ident("read_only") {
5108 out.read_only = true;
5109 return Ok(());
5110 }
5111 if meta.path.is_ident("write_only") {
5112 out.write_only = true;
5113 return Ok(());
5114 }
5115 if meta.path.is_ident("skip") {
5116 out.skip = true;
5117 return Ok(());
5118 }
5119 if meta.path.is_ident("source") {
5120 let s: LitStr = meta.value()?.parse()?;
5121 out.source = Some(s.value());
5122 return Ok(());
5123 }
5124 if meta.path.is_ident("method") {
5125 let s: LitStr = meta.value()?.parse()?;
5126 out.method = Some(s.value());
5127 return Ok(());
5128 }
5129 if meta.path.is_ident("validate") {
5130 let s: LitStr = meta.value()?.parse()?;
5131 out.validate = Some(s.value());
5132 return Ok(());
5133 }
5134 if meta.path.is_ident("many") {
5135 let _eq: syn::Token![=] = meta.input.parse()?;
5136 out.many = Some(meta.input.parse()?);
5137 return Ok(());
5138 }
5139 if meta.path.is_ident("nested") {
5140 out.nested = true;
5141 if meta.input.peek(syn::token::Paren) {
5144 meta.parse_nested_meta(|inner| {
5145 if inner.path.is_ident("strict") {
5146 out.nested_strict = true;
5147 return Ok(());
5148 }
5149 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5150 })?;
5151 }
5152 return Ok(());
5153 }
5154 Err(meta.error(
5155 "unknown serializer field attribute (supported: \
5156 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5157 ))
5158 })?;
5159 }
5160 if out.read_only && out.write_only {
5162 return Err(syn::Error::new_spanned(
5163 field,
5164 "a field cannot be both `read_only` and `write_only`",
5165 ));
5166 }
5167 if out.method.is_some() && out.source.is_some() {
5168 return Err(syn::Error::new_spanned(
5169 field,
5170 "`method` and `source` are mutually exclusive — `method` computes \
5171 the value from a method, `source` reads it from a different model field",
5172 ));
5173 }
5174 Ok(out)
5175}
5176
5177fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5178 let struct_name = &input.ident;
5179 let struct_name_lit = struct_name.to_string();
5180
5181 let Data::Struct(data) = &input.data else {
5182 return Err(syn::Error::new_spanned(
5183 struct_name,
5184 "Serializer can only be derived on structs",
5185 ));
5186 };
5187 let Fields::Named(named) = &data.fields else {
5188 return Err(syn::Error::new_spanned(
5189 struct_name,
5190 "Serializer requires a struct with named fields",
5191 ));
5192 };
5193
5194 let container = parse_serializer_container_attrs(input)?;
5195 let model_path = &container.model;
5196
5197 #[allow(dead_code)]
5201 struct FieldInfo {
5202 ident: syn::Ident,
5203 ty: syn::Type,
5204 attrs: SerializerFieldAttrs,
5205 }
5206 let mut fields_info: Vec<FieldInfo> = Vec::new();
5207 for field in &named.named {
5208 let ident = field.ident.clone().expect("named field has ident");
5209 let attrs = parse_serializer_field_attrs(field)?;
5210 fields_info.push(FieldInfo {
5211 ident,
5212 ty: field.ty.clone(),
5213 attrs,
5214 });
5215 }
5216
5217 let from_model_fields = fields_info.iter().map(|fi| {
5219 let ident = &fi.ident;
5220 let ty = &fi.ty;
5221 if let Some(_inner) = &fi.attrs.many {
5222 quote! { #ident: ::std::vec::Vec::new() }
5226 } else if let Some(method) = &fi.attrs.method {
5227 let method_ident = syn::Ident::new(method, ident.span());
5231 quote! { #ident: Self::#method_ident(model) }
5232 } else if fi.attrs.nested {
5233 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5249 let src_ident = syn::Ident::new(&src_name, ident.span());
5250 if fi.attrs.nested_strict {
5251 let panic_msg = format!(
5252 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5253 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5254 );
5255 quote! {
5256 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5257 model.#src_ident.value().expect(#panic_msg),
5258 )
5259 }
5260 } else {
5261 quote! {
5262 #ident: match model.#src_ident.value() {
5263 ::core::option::Option::Some(__loaded) =>
5264 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5265 ::core::option::Option::None =>
5266 ::core::default::Default::default(),
5267 }
5268 }
5269 }
5270 } else if fi.attrs.write_only || fi.attrs.skip {
5271 quote! { #ident: ::core::default::Default::default() }
5273 } else if let Some(src) = &fi.attrs.source {
5274 let src_ident = syn::Ident::new(src, ident.span());
5275 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5276 } else {
5277 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5278 }
5279 });
5280
5281 let validator_calls: Vec<_> = fields_info.iter().filter_map(|fi| {
5285 let ident = &fi.ident;
5286 let name_lit = ident.to_string();
5287 let method = fi.attrs.validate.as_ref()?;
5288 let method_ident = syn::Ident::new(method, ident.span());
5289 Some(quote! {
5290 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5291 __errors.add(#name_lit.to_owned(), __e);
5292 }
5293 })
5294 }).collect();
5295 let validate_method = if validator_calls.is_empty() {
5296 quote! {}
5297 } else {
5298 quote! {
5299 impl #struct_name {
5300 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5304 let mut __errors = ::rustango::forms::FormErrors::default();
5305 #( #validator_calls )*
5306 if __errors.is_empty() {
5307 ::core::result::Result::Ok(())
5308 } else {
5309 ::core::result::Result::Err(__errors)
5310 }
5311 }
5312 }
5313 }
5314 };
5315
5316 let many_setters: Vec<_> = fields_info.iter().filter_map(|fi| {
5320 let many_ty = fi.attrs.many.as_ref()?;
5321 let ident = &fi.ident;
5322 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5323 Some(quote! {
5324 pub fn #setter(
5329 &mut self,
5330 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5331 ) -> &mut Self {
5332 self.#ident = models.iter()
5333 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5334 .collect();
5335 self
5336 }
5337 })
5338 }).collect();
5339 let many_setters_impl = if many_setters.is_empty() {
5340 quote! {}
5341 } else {
5342 quote! {
5343 impl #struct_name {
5344 #( #many_setters )*
5345 }
5346 }
5347 };
5348
5349 let output_fields: Vec<_> = fields_info
5351 .iter()
5352 .filter(|fi| !fi.attrs.write_only)
5353 .collect();
5354 let output_field_count = output_fields.len();
5355 let serialize_fields = output_fields.iter().map(|fi| {
5356 let ident = &fi.ident;
5357 let name_lit = ident.to_string();
5358 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5359 });
5360
5361 let writable_lits: Vec<_> = fields_info
5363 .iter()
5364 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5365 .map(|fi| fi.ident.to_string())
5366 .collect();
5367
5368 let openapi_impl = {
5372 #[cfg(feature = "openapi")]
5373 {
5374 let property_calls = output_fields.iter().map(|fi| {
5375 let ident = &fi.ident;
5376 let name_lit = ident.to_string();
5377 let ty = &fi.ty;
5378 let nullable_call = if is_option(ty) {
5379 quote! { .nullable() }
5380 } else {
5381 quote! {}
5382 };
5383 quote! {
5384 .property(
5385 #name_lit,
5386 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5387 #nullable_call,
5388 )
5389 }
5390 });
5391 let required_lits: Vec<_> = output_fields
5392 .iter()
5393 .filter(|fi| !is_option(&fi.ty))
5394 .map(|fi| fi.ident.to_string())
5395 .collect();
5396 quote! {
5397 impl ::rustango::openapi::OpenApiSchema for #struct_name {
5398 fn openapi_schema() -> ::rustango::openapi::Schema {
5399 ::rustango::openapi::Schema::object()
5400 #( #property_calls )*
5401 .required([ #( #required_lits ),* ])
5402 }
5403 }
5404 }
5405 }
5406 #[cfg(not(feature = "openapi"))]
5407 {
5408 quote! {}
5409 }
5410 };
5411
5412 Ok(quote! {
5413 impl ::rustango::serializer::ModelSerializer for #struct_name {
5414 type Model = #model_path;
5415
5416 fn from_model(model: &Self::Model) -> Self {
5417 Self {
5418 #( #from_model_fields ),*
5419 }
5420 }
5421
5422 fn writable_fields() -> &'static [&'static str] {
5423 &[ #( #writable_lits ),* ]
5424 }
5425 }
5426
5427 impl ::serde::Serialize for #struct_name {
5428 fn serialize<S>(&self, serializer: S)
5429 -> ::core::result::Result<S::Ok, S::Error>
5430 where
5431 S: ::serde::Serializer,
5432 {
5433 use ::serde::ser::SerializeStruct;
5434 let mut __state = serializer.serialize_struct(
5435 #struct_name_lit,
5436 #output_field_count,
5437 )?;
5438 #( #serialize_fields )*
5439 __state.end()
5440 }
5441 }
5442
5443 #openapi_impl
5444
5445 #validate_method
5446
5447 #many_setters_impl
5448 })
5449}
5450
5451#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5455fn is_option(ty: &syn::Type) -> bool {
5456 if let syn::Type::Path(p) = ty {
5457 if let Some(last) = p.path.segments.last() {
5458 return last.ident == "Option";
5459 }
5460 }
5461 false
5462}