1use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20 let input = parse_macro_input!(input as DeriveInput);
21 expand(&input)
22 .unwrap_or_else(syn::Error::into_compile_error)
23 .into()
24}
25
26#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60 let input = parse_macro_input!(input as DeriveInput);
61 expand_viewset(&input)
62 .unwrap_or_else(syn::Error::into_compile_error)
63 .into()
64}
65
66#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95 let input = parse_macro_input!(input as DeriveInput);
96 expand_form(&input)
97 .unwrap_or_else(syn::Error::into_compile_error)
98 .into()
99}
100
101#[proc_macro_derive(Serializer, attributes(serializer))]
121pub fn derive_serializer(input: TokenStream) -> TokenStream {
122 let input = parse_macro_input!(input as DeriveInput);
123 expand_serializer(&input)
124 .unwrap_or_else(syn::Error::into_compile_error)
125 .into()
126}
127
128#[proc_macro]
163pub fn embed_migrations(input: TokenStream) -> TokenStream {
164 expand_embed_migrations(input.into())
165 .unwrap_or_else(syn::Error::into_compile_error)
166 .into()
167}
168
169#[proc_macro_attribute]
192pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
193 expand_main(args.into(), item.into())
194 .unwrap_or_else(syn::Error::into_compile_error)
195 .into()
196}
197
198fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
199 let mut input: syn::ItemFn = syn::parse2(item)?;
200 if input.sig.asyncness.is_none() {
201 return Err(syn::Error::new(
202 input.sig.ident.span(),
203 "`#[rustango::main]` must wrap an `async fn`",
204 ));
205 }
206
207 let flavor = parse_flavor(&args);
219 let builder_call = match flavor {
220 Flavor::CurrentThread => quote! {
221 ::rustango::__private_runtime::tokio::runtime::Builder::new_current_thread()
222 },
223 Flavor::MultiThread => quote! {
224 ::rustango::__private_runtime::tokio::runtime::Builder::new_multi_thread()
225 },
226 };
227
228 let user_body = input.block.clone();
231 input.sig.asyncness = None;
232 input.block = syn::parse2(quote! {{
233 {
234 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
235 let _ = tracing_subscriber::fmt()
238 .with_env_filter(
239 EnvFilter::try_from_default_env()
240 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
241 )
242 .try_init();
243 }
244 let __rt = #builder_call
245 .enable_all()
246 .build()
247 .expect("failed to build tokio runtime");
248 __rt.block_on(async move #user_body)
249 }})?;
250
251 Ok(quote! {
252 #input
253 })
254}
255
256enum Flavor {
257 MultiThread,
258 CurrentThread,
259}
260
261fn parse_flavor(args: &TokenStream2) -> Flavor {
262 let s = args.to_string();
266 if s.contains("current_thread") {
267 Flavor::CurrentThread
268 } else {
269 Flavor::MultiThread
270 }
271}
272
273fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
274 let path_str = if input.is_empty() {
276 "./migrations".to_string()
277 } else {
278 let lit: LitStr = syn::parse2(input)?;
279 lit.value()
280 };
281
282 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
283 syn::Error::new(
284 proc_macro2::Span::call_site(),
285 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
286 )
287 })?;
288 let abs = std::path::Path::new(&manifest).join(&path_str);
289
290 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
291 if abs.is_dir() {
292 let read = std::fs::read_dir(&abs).map_err(|e| {
293 syn::Error::new(
294 proc_macro2::Span::call_site(),
295 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
296 )
297 })?;
298 for entry in read.flatten() {
299 let path = entry.path();
300 if !path.is_file() {
301 continue;
302 }
303 if path.extension().and_then(|s| s.to_str()) != Some("json") {
304 continue;
305 }
306 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
307 continue;
308 };
309 entries.push((stem.to_owned(), path));
310 }
311 }
312 entries.sort_by(|a, b| a.0.cmp(&b.0));
313
314 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
327 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
328 for (stem, path) in &entries {
329 let raw = std::fs::read_to_string(path).map_err(|e| {
330 syn::Error::new(
331 proc_macro2::Span::call_site(),
332 format!(
333 "embed_migrations!: cannot read {} for chain validation: {e}",
334 path.display()
335 ),
336 )
337 })?;
338 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
339 syn::Error::new(
340 proc_macro2::Span::call_site(),
341 format!(
342 "embed_migrations!: {} is not valid JSON: {e}",
343 path.display()
344 ),
345 )
346 })?;
347 let name = json
348 .get("name")
349 .and_then(|v| v.as_str())
350 .ok_or_else(|| {
351 syn::Error::new(
352 proc_macro2::Span::call_site(),
353 format!(
354 "embed_migrations!: {} is missing the `name` field",
355 path.display()
356 ),
357 )
358 })?
359 .to_owned();
360 if name != *stem {
361 return Err(syn::Error::new(
362 proc_macro2::Span::call_site(),
363 format!(
364 "embed_migrations!: file stem `{stem}` does not match the migration's \
365 `name` field `{name}` — rename the file or fix the JSON",
366 ),
367 ));
368 }
369 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
370 chain_names.push(name.clone());
371 prev_refs.push((name, prev));
372 }
373
374 let name_set: std::collections::HashSet<&str> =
375 chain_names.iter().map(String::as_str).collect();
376 for (name, prev) in &prev_refs {
377 if let Some(p) = prev {
378 if !name_set.contains(p.as_str()) {
379 return Err(syn::Error::new(
380 proc_macro2::Span::call_site(),
381 format!(
382 "embed_migrations!: broken migration chain — `{name}` declares \
383 prev=`{p}` but no migration with that name exists in {}",
384 abs.display()
385 ),
386 ));
387 }
388 }
389 }
390
391 let pairs: Vec<TokenStream2> = entries
392 .iter()
393 .map(|(name, path)| {
394 let path_lit = path.display().to_string();
395 quote! { (#name, ::core::include_str!(#path_lit)) }
396 })
397 .collect();
398
399 Ok(quote! {
400 {
401 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
402 __RUSTANGO_EMBEDDED
403 }
404 })
405}
406
407fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
408 let struct_name = &input.ident;
409
410 let Data::Struct(data) = &input.data else {
411 return Err(syn::Error::new_spanned(
412 struct_name,
413 "Model can only be derived on structs",
414 ));
415 };
416 let Fields::Named(named) = &data.fields else {
417 return Err(syn::Error::new_spanned(
418 struct_name,
419 "Model requires a struct with named fields",
420 ));
421 };
422
423 let container = parse_container_attrs(input)?;
424 let table = container
425 .table
426 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
427 let model_name = struct_name.to_string();
428
429 let collected = collect_fields(named, &table)?;
430
431 if let Some((ref display, span)) = container.display {
433 if !collected.field_names.iter().any(|n| n == display) {
434 return Err(syn::Error::new(
435 span,
436 format!("`display = \"{display}\"` does not match any field on this struct"),
437 ));
438 }
439 }
440 let display = container.display.map(|(name, _)| name);
441 let app_label = container.app.clone();
442
443 if let Some(admin) = &container.admin {
453 for (label, list) in [
454 ("search_fields", &admin.search_fields),
455 ("readonly_fields", &admin.readonly_fields),
456 ("list_filter", &admin.list_filter),
457 ] {
458 if let Some((names, span)) = list {
459 for name in names {
460 if !collected.field_names.iter().any(|n| n == name) {
461 return Err(syn::Error::new(
462 *span,
463 format!(
464 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
465 ),
466 ));
467 }
468 }
469 }
470 }
471 if let Some((pairs, span)) = &admin.ordering {
472 for (name, _) in pairs {
473 if !collected.field_names.iter().any(|n| n == name) {
474 return Err(syn::Error::new(
475 *span,
476 format!(
477 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
478 ),
479 ));
480 }
481 }
482 }
483 if let Some((groups, span)) = &admin.fieldsets {
484 for (_, fields) in groups {
485 for name in fields {
486 if !collected.field_names.iter().any(|n| n == name) {
487 return Err(syn::Error::new(
488 *span,
489 format!(
490 "`fieldsets`: \"{name}\" is not a declared field on this struct"
491 ),
492 ));
493 }
494 }
495 }
496 }
497 }
498 if let Some(audit) = &container.audit {
499 if let Some((names, span)) = &audit.track {
500 for name in names {
501 if !collected.field_names.iter().any(|n| n == name) {
502 return Err(syn::Error::new(
503 *span,
504 format!(
505 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
506 ),
507 ));
508 }
509 }
510 }
511 }
512
513 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
516 audit
517 .track
518 .as_ref()
519 .map(|(names, _)| names.clone())
520 .unwrap_or_default()
521 });
522
523 let mut all_indexes: Vec<IndexAttr> = container.indexes;
525 for field in &named.named {
526 let ident = field.ident.as_ref().expect("named");
527 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
530 if fa.index {
531 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
532 let auto_name = if fa.index_unique {
533 format!("{table}_{col_name}_uq_idx")
534 } else {
535 format!("{table}_{col_name}_idx")
536 };
537 all_indexes.push(IndexAttr {
538 name: fa.index_name.or(Some(auto_name)),
539 columns: vec![col_name],
540 unique: fa.index_unique,
541 method: fa.index_method,
542 });
543 }
544 }
545 }
546
547 let model_impl = model_impl_tokens(
548 struct_name,
549 &model_name,
550 &table,
551 display.as_deref(),
552 app_label.as_deref(),
553 container.admin.as_ref(),
554 &collected.field_schemas,
555 collected.soft_delete_column.as_deref(),
556 container.permissions,
557 audit_track_names.as_deref(),
558 &container.m2m,
559 &all_indexes,
560 &container.checks,
561 &container.composite_fks,
562 &container.generic_fks,
563 container.scope.as_deref(),
564 );
565 let module_ident = column_module_ident(struct_name);
566 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
567 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
568 let track_set: Option<std::collections::HashSet<&str>> = audit
569 .track
570 .as_ref()
571 .map(|(names, _)| names.iter().map(String::as_str).collect());
572 collected
573 .column_entries
574 .iter()
575 .filter(|c| {
576 track_set
577 .as_ref()
578 .map_or(true, |s| s.contains(c.name.as_str()))
579 })
580 .collect()
581 });
582 let inherent_impl = inherent_impl_tokens(
583 struct_name,
584 &collected,
585 collected.primary_key.as_ref(),
586 &column_consts,
587 audited_fields.as_deref(),
588 &all_indexes,
589 );
590 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
591 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
592 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
593 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
594 let generic_fk_accessors = generic_fk_accessor_tokens(
595 struct_name,
596 &container.generic_fks,
597 &collected.column_entries,
598 );
599
600 Ok(quote! {
601 #model_impl
602 #inherent_impl
603 #from_row_impl
604 #column_module
605 #reverse_helpers
606 #m2m_accessors
607 #generic_fk_accessors
608
609 ::rustango::core::inventory::submit! {
610 ::rustango::core::ModelEntry {
611 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
612 module_path: ::core::module_path!(),
617 }
618 }
619 })
620}
621
622fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
633 let arms = fk_relations.iter().map(|rel| {
634 let parent_ty = &rel.parent_type;
635 let fk_col = rel.fk_column.as_str();
636 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
639 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
640 let assign = if rel.nullable {
641 quote! {
642 self.#field_ident = ::core::option::Option::Some(
643 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
644 );
645 }
646 } else {
647 quote! {
648 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
649 }
650 };
651 quote! {
652 #fk_col => {
653 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
654 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
661 ::rustango::core::SqlValue::#variant_ident(v) => v,
662 _other => {
663 ::core::debug_assert!(
664 false,
665 "rustango macro bug: load_related on FK `{}` expected \
666 SqlValue::{} from parent's __rustango_pk_value but got \
667 {:?} — file a bug at https://github.com/ujeenet/rustango",
668 #fk_col,
669 ::core::stringify!(#variant_ident),
670 _other,
671 );
672 #default_expr
673 }
674 };
675 #assign
676 ::core::result::Result::Ok(true)
677 }
678 }
679 });
680 quote! {
681 #[cfg(feature = "postgres")]
682 impl ::rustango::sql::LoadRelated for #struct_name {
683 #[allow(unused_variables)]
684 fn __rustango_load_related(
685 &mut self,
686 row: &::rustango::sql::sqlx::postgres::PgRow,
687 field_name: &str,
688 alias: &str,
689 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
690 match field_name {
691 #( #arms )*
692 _ => ::core::result::Result::Ok(false),
693 }
694 }
695 }
696 }
697}
698
699fn load_related_impl_my_tokens(
707 struct_name: &syn::Ident,
708 fk_relations: &[FkRelation],
709) -> TokenStream2 {
710 let arms = fk_relations.iter().map(|rel| {
711 let parent_ty = &rel.parent_type;
712 let fk_col = rel.fk_column.as_str();
713 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
714 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
715 let assign = if rel.nullable {
716 quote! {
717 __self.#field_ident = ::core::option::Option::Some(
718 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
719 );
720 }
721 } else {
722 quote! {
723 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
724 }
725 };
726 quote! {
731 #fk_col => {
732 let _parent: #parent_ty =
733 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
734 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
737 ::rustango::core::SqlValue::#variant_ident(v) => v,
738 _other => {
739 ::core::debug_assert!(
740 false,
741 "rustango macro bug: load_related on FK `{}` expected \
742 SqlValue::{} from parent's __rustango_pk_value but got \
743 {:?} — file a bug at https://github.com/ujeenet/rustango",
744 #fk_col,
745 ::core::stringify!(#variant_ident),
746 _other,
747 );
748 #default_expr
749 }
750 };
751 #assign
752 ::core::result::Result::Ok(true)
753 }
754 }
755 });
756 quote! {
757 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
758 #( #arms )*
759 });
760 }
761}
762
763fn load_related_impl_sqlite_tokens(
767 struct_name: &syn::Ident,
768 fk_relations: &[FkRelation],
769) -> TokenStream2 {
770 let arms = fk_relations.iter().map(|rel| {
771 let parent_ty = &rel.parent_type;
772 let fk_col = rel.fk_column.as_str();
773 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
774 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
775 let assign = if rel.nullable {
776 quote! {
777 __self.#field_ident = ::core::option::Option::Some(
778 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
779 );
780 }
781 } else {
782 quote! {
783 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
784 }
785 };
786 quote! {
787 #fk_col => {
788 let _parent: #parent_ty =
789 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
790 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
791 ::rustango::core::SqlValue::#variant_ident(v) => v,
792 _other => {
793 ::core::debug_assert!(
794 false,
795 "rustango macro bug: load_related on FK `{}` expected \
796 SqlValue::{} from parent's __rustango_pk_value but got \
797 {:?} — file a bug at https://github.com/ujeenet/rustango",
798 #fk_col,
799 ::core::stringify!(#variant_ident),
800 _other,
801 );
802 #default_expr
803 }
804 };
805 #assign
806 ::core::result::Result::Ok(true)
807 }
808 }
809 });
810 quote! {
811 ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
812 #( #arms )*
813 });
814 }
815}
816
817fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
825 let arms = fk_relations.iter().map(|rel| {
826 let fk_col = rel.fk_column.as_str();
827 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
828 if rel.pk_kind == DetectedKind::I64 {
829 if rel.nullable {
835 quote! {
836 #fk_col => self.#field_ident
837 .as_ref()
838 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
839 }
840 } else {
841 quote! {
842 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
843 }
844 }
845 } else {
846 quote! {
854 #fk_col => ::core::option::Option::None,
855 }
856 }
857 });
858 let value_arms = fk_relations.iter().map(|rel| {
864 let fk_col = rel.fk_column.as_str();
865 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
866 if rel.nullable {
867 quote! {
868 #fk_col => self.#field_ident
869 .as_ref()
870 .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
871 ::rustango::sql::ForeignKey::pk(fk)
872 )),
873 }
874 } else {
875 quote! {
876 #fk_col => ::core::option::Option::Some(
877 ::core::convert::Into::<::rustango::core::SqlValue>::into(
878 self.#field_ident.pk()
879 )
880 ),
881 }
882 }
883 });
884 quote! {
885 impl ::rustango::sql::FkPkAccess for #struct_name {
886 #[allow(unused_variables)]
887 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
888 match field_name {
889 #( #arms )*
890 _ => ::core::option::Option::None,
891 }
892 }
893 #[allow(unused_variables)]
894 fn __rustango_fk_pk_value(
895 &self,
896 field_name: &str,
897 ) -> ::core::option::Option<::rustango::core::SqlValue> {
898 match field_name {
899 #( #value_arms )*
900 _ => ::core::option::Option::None,
901 }
902 }
903 }
904 }
905}
906
907fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
913 if fk_relations.is_empty() {
914 return TokenStream2::new();
915 }
916 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
920 let method_ident = syn::Ident::new(&suffix, child_ident.span());
921 let impls = fk_relations.iter().map(|rel| {
922 let parent_ty = &rel.parent_type;
923 let fk_col = rel.fk_column.as_str();
924 let doc = format!(
925 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
926 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
927 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
928 further `{child_ident}::objects()` filters via direct queryset use."
929 );
930 quote! {
931 impl #parent_ty {
932 #[doc = #doc]
933 pub async fn #method_ident<'_c, _E>(
938 &self,
939 _executor: _E,
940 ) -> ::core::result::Result<
941 ::std::vec::Vec<#child_ident>,
942 ::rustango::sql::ExecError,
943 >
944 where
945 _E: ::rustango::sql::sqlx::Executor<
946 '_c,
947 Database = ::rustango::sql::sqlx::Postgres,
948 >,
949 {
950 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
951 ::rustango::query::QuerySet::<#child_ident>::new()
952 .filter_op(#fk_col, ::rustango::core::Op::Eq, _pk)
953 .fetch_on(_executor)
954 .await
955 }
956 }
957 }
958 });
959 quote! { #( #impls )* }
960}
961
962fn generic_fk_accessor_tokens(
976 struct_name: &syn::Ident,
977 generic_fks: &[GenericFkAttr],
978 column_entries: &[ColumnEntry],
979) -> TokenStream2 {
980 if generic_fks.is_empty() {
981 return TokenStream2::new();
982 }
983 let methods = generic_fks.iter().filter_map(|gfk| {
984 let ct_ident = column_entries
990 .iter()
991 .find(|c| c.column == gfk.ct_column)
992 .map(|c| c.ident.clone())?;
993 let pk_ident = column_entries
994 .iter()
995 .find(|c| c.column == gfk.pk_column)
996 .map(|c| c.ident.clone())?;
997
998 let accessor_ident =
999 syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1000 let setter_ident =
1001 syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1002 let name_literal = gfk.name.as_str();
1003
1004 Some(quote! {
1005 #[doc = concat!(
1006 "Resolve the polymorphic `",
1007 #name_literal,
1008 "` relation. Reads `self.",
1009 stringify!(#ct_ident),
1010 "` + `self.",
1011 stringify!(#pk_ident),
1012 "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1013 "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1014 #name_literal,
1015 "\", ...))]`."
1016 )]
1017 pub async fn #accessor_ident(
1018 &self,
1019 pool: &::rustango::sql::Pool,
1020 ) -> ::core::result::Result<
1021 ::core::option::Option<::serde_json::Value>,
1022 ::rustango::sql::ExecError,
1023 > {
1024 let gfk = ::rustango::contenttypes::GenericForeignKey::new(
1025 self.#ct_ident as i64,
1026 self.#pk_ident as i64,
1027 );
1028 gfk.get_object(pool).await
1029 }
1030
1031 #[doc = concat!(
1032 "Set the polymorphic `",
1033 #name_literal,
1034 "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1035 stringify!(#ct_ident),
1036 "` and `self.",
1037 stringify!(#pk_ident),
1038 "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1039 #name_literal,
1040 "\", ...))]`."
1041 )]
1042 pub async fn #setter_ident<T: ::rustango::core::Model>(
1043 &mut self,
1044 pool: &::rustango::sql::Pool,
1045 target_pk: i64,
1046 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1047 let gfk = ::rustango::contenttypes::GenericForeignKey::for_target::<T>(
1048 pool,
1049 target_pk,
1050 ).await?;
1051 self.#ct_ident = gfk.content_type_id as _;
1052 self.#pk_ident = gfk.object_pk as _;
1053 ::core::result::Result::Ok(())
1054 }
1055 })
1056 });
1057 quote! {
1058 impl #struct_name {
1059 #( #methods )*
1060 }
1061 }
1062}
1063
1064fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1065 if m2m_relations.is_empty() {
1066 return TokenStream2::new();
1067 }
1068 let methods = m2m_relations.iter().map(|rel| {
1069 let method_name = format!("{}_m2m", rel.name);
1070 let method_ident = syn::Ident::new(&method_name, struct_name.span());
1071 let through = rel.through.as_str();
1072 let src_col = rel.src.as_str();
1073 let dst_col = rel.dst.as_str();
1074 quote! {
1075 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
1076 ::rustango::sql::M2MManager {
1077 src_pk: self.__rustango_pk_value(),
1078 through: #through,
1079 src_col: #src_col,
1080 dst_col: #dst_col,
1081 }
1082 }
1083 }
1084 });
1085 quote! {
1086 impl #struct_name {
1087 #( #methods )*
1088 }
1089 }
1090}
1091
1092struct ColumnEntry {
1093 ident: syn::Ident,
1096 value_ty: Type,
1098 name: String,
1100 column: String,
1102 field_type_tokens: TokenStream2,
1104}
1105
1106struct CollectedFields {
1107 field_schemas: Vec<TokenStream2>,
1108 from_row_inits: Vec<TokenStream2>,
1109 from_aliased_row_inits: Vec<TokenStream2>,
1113 insert_columns: Vec<TokenStream2>,
1116 insert_values: Vec<TokenStream2>,
1119 insert_pushes: Vec<TokenStream2>,
1124 returning_cols: Vec<TokenStream2>,
1127 auto_assigns: Vec<TokenStream2>,
1130 auto_field_idents: Vec<(syn::Ident, String)>,
1134 first_auto_value_ty: Option<Type>,
1137 bulk_pushes_no_auto: Vec<TokenStream2>,
1141 bulk_pushes_all: Vec<TokenStream2>,
1145 bulk_columns_no_auto: Vec<TokenStream2>,
1148 bulk_columns_all: Vec<TokenStream2>,
1151 bulk_auto_uniformity: Vec<TokenStream2>,
1155 first_auto_ident: Option<syn::Ident>,
1158 has_auto: bool,
1160 pk_is_auto: bool,
1164 update_assignments: Vec<TokenStream2>,
1167 upsert_update_columns: Vec<TokenStream2>,
1170 primary_key: Option<(syn::Ident, String)>,
1171 column_entries: Vec<ColumnEntry>,
1172 field_names: Vec<String>,
1175 fk_relations: Vec<FkRelation>,
1180 soft_delete_column: Option<String>,
1185}
1186
1187#[derive(Clone)]
1188struct FkRelation {
1189 parent_type: Type,
1192 fk_column: String,
1195 pk_kind: DetectedKind,
1200 nullable: bool,
1205}
1206
1207fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1208 let cap = named.named.len();
1209 let mut out = CollectedFields {
1210 field_schemas: Vec::with_capacity(cap),
1211 from_row_inits: Vec::with_capacity(cap),
1212 from_aliased_row_inits: Vec::with_capacity(cap),
1213 insert_columns: Vec::with_capacity(cap),
1214 insert_values: Vec::with_capacity(cap),
1215 insert_pushes: Vec::with_capacity(cap),
1216 returning_cols: Vec::new(),
1217 auto_assigns: Vec::new(),
1218 auto_field_idents: Vec::new(),
1219 first_auto_value_ty: None,
1220 bulk_pushes_no_auto: Vec::with_capacity(cap),
1221 bulk_pushes_all: Vec::with_capacity(cap),
1222 bulk_columns_no_auto: Vec::with_capacity(cap),
1223 bulk_columns_all: Vec::with_capacity(cap),
1224 bulk_auto_uniformity: Vec::new(),
1225 first_auto_ident: None,
1226 has_auto: false,
1227 pk_is_auto: false,
1228 update_assignments: Vec::with_capacity(cap),
1229 upsert_update_columns: Vec::with_capacity(cap),
1230 primary_key: None,
1231 column_entries: Vec::with_capacity(cap),
1232 field_names: Vec::with_capacity(cap),
1233 fk_relations: Vec::new(),
1234 soft_delete_column: None,
1235 };
1236
1237 for field in &named.named {
1238 let info = process_field(field, table)?;
1239 out.field_names.push(info.ident.to_string());
1240 out.field_schemas.push(info.schema);
1241 out.from_row_inits.push(info.from_row_init);
1242 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1243 if let Some(parent_ty) = info.fk_inner.clone() {
1244 out.fk_relations.push(FkRelation {
1245 parent_type: parent_ty,
1246 fk_column: info.column.clone(),
1247 pk_kind: info.fk_pk_kind,
1248 nullable: info.nullable,
1249 });
1250 }
1251 if info.soft_delete {
1252 if out.soft_delete_column.is_some() {
1253 return Err(syn::Error::new_spanned(
1254 field,
1255 "only one field may be marked `#[rustango(soft_delete)]`",
1256 ));
1257 }
1258 out.soft_delete_column = Some(info.column.clone());
1259 }
1260 let column = info.column.as_str();
1261 let ident = info.ident;
1262 if info.generated_as.is_some() {
1271 out.column_entries.push(ColumnEntry {
1272 ident: ident.clone(),
1273 value_ty: info.value_ty.clone(),
1274 name: ident.to_string(),
1275 column: info.column.clone(),
1276 field_type_tokens: info.field_type_tokens,
1277 });
1278 continue;
1279 }
1280 out.insert_columns.push(quote!(#column));
1281 out.insert_values.push(quote! {
1282 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1283 ::core::clone::Clone::clone(&self.#ident)
1284 )
1285 });
1286 if info.auto {
1287 out.has_auto = true;
1288 if out.first_auto_ident.is_none() {
1289 out.first_auto_ident = Some(ident.clone());
1290 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1291 }
1292 out.returning_cols.push(quote!(#column));
1293 out.auto_field_idents
1294 .push((ident.clone(), info.column.clone()));
1295 out.auto_assigns.push(quote! {
1296 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1297 });
1298 out.insert_pushes.push(quote! {
1299 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1300 _columns.push(#column);
1301 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1302 ::core::clone::Clone::clone(_v)
1303 ));
1304 }
1305 });
1306 out.bulk_columns_all.push(quote!(#column));
1309 out.bulk_pushes_all.push(quote! {
1310 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1311 ::core::clone::Clone::clone(&_row.#ident)
1312 ));
1313 });
1314 let ident_clone = ident.clone();
1318 out.bulk_auto_uniformity.push(quote! {
1319 for _r in rows.iter().skip(1) {
1320 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1321 return ::core::result::Result::Err(
1322 ::rustango::sql::ExecError::Sql(
1323 ::rustango::sql::SqlError::BulkAutoMixed
1324 )
1325 );
1326 }
1327 }
1328 });
1329 } else {
1330 out.insert_pushes.push(quote! {
1331 _columns.push(#column);
1332 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1333 ::core::clone::Clone::clone(&self.#ident)
1334 ));
1335 });
1336 out.bulk_columns_no_auto.push(quote!(#column));
1338 out.bulk_columns_all.push(quote!(#column));
1339 let push_expr = quote! {
1340 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1341 ::core::clone::Clone::clone(&_row.#ident)
1342 ));
1343 };
1344 out.bulk_pushes_no_auto.push(push_expr.clone());
1345 out.bulk_pushes_all.push(push_expr);
1346 }
1347 if info.primary_key {
1348 if out.primary_key.is_some() {
1349 return Err(syn::Error::new_spanned(
1350 field,
1351 "only one field may be marked `#[rustango(primary_key)]`",
1352 ));
1353 }
1354 out.primary_key = Some((ident.clone(), info.column.clone()));
1355 if info.auto {
1356 out.pk_is_auto = true;
1357 }
1358 } else if info.auto_now_add {
1359 } else if info.auto_now {
1361 out.update_assignments.push(quote! {
1366 ::rustango::core::Assignment {
1367 column: #column,
1368 value: ::core::convert::Into::<::rustango::core::Expr>::into(
1369 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1370 ::chrono::Utc::now()
1371 )
1372 ),
1373 }
1374 });
1375 out.upsert_update_columns.push(quote!(#column));
1376 } else {
1377 out.update_assignments.push(quote! {
1378 ::rustango::core::Assignment {
1379 column: #column,
1380 value: ::core::convert::Into::<::rustango::core::Expr>::into(
1381 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1382 ::core::clone::Clone::clone(&self.#ident)
1383 )
1384 ),
1385 }
1386 });
1387 out.upsert_update_columns.push(quote!(#column));
1388 }
1389 out.column_entries.push(ColumnEntry {
1390 ident: ident.clone(),
1391 value_ty: info.value_ty.clone(),
1392 name: ident.to_string(),
1393 column: info.column.clone(),
1394 field_type_tokens: info.field_type_tokens,
1395 });
1396 }
1397 Ok(out)
1398}
1399
1400fn model_impl_tokens(
1401 struct_name: &syn::Ident,
1402 model_name: &str,
1403 table: &str,
1404 display: Option<&str>,
1405 app_label: Option<&str>,
1406 admin: Option<&AdminAttrs>,
1407 field_schemas: &[TokenStream2],
1408 soft_delete_column: Option<&str>,
1409 permissions: bool,
1410 audit_track: Option<&[String]>,
1411 m2m_relations: &[M2MAttr],
1412 indexes: &[IndexAttr],
1413 checks: &[CheckAttr],
1414 composite_fks: &[CompositeFkAttr],
1415 generic_fks: &[GenericFkAttr],
1416 scope: Option<&str>,
1417) -> TokenStream2 {
1418 let display_tokens = if let Some(name) = display {
1419 quote!(::core::option::Option::Some(#name))
1420 } else {
1421 quote!(::core::option::Option::None)
1422 };
1423 let app_label_tokens = if let Some(name) = app_label {
1424 quote!(::core::option::Option::Some(#name))
1425 } else {
1426 quote!(::core::option::Option::None)
1427 };
1428 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1429 quote!(::core::option::Option::Some(#col))
1430 } else {
1431 quote!(::core::option::Option::None)
1432 };
1433 let audit_track_tokens = match audit_track {
1434 None => quote!(::core::option::Option::None),
1435 Some(names) => {
1436 let lits = names.iter().map(|n| n.as_str());
1437 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1438 }
1439 };
1440 let admin_tokens = admin_config_tokens(admin);
1441 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1445 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1446 _ => quote!(::rustango::core::ModelScope::Tenant),
1447 };
1448 let indexes_tokens = indexes.iter().map(|idx| {
1449 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1450 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1451 let unique = idx.unique;
1452 let method_variant = match idx.method.as_str() {
1456 "gin" => quote!(::rustango::core::IndexMethod::Gin),
1457 "gist" => quote!(::rustango::core::IndexMethod::Gist),
1458 "brin" => quote!(::rustango::core::IndexMethod::Brin),
1459 "spgist" => quote!(::rustango::core::IndexMethod::SpGist),
1460 "hash" => quote!(::rustango::core::IndexMethod::Hash),
1461 "bloom" => quote!(::rustango::core::IndexMethod::Bloom),
1462 _ => quote!(::rustango::core::IndexMethod::BTree),
1463 };
1464 quote! {
1465 ::rustango::core::IndexSchema {
1466 name: #name,
1467 columns: &[ #(#cols),* ],
1468 unique: #unique,
1469 method: #method_variant,
1470 }
1471 }
1472 });
1473 let checks_tokens = checks.iter().map(|c| {
1474 let name = c.name.as_str();
1475 let expr = c.expr.as_str();
1476 quote! {
1477 ::rustango::core::CheckConstraint {
1478 name: #name,
1479 expr: #expr,
1480 }
1481 }
1482 });
1483 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1484 let name = rel.name.as_str();
1485 let to = rel.to.as_str();
1486 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1487 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1488 quote! {
1489 ::rustango::core::CompositeFkRelation {
1490 name: #name,
1491 to: #to,
1492 from: &[ #(#from_cols),* ],
1493 on: &[ #(#on_cols),* ],
1494 }
1495 }
1496 });
1497 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1498 let name = rel.name.as_str();
1499 let ct_col = rel.ct_column.as_str();
1500 let pk_col = rel.pk_column.as_str();
1501 quote! {
1502 ::rustango::core::GenericRelation {
1503 name: #name,
1504 ct_column: #ct_col,
1505 pk_column: #pk_col,
1506 }
1507 }
1508 });
1509 let m2m_tokens = m2m_relations.iter().map(|rel| {
1510 let name = rel.name.as_str();
1511 let to = rel.to.as_str();
1512 let through = rel.through.as_str();
1513 let src = rel.src.as_str();
1514 let dst = rel.dst.as_str();
1515 quote! {
1516 ::rustango::core::M2MRelation {
1517 name: #name,
1518 to: #to,
1519 through: #through,
1520 src_col: #src,
1521 dst_col: #dst,
1522 }
1523 }
1524 });
1525 quote! {
1526 impl ::rustango::core::Model for #struct_name {
1527 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1528 name: #model_name,
1529 table: #table,
1530 fields: &[ #(#field_schemas),* ],
1531 display: #display_tokens,
1532 app_label: #app_label_tokens,
1533 admin: #admin_tokens,
1534 soft_delete_column: #soft_delete_tokens,
1535 permissions: #permissions,
1536 audit_track: #audit_track_tokens,
1537 m2m: &[ #(#m2m_tokens),* ],
1538 indexes: &[ #(#indexes_tokens),* ],
1539 check_constraints: &[ #(#checks_tokens),* ],
1540 composite_relations: &[ #(#composite_fk_tokens),* ],
1541 generic_relations: &[ #(#generic_fk_tokens),* ],
1542 scope: #scope_tokens,
1543 };
1544 }
1545 }
1546}
1547
1548fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1552 let Some(admin) = admin else {
1553 return quote!(::core::option::Option::None);
1554 };
1555
1556 let list_display = admin
1557 .list_display
1558 .as_ref()
1559 .map(|(v, _)| v.as_slice())
1560 .unwrap_or(&[]);
1561 let list_display_lits = list_display.iter().map(|s| s.as_str());
1562
1563 let search_fields = admin
1564 .search_fields
1565 .as_ref()
1566 .map(|(v, _)| v.as_slice())
1567 .unwrap_or(&[]);
1568 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1569
1570 let readonly_fields = admin
1571 .readonly_fields
1572 .as_ref()
1573 .map(|(v, _)| v.as_slice())
1574 .unwrap_or(&[]);
1575 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1576
1577 let list_filter = admin
1578 .list_filter
1579 .as_ref()
1580 .map(|(v, _)| v.as_slice())
1581 .unwrap_or(&[]);
1582 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1583
1584 let actions = admin
1585 .actions
1586 .as_ref()
1587 .map(|(v, _)| v.as_slice())
1588 .unwrap_or(&[]);
1589 let actions_lits = actions.iter().map(|s| s.as_str());
1590
1591 let fieldsets = admin
1592 .fieldsets
1593 .as_ref()
1594 .map(|(v, _)| v.as_slice())
1595 .unwrap_or(&[]);
1596 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1597 let title = title.as_str();
1598 let field_lits = fields.iter().map(|s| s.as_str());
1599 quote!(::rustango::core::Fieldset {
1600 title: #title,
1601 fields: &[ #( #field_lits ),* ],
1602 })
1603 });
1604
1605 let list_per_page = admin.list_per_page.unwrap_or(0);
1606
1607 let ordering_pairs = admin
1608 .ordering
1609 .as_ref()
1610 .map(|(v, _)| v.as_slice())
1611 .unwrap_or(&[]);
1612 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1613 let name = name.as_str();
1614 let desc = *desc;
1615 quote!((#name, #desc))
1616 });
1617
1618 quote! {
1619 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1620 list_display: &[ #( #list_display_lits ),* ],
1621 search_fields: &[ #( #search_fields_lits ),* ],
1622 list_per_page: #list_per_page,
1623 ordering: &[ #( #ordering_tokens ),* ],
1624 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1625 list_filter: &[ #( #list_filter_lits ),* ],
1626 actions: &[ #( #actions_lits ),* ],
1627 fieldsets: &[ #( #fieldset_tokens ),* ],
1628 })
1629 }
1630}
1631
1632fn inherent_impl_tokens(
1633 struct_name: &syn::Ident,
1634 fields: &CollectedFields,
1635 primary_key: Option<&(syn::Ident, String)>,
1636 column_consts: &TokenStream2,
1637 audited_fields: Option<&[&ColumnEntry]>,
1638 indexes: &[IndexAttr],
1639) -> TokenStream2 {
1640 let executor_passes_to_data_write = if audited_fields.is_some() {
1646 quote!(&mut *_executor)
1647 } else {
1648 quote!(_executor)
1649 };
1650 let executor_param = if audited_fields.is_some() {
1651 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1652 } else {
1653 quote!(_executor: _E)
1654 };
1655 let executor_generics = if audited_fields.is_some() {
1656 quote!()
1657 } else {
1658 quote!(<'_c, _E>)
1659 };
1660 let executor_where = if audited_fields.is_some() {
1661 quote!()
1662 } else {
1663 quote! {
1664 where
1665 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1666 }
1667 };
1668 let pool_to_save_on = if audited_fields.is_some() {
1673 quote! {
1674 let mut _conn = pool.acquire().await?;
1675 self.save_on(&mut *_conn).await
1676 }
1677 } else {
1678 quote!(self.save_on(pool).await)
1679 };
1680 let pool_to_insert_on = if audited_fields.is_some() {
1681 quote! {
1682 let mut _conn = pool.acquire().await?;
1683 self.insert_on(&mut *_conn).await
1684 }
1685 } else {
1686 quote!(self.insert_on(pool).await)
1687 };
1688 let pool_to_delete_on = if audited_fields.is_some() {
1689 quote! {
1690 let mut _conn = pool.acquire().await?;
1691 self.delete_on(&mut *_conn).await
1692 }
1693 } else {
1694 quote!(self.delete_on(pool).await)
1695 };
1696 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1697 quote! {
1698 let mut _conn = pool.acquire().await?;
1699 Self::bulk_insert_on(rows, &mut *_conn).await
1700 }
1701 } else {
1702 quote!(Self::bulk_insert_on(rows, pool).await)
1703 };
1704 let pool_to_upsert_on = if audited_fields.is_some() {
1711 quote! {
1712 let mut _conn = pool.acquire().await?;
1713 self.upsert_on(&mut *_conn).await
1714 }
1715 } else {
1716 quote!(self.upsert_on(pool).await)
1717 };
1718
1719 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1737 quote!()
1746 } else if audited_fields.is_some() && fields.has_auto {
1747 quote!()
1750 } else if fields.has_auto {
1751 let pushes = &fields.insert_pushes;
1752 let returning_cols = &fields.returning_cols;
1753 quote! {
1754 pub async fn insert_pool(
1760 &mut self,
1761 pool: &::rustango::sql::Pool,
1762 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1763 let mut _columns: ::std::vec::Vec<&'static str> =
1764 ::std::vec::Vec::new();
1765 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1766 ::std::vec::Vec::new();
1767 #( #pushes )*
1768 let _query = ::rustango::core::InsertQuery {
1769 model: <Self as ::rustango::core::Model>::SCHEMA,
1770 columns: _columns,
1771 values: _values,
1772 returning: ::std::vec![ #( #returning_cols ),* ],
1773 on_conflict: ::core::option::Option::None,
1774 };
1775 let _result = ::rustango::sql::insert_returning_pool(
1776 pool, &_query,
1777 ).await?;
1778 ::rustango::sql::apply_auto_pk(_result, self)
1779 }
1780 }
1781 } else {
1782 let insert_columns = &fields.insert_columns;
1783 let insert_values = &fields.insert_values;
1784 quote! {
1785 pub async fn insert_pool(
1792 &self,
1793 pool: &::rustango::sql::Pool,
1794 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1795 let _query = ::rustango::core::InsertQuery {
1796 model: <Self as ::rustango::core::Model>::SCHEMA,
1797 columns: ::std::vec![ #( #insert_columns ),* ],
1798 values: ::std::vec![ #( #insert_values ),* ],
1799 returning: ::std::vec::Vec::new(),
1800 on_conflict: ::core::option::Option::None,
1801 };
1802 ::rustango::sql::insert_pool(pool, &_query).await
1803 }
1804 }
1805 };
1806
1807 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1820 .map(|tracked| {
1821 tracked
1822 .iter()
1823 .map(|c| {
1824 let column_lit = c.column.as_str();
1825 let ident = &c.ident;
1826 quote! {
1827 (
1828 #column_lit,
1829 ::serde_json::to_value(&self.#ident)
1830 .unwrap_or(::serde_json::Value::Null),
1831 )
1832 }
1833 })
1834 .collect()
1835 })
1836 .unwrap_or_default();
1837 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1838 if fields.pk_is_auto {
1839 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1840 } else {
1841 quote!(::std::format!("{}", &self.#pk_ident))
1842 }
1843 } else {
1844 quote!(::std::string::String::new())
1845 };
1846 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1847 if audited_fields.is_some() {
1848 let pairs = audit_pair_tokens.iter();
1849 let pk_str = audit_pk_to_string.clone();
1850 quote! {
1851 let _audit_entry = ::rustango::audit::PendingEntry {
1852 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1853 entity_pk: #pk_str,
1854 operation: #op_path,
1855 source: ::rustango::audit::current_source(),
1856 changes: ::rustango::audit::snapshot_changes(&[
1857 #( #pairs ),*
1858 ]),
1859 };
1860 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1861 }
1862 } else {
1863 quote!()
1864 }
1865 };
1866 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1867 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1868 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1869 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1870
1871 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1887 let pk_column_lit = pk_col.as_str();
1888 let assignments = &fields.update_assignments;
1889 if audited_fields.is_some() {
1890 if fields.pk_is_auto {
1891 quote!()
1895 } else {
1896 let pairs = audit_pair_tokens.iter();
1897 let pairs2 = audit_pair_tokens.iter();
1898 let pk_str = audit_pk_to_string.clone();
1899 let pk_str2 = audit_pk_to_string.clone();
1900 quote! {
1901 pub async fn save_pool(
1915 &mut self,
1916 pool: &::rustango::sql::Pool,
1917 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1918 let _query = ::rustango::core::UpdateQuery {
1919 model: <Self as ::rustango::core::Model>::SCHEMA,
1920 set: ::std::vec![ #( #assignments ),* ],
1921 where_clause: ::rustango::core::WhereExpr::Predicate(
1922 ::rustango::core::Filter {
1923 column: #pk_column_lit,
1924 op: ::rustango::core::Op::Eq,
1925 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1926 ::core::clone::Clone::clone(&self.#pk_ident)
1927 ),
1928 }
1929 ),
1930 };
1931 let _audit_entry = ::rustango::audit::PendingEntry {
1932 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1933 entity_pk: #pk_str,
1934 operation: ::rustango::audit::AuditOp::Update,
1935 source: ::rustango::audit::current_source(),
1936 changes: ::rustango::audit::snapshot_changes(&[
1937 #( #pairs ),*
1938 ]),
1939 };
1940 let _ = ::rustango::audit::save_one_with_audit(
1941 pool, &_query, &_audit_entry,
1942 ).await?;
1943 ::core::result::Result::Ok(())
1944 }
1945
1946 pub async fn save_partial(
1956 &mut self,
1957 fields: &[&str],
1958 pool: &::rustango::sql::Pool,
1959 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1960 if fields.is_empty() {
1961 ::tracing::warn!(
1962 target: "rustango::save_partial",
1963 model = <Self as ::rustango::core::Model>::SCHEMA.name,
1964 "save_partial called with empty field list — no-op"
1965 );
1966 return ::core::result::Result::Ok(());
1967 }
1968 let _schema = <Self as ::rustango::core::Model>::SCHEMA;
1969 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
1970 ::std::collections::HashSet::with_capacity(fields.len());
1971 for f in fields {
1972 match _schema.field(f) {
1973 ::core::option::Option::Some(fs) => {
1974 _wanted_cols.insert(fs.column);
1975 }
1976 ::core::option::Option::None => {
1977 return ::core::result::Result::Err(
1978 ::rustango::sql::ExecError::Query(
1979 ::rustango::core::QueryError::UnknownField {
1980 model: _schema.name,
1981 field: (*f).to_owned(),
1982 }
1983 )
1984 );
1985 }
1986 }
1987 }
1988 let _full: ::std::vec::Vec<::rustango::core::Assignment> =
1989 ::std::vec![ #( #assignments ),* ];
1990 let _filtered: ::std::vec::Vec<::rustango::core::Assignment> = _full
1991 .into_iter()
1992 .filter(|a| _wanted_cols.contains(a.column))
1993 .collect();
1994 if _filtered.is_empty() {
1995 ::tracing::warn!(
1996 target: "rustango::save_partial",
1997 model = _schema.name,
1998 "save_partial: every named field maps to a non-assignable column — no-op"
1999 );
2000 return ::core::result::Result::Ok(());
2001 }
2002 let _query = ::rustango::core::UpdateQuery {
2003 model: _schema,
2004 set: _filtered,
2005 where_clause: ::rustango::core::WhereExpr::Predicate(
2006 ::rustango::core::Filter {
2007 column: #pk_column_lit,
2008 op: ::rustango::core::Op::Eq,
2009 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2010 ::core::clone::Clone::clone(&self.#pk_ident)
2011 ),
2012 }
2013 ),
2014 };
2015 let _all_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2017 ::std::vec![ #( #pairs2 ),* ];
2018 let _narrowed: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2019 _all_pairs
2020 .into_iter()
2021 .filter(|(col, _)| _wanted_cols.contains(col))
2022 .collect();
2023 let _audit_entry = ::rustango::audit::PendingEntry {
2024 entity_table: _schema.table,
2025 entity_pk: #pk_str2,
2026 operation: ::rustango::audit::AuditOp::Update,
2027 source: ::rustango::audit::current_source(),
2028 changes: ::rustango::audit::snapshot_changes(&_narrowed),
2029 };
2030 let _ = ::rustango::audit::save_one_with_audit(
2031 pool, &_query, &_audit_entry,
2032 ).await?;
2033 ::core::result::Result::Ok(())
2034 }
2035
2036 pub async fn save_partial_typed<
2055 L: ::rustango::core::TypedFieldList<Self>,
2056 >(
2057 &mut self,
2058 fields: L,
2059 pool: &::rustango::sql::Pool,
2060 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2061 let _names = fields.rust_field_names();
2062 let _refs: ::std::vec::Vec<&str> =
2063 _names.iter().copied().collect();
2064 self.save_partial(&_refs, pool).await
2065 }
2066 }
2067 }
2068 } else {
2069 let dispatch_unset = if fields.pk_is_auto {
2070 quote! {
2071 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2072 return self.insert_pool(pool).await;
2073 }
2074 }
2075 } else {
2076 quote!()
2077 };
2078 quote! {
2079 pub async fn save_pool(
2086 &mut self,
2087 pool: &::rustango::sql::Pool,
2088 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2089 #dispatch_unset
2090 let _query = ::rustango::core::UpdateQuery {
2091 model: <Self as ::rustango::core::Model>::SCHEMA,
2092 set: ::std::vec![ #( #assignments ),* ],
2093 where_clause: ::rustango::core::WhereExpr::Predicate(
2094 ::rustango::core::Filter {
2095 column: #pk_column_lit,
2096 op: ::rustango::core::Op::Eq,
2097 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2098 ::core::clone::Clone::clone(&self.#pk_ident)
2099 ),
2100 }
2101 ),
2102 };
2103 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
2104 ::core::result::Result::Ok(())
2105 }
2106
2107 pub async fn save_partial(
2136 &mut self,
2137 fields: &[&str],
2138 pool: &::rustango::sql::Pool,
2139 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2140 if fields.is_empty() {
2141 ::tracing::warn!(
2142 target: "rustango::save_partial",
2143 model = <Self as ::rustango::core::Model>::SCHEMA.name,
2144 "save_partial called with empty field list — no-op"
2145 );
2146 return ::core::result::Result::Ok(());
2147 }
2148 let _schema = <Self as ::rustango::core::Model>::SCHEMA;
2149 let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
2151 ::std::collections::HashSet::with_capacity(fields.len());
2152 for f in fields {
2153 match _schema.field(f) {
2154 ::core::option::Option::Some(fs) => {
2155 _wanted_cols.insert(fs.column);
2156 }
2157 ::core::option::Option::None => {
2158 return ::core::result::Result::Err(
2159 ::rustango::sql::ExecError::Query(
2160 ::rustango::core::QueryError::UnknownField {
2161 model: _schema.name,
2162 field: (*f).to_owned(),
2163 }
2164 )
2165 );
2166 }
2167 }
2168 }
2169 let _full: ::std::vec::Vec<::rustango::core::Assignment> =
2172 ::std::vec![ #( #assignments ),* ];
2173 let _filtered: ::std::vec::Vec<::rustango::core::Assignment> = _full
2174 .into_iter()
2175 .filter(|a| _wanted_cols.contains(a.column))
2176 .collect();
2177 if _filtered.is_empty() {
2178 ::tracing::warn!(
2183 target: "rustango::save_partial",
2184 model = _schema.name,
2185 "save_partial: every named field maps to a non-assignable column — no-op"
2186 );
2187 return ::core::result::Result::Ok(());
2188 }
2189 let _query = ::rustango::core::UpdateQuery {
2190 model: _schema,
2191 set: _filtered,
2192 where_clause: ::rustango::core::WhereExpr::Predicate(
2193 ::rustango::core::Filter {
2194 column: #pk_column_lit,
2195 op: ::rustango::core::Op::Eq,
2196 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2197 ::core::clone::Clone::clone(&self.#pk_ident)
2198 ),
2199 }
2200 ),
2201 };
2202 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
2203 ::core::result::Result::Ok(())
2204 }
2205
2206 pub async fn save_partial_typed<
2226 L: ::rustango::core::TypedFieldList<Self>,
2227 >(
2228 &mut self,
2229 fields: L,
2230 pool: &::rustango::sql::Pool,
2231 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2232 let _names = fields.rust_field_names();
2233 let _refs: ::std::vec::Vec<&str> =
2234 _names.iter().copied().collect();
2235 self.save_partial(&_refs, pool).await
2236 }
2237 }
2238 }
2239 } else {
2240 quote!()
2241 };
2242
2243 let pool_insert_method = if audited_fields.is_some() {
2250 if let Some(_) = primary_key {
2251 let pushes = if fields.has_auto {
2252 fields.insert_pushes.clone()
2253 } else {
2254 fields
2259 .insert_columns
2260 .iter()
2261 .zip(&fields.insert_values)
2262 .map(|(col, val)| {
2263 quote! {
2264 _columns.push(#col);
2265 _values.push(#val);
2266 }
2267 })
2268 .collect()
2269 };
2270 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
2271 fields.returning_cols.clone()
2272 } else {
2273 primary_key
2280 .map(|(_, col)| {
2281 let lit = col.as_str();
2282 vec![quote!(#lit)]
2283 })
2284 .unwrap_or_default()
2285 };
2286 let pairs = audit_pair_tokens.iter();
2287 let pk_str = audit_pk_to_string.clone();
2288 quote! {
2289 pub async fn insert_pool(
2298 &mut self,
2299 pool: &::rustango::sql::Pool,
2300 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2301 let mut _columns: ::std::vec::Vec<&'static str> =
2302 ::std::vec::Vec::new();
2303 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2304 ::std::vec::Vec::new();
2305 #( #pushes )*
2306 let _query = ::rustango::core::InsertQuery {
2307 model: <Self as ::rustango::core::Model>::SCHEMA,
2308 columns: _columns,
2309 values: _values,
2310 returning: ::std::vec![ #( #returning_cols ),* ],
2311 on_conflict: ::core::option::Option::None,
2312 };
2313 let _audit_entry = ::rustango::audit::PendingEntry {
2314 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2315 entity_pk: #pk_str,
2316 operation: ::rustango::audit::AuditOp::Create,
2317 source: ::rustango::audit::current_source(),
2318 changes: ::rustango::audit::snapshot_changes(&[
2319 #( #pairs ),*
2320 ]),
2321 };
2322 let _result = ::rustango::audit::insert_one_with_audit(
2323 pool, &_query, &_audit_entry,
2324 ).await?;
2325 ::rustango::sql::apply_auto_pk(_result, self)
2326 }
2327 }
2328 } else {
2329 quote!()
2330 }
2331 } else {
2332 pool_insert_method
2334 };
2335
2336 let pool_save_method = if let Some(tracked) = audited_fields {
2357 if let Some((pk_ident, pk_col)) = primary_key {
2358 let pk_column_lit = pk_col.as_str();
2359 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
2363 let pk_str = audit_pk_to_string.clone();
2364 let mk_before_pairs =
2369 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
2370 tracked
2371 .iter()
2372 .map(|c| {
2373 let column_lit = c.column.as_str();
2374 let value_ty = &c.value_ty;
2375 quote! {
2376 (
2377 #column_lit,
2378 match #getter::<#value_ty>(
2379 _audit_before_row, #column_lit,
2380 ) {
2381 ::core::result::Result::Ok(v) => {
2382 ::serde_json::to_value(&v)
2383 .unwrap_or(::serde_json::Value::Null)
2384 }
2385 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2386 },
2387 )
2388 }
2389 })
2390 .collect()
2391 };
2392 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
2393 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
2394 let before_pairs_my: Vec<proc_macro2::TokenStream> =
2395 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
2396 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
2397 mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
2398 let pg_select_cols: String = tracked
2399 .iter()
2400 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2401 .collect::<Vec<_>>()
2402 .join(", ");
2403 let my_select_cols: String = tracked
2404 .iter()
2405 .map(|c| format!("`{}`", c.column.replace('`', "``")))
2406 .collect::<Vec<_>>()
2407 .join(", ");
2408 let sqlite_select_cols: String = pg_select_cols.clone();
2412 let pk_value_for_bind = if fields.pk_is_auto {
2413 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2414 } else {
2415 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2416 };
2417 let assignments = &fields.update_assignments;
2418 let unset_dispatch = if fields.has_auto {
2419 quote! {
2420 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2421 return self.insert_pool(pool).await;
2422 }
2423 }
2424 } else {
2425 quote!()
2426 };
2427 quote! {
2428 pub async fn save_pool(
2442 &mut self,
2443 pool: &::rustango::sql::Pool,
2444 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2445 #unset_dispatch
2446 let _query = ::rustango::core::UpdateQuery {
2447 model: <Self as ::rustango::core::Model>::SCHEMA,
2448 set: ::std::vec![ #( #assignments ),* ],
2449 where_clause: ::rustango::core::WhereExpr::Predicate(
2450 ::rustango::core::Filter {
2451 column: #pk_column_lit,
2452 op: ::rustango::core::Op::Eq,
2453 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2454 ::core::clone::Clone::clone(&self.#pk_ident)
2455 ),
2456 }
2457 ),
2458 };
2459 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2460 ::std::vec![ #( #after_pairs_pg ),* ];
2461 ::rustango::audit::save_one_with_diff(
2462 pool,
2463 &_query,
2464 #pk_column_lit,
2465 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2466 #pk_value_for_bind,
2467 ),
2468 <Self as ::rustango::core::Model>::SCHEMA.table,
2469 #pk_str,
2470 _after_pairs,
2471 #pg_select_cols,
2472 #my_select_cols,
2473 #sqlite_select_cols,
2474 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2475 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2476 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2477 ).await
2478 }
2479 }
2480 } else {
2481 quote!()
2482 }
2483 } else {
2484 pool_save_method
2485 };
2486
2487 let pool_delete_method = {
2494 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2495 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2496 if let Some(pk_ident) = pk_ident_for_pool {
2497 if audited_fields.is_some() {
2498 let pairs = audit_pair_tokens.iter();
2499 let pk_str = audit_pk_to_string.clone();
2500 quote! {
2501 pub async fn delete_pool(
2508 &self,
2509 pool: &::rustango::sql::Pool,
2510 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2511 let _query = ::rustango::core::DeleteQuery {
2512 model: <Self as ::rustango::core::Model>::SCHEMA,
2513 where_clause: ::rustango::core::WhereExpr::Predicate(
2514 ::rustango::core::Filter {
2515 column: #pk_column_lit,
2516 op: ::rustango::core::Op::Eq,
2517 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2518 ::core::clone::Clone::clone(&self.#pk_ident)
2519 ),
2520 }
2521 ),
2522 };
2523 let _audit_entry = ::rustango::audit::PendingEntry {
2524 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2525 entity_pk: #pk_str,
2526 operation: ::rustango::audit::AuditOp::Delete,
2527 source: ::rustango::audit::current_source(),
2528 changes: ::rustango::audit::snapshot_changes(&[
2529 #( #pairs ),*
2530 ]),
2531 };
2532 ::rustango::audit::delete_one_with_audit(
2533 pool, &_query, &_audit_entry,
2534 ).await
2535 }
2536 }
2537 } else {
2538 quote! {
2539 pub async fn delete_pool(
2546 &self,
2547 pool: &::rustango::sql::Pool,
2548 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2549 let _query = ::rustango::core::DeleteQuery {
2550 model: <Self as ::rustango::core::Model>::SCHEMA,
2551 where_clause: ::rustango::core::WhereExpr::Predicate(
2552 ::rustango::core::Filter {
2553 column: #pk_column_lit,
2554 op: ::rustango::core::Op::Eq,
2555 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2556 ::core::clone::Clone::clone(&self.#pk_ident)
2557 ),
2558 }
2559 ),
2560 };
2561 ::rustango::sql::delete_pool(pool, &_query).await
2562 }
2563 }
2564 }
2565 } else {
2566 quote!()
2567 }
2568 };
2569
2570 let tx_insert_method = if fields.has_auto {
2576 let pushes = &fields.insert_pushes;
2577 let returning_cols = &fields.returning_cols;
2578 quote! {
2579 pub async fn insert_tx(
2586 &mut self,
2587 tx: &mut ::rustango::sql::PoolTx<'_>,
2588 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2589 let mut _columns: ::std::vec::Vec<&'static str> =
2590 ::std::vec::Vec::new();
2591 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2592 ::std::vec::Vec::new();
2593 #( #pushes )*
2594 let _query = ::rustango::core::InsertQuery {
2595 model: <Self as ::rustango::core::Model>::SCHEMA,
2596 columns: _columns,
2597 values: _values,
2598 returning: ::std::vec![ #( #returning_cols ),* ],
2599 on_conflict: ::core::option::Option::None,
2600 };
2601 let _result = ::rustango::sql::insert_returning_tx(tx, &_query).await?;
2602 ::rustango::sql::apply_auto_pk(_result, self)
2603 }
2604 }
2605 } else {
2606 let insert_columns = &fields.insert_columns;
2607 let insert_values = &fields.insert_values;
2608 quote! {
2609 pub async fn insert_tx(
2614 &self,
2615 tx: &mut ::rustango::sql::PoolTx<'_>,
2616 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2617 let _query = ::rustango::core::InsertQuery {
2618 model: <Self as ::rustango::core::Model>::SCHEMA,
2619 columns: ::std::vec![ #( #insert_columns ),* ],
2620 values: ::std::vec![ #( #insert_values ),* ],
2621 returning: ::std::vec::Vec::new(),
2622 on_conflict: ::core::option::Option::None,
2623 };
2624 ::rustango::sql::insert_tx(tx, &_query).await
2625 }
2626 }
2627 };
2628
2629 let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
2630 let pk_column_lit = pk_col.as_str();
2631 let assignments = &fields.update_assignments;
2632 let dispatch_unset = if fields.pk_is_auto {
2633 quote! {
2634 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2635 return self.insert_tx(tx).await;
2636 }
2637 }
2638 } else {
2639 quote!()
2640 };
2641 quote! {
2642 pub async fn save_tx(
2649 &mut self,
2650 tx: &mut ::rustango::sql::PoolTx<'_>,
2651 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2652 #dispatch_unset
2653 let _query = ::rustango::core::UpdateQuery {
2654 model: <Self as ::rustango::core::Model>::SCHEMA,
2655 set: ::std::vec![ #( #assignments ),* ],
2656 where_clause: ::rustango::core::WhereExpr::Predicate(
2657 ::rustango::core::Filter {
2658 column: #pk_column_lit,
2659 op: ::rustango::core::Op::Eq,
2660 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2661 ::core::clone::Clone::clone(&self.#pk_ident)
2662 ),
2663 }
2664 ),
2665 };
2666 let _ = ::rustango::sql::update_tx(tx, &_query).await?;
2667 ::core::result::Result::Ok(())
2668 }
2669 }
2670 } else {
2671 quote!()
2672 };
2673
2674 let tx_delete_method = {
2675 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2676 let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
2677 if let Some(pk_ident) = pk_ident_for_tx {
2678 quote! {
2679 pub async fn delete_tx(
2686 &self,
2687 tx: &mut ::rustango::sql::PoolTx<'_>,
2688 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2689 let _query = ::rustango::core::DeleteQuery {
2690 model: <Self as ::rustango::core::Model>::SCHEMA,
2691 where_clause: ::rustango::core::WhereExpr::Predicate(
2692 ::rustango::core::Filter {
2693 column: #pk_column_lit,
2694 op: ::rustango::core::Op::Eq,
2695 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2696 ::core::clone::Clone::clone(&self.#pk_ident)
2697 ),
2698 }
2699 ),
2700 };
2701 ::rustango::sql::delete_tx(tx, &_query).await
2702 }
2703 }
2704 } else {
2705 quote!()
2706 }
2707 };
2708
2709 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
2719 audited_fields
2720 {
2721 if tracked.is_empty() {
2722 (quote!(), quote!())
2723 } else {
2724 let select_cols: String = tracked
2725 .iter()
2726 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2727 .collect::<Vec<_>>()
2728 .join(", ");
2729 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
2730 let select_cols_lit = select_cols;
2731 let pk_column_lit_for_select = pk_column_for_select;
2732 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2733 if fields.pk_is_auto {
2734 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2735 } else {
2736 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2737 }
2738 } else {
2739 quote!(0_i64)
2740 };
2741 let before_pairs = tracked.iter().map(|c| {
2742 let column_lit = c.column.as_str();
2743 let value_ty = &c.value_ty;
2744 quote! {
2745 (
2746 #column_lit,
2747 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2748 &_audit_before_row, #column_lit,
2749 ) {
2750 ::core::result::Result::Ok(v) => {
2751 ::serde_json::to_value(&v)
2752 .unwrap_or(::serde_json::Value::Null)
2753 }
2754 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2755 },
2756 )
2757 }
2758 });
2759 let after_pairs = tracked.iter().map(|c| {
2760 let column_lit = c.column.as_str();
2761 let ident = &c.ident;
2762 quote! {
2763 (
2764 #column_lit,
2765 ::serde_json::to_value(&self.#ident)
2766 .unwrap_or(::serde_json::Value::Null),
2767 )
2768 }
2769 });
2770 let pk_str = audit_pk_to_string.clone();
2771 let pre = quote! {
2772 let _audit_select_sql = ::std::format!(
2773 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2774 #select_cols_lit,
2775 <Self as ::rustango::core::Model>::SCHEMA.table,
2776 #pk_column_lit_for_select,
2777 );
2778 let _audit_before_pairs:
2779 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2780 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2781 .bind(#pk_value_for_bind)
2782 .fetch_optional(&mut *_executor)
2783 .await
2784 {
2785 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2786 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2787 }
2788 _ => ::core::option::Option::None,
2789 };
2790 };
2791 let post = quote! {
2792 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2793 let _audit_after:
2794 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2795 ::std::vec![ #( #after_pairs ),* ];
2796 let _audit_entry = ::rustango::audit::PendingEntry {
2797 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2798 entity_pk: #pk_str,
2799 operation: ::rustango::audit::AuditOp::Update,
2800 source: ::rustango::audit::current_source(),
2801 changes: ::rustango::audit::diff_changes(
2802 &_audit_before,
2803 &_audit_after,
2804 ),
2805 };
2806 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2807 }
2808 };
2809 (pre, post)
2810 }
2811 } else {
2812 (quote!(), quote!())
2813 };
2814
2815 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2819 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2820 if fields.pk_is_auto {
2821 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2822 } else {
2823 quote!(::std::format!("{}", &_row.#pk_ident))
2824 }
2825 } else {
2826 quote!(::std::string::String::new())
2827 };
2828 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
2829 let column_lit = c.column.as_str();
2830 let ident = &c.ident;
2831 quote! {
2832 (
2833 #column_lit,
2834 ::serde_json::to_value(&_row.#ident)
2835 .unwrap_or(::serde_json::Value::Null),
2836 )
2837 }
2838 });
2839 quote! {
2840 let _audit_source = ::rustango::audit::current_source();
2841 let mut _audit_entries:
2842 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2843 ::std::vec::Vec::with_capacity(rows.len());
2844 for _row in rows.iter() {
2845 _audit_entries.push(::rustango::audit::PendingEntry {
2846 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2847 entity_pk: #row_pk_str,
2848 operation: ::rustango::audit::AuditOp::Create,
2849 source: _audit_source.clone(),
2850 changes: ::rustango::audit::snapshot_changes(&[
2851 #( #row_pairs ),*
2852 ]),
2853 });
2854 }
2855 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2856 }
2857 } else {
2858 quote!()
2859 };
2860
2861 let save_method = if fields.pk_is_auto {
2862 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
2863 let pk_column_lit = pk_column.as_str();
2864 let assignments = &fields.update_assignments;
2865 let upsert_cols = &fields.upsert_update_columns;
2866 let upsert_pushes = &fields.insert_pushes;
2867 let upsert_returning = &fields.returning_cols;
2868 let upsert_auto_assigns = &fields.auto_assigns;
2869 let upsert_target_columns: Vec<String> = indexes
2878 .iter()
2879 .find(|i| i.unique && !i.columns.is_empty())
2880 .map(|i| i.columns.clone())
2881 .unwrap_or_else(|| vec![pk_column.clone()]);
2882 let upsert_target_lits = upsert_target_columns
2883 .iter()
2884 .map(String::as_str)
2885 .collect::<Vec<_>>();
2886 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2887 quote!(::rustango::core::ConflictClause::DoNothing)
2888 } else {
2889 quote!(::rustango::core::ConflictClause::DoUpdate {
2890 target: ::std::vec![ #( #upsert_target_lits ),* ],
2891 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2892 })
2893 };
2894 Some(quote! {
2895 #[cfg(feature = "postgres")]
2913 pub async fn save(
2914 &mut self,
2915 pool: &::rustango::sql::sqlx::PgPool,
2916 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2917 #pool_to_save_on
2918 }
2919
2920 #[cfg(feature = "postgres")]
2931 pub async fn save_on #executor_generics (
2932 &mut self,
2933 #executor_param,
2934 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2935 #executor_where
2936 {
2937 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2938 return self.insert_on(#executor_passes_to_data_write).await;
2939 }
2940 #audit_update_pre
2941 let _query = ::rustango::core::UpdateQuery {
2942 model: <Self as ::rustango::core::Model>::SCHEMA,
2943 set: ::std::vec![ #( #assignments ),* ],
2944 where_clause: ::rustango::core::WhereExpr::Predicate(
2945 ::rustango::core::Filter {
2946 column: #pk_column_lit,
2947 op: ::rustango::core::Op::Eq,
2948 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2949 ::core::clone::Clone::clone(&self.#pk_ident)
2950 ),
2951 }
2952 ),
2953 };
2954 let _ = ::rustango::sql::update_on(
2955 #executor_passes_to_data_write,
2956 &_query,
2957 ).await?;
2958 #audit_update_post
2959 ::core::result::Result::Ok(())
2960 }
2961
2962 #[cfg(feature = "postgres")]
2973 pub async fn save_on_with #executor_generics (
2974 &mut self,
2975 #executor_param,
2976 source: ::rustango::audit::AuditSource,
2977 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2978 #executor_where
2979 {
2980 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2981 }
2982
2983 #[cfg(feature = "postgres")]
2993 pub async fn upsert(
2994 &mut self,
2995 pool: &::rustango::sql::sqlx::PgPool,
2996 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2997 #pool_to_upsert_on
2998 }
2999
3000 #[cfg(feature = "postgres")]
3006 pub async fn upsert_on #executor_generics (
3007 &mut self,
3008 #executor_param,
3009 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3010 #executor_where
3011 {
3012 let mut _columns: ::std::vec::Vec<&'static str> =
3013 ::std::vec::Vec::new();
3014 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
3015 ::std::vec::Vec::new();
3016 #( #upsert_pushes )*
3017 let query = ::rustango::core::InsertQuery {
3018 model: <Self as ::rustango::core::Model>::SCHEMA,
3019 columns: _columns,
3020 values: _values,
3021 returning: ::std::vec![ #( #upsert_returning ),* ],
3022 on_conflict: ::core::option::Option::Some(#conflict_clause),
3023 };
3024 let _returning_row_v = ::rustango::sql::insert_returning_on(
3025 #executor_passes_to_data_write,
3026 &query,
3027 ).await?;
3028 let _returning_row = &_returning_row_v;
3029 #( #upsert_auto_assigns )*
3030 ::core::result::Result::Ok(())
3031 }
3032 })
3033 } else {
3034 None
3035 };
3036
3037 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
3038 let pk_column_lit = pk_column.as_str();
3039 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
3046 let col_lit = col;
3047 quote! {
3048 pub async fn soft_delete_on #executor_generics (
3058 &self,
3059 #executor_param,
3060 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3061 #executor_where
3062 {
3063 let _query = ::rustango::core::UpdateQuery {
3064 model: <Self as ::rustango::core::Model>::SCHEMA,
3065 set: ::std::vec![
3066 ::rustango::core::Assignment {
3067 column: #col_lit,
3068 value: ::core::convert::Into::<::rustango::core::Expr>::into(
3069 ::core::convert::Into::<::rustango::core::SqlValue>::into(
3070 ::chrono::Utc::now()
3071 )
3072 ),
3073 },
3074 ],
3075 where_clause: ::rustango::core::WhereExpr::Predicate(
3076 ::rustango::core::Filter {
3077 column: #pk_column_lit,
3078 op: ::rustango::core::Op::Eq,
3079 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3080 ::core::clone::Clone::clone(&self.#pk_ident)
3081 ),
3082 }
3083 ),
3084 };
3085 let _affected = ::rustango::sql::update_on(
3086 #executor_passes_to_data_write,
3087 &_query,
3088 ).await?;
3089 #audit_softdelete_emit
3090 ::core::result::Result::Ok(_affected)
3091 }
3092
3093 pub async fn restore_on #executor_generics (
3100 &self,
3101 #executor_param,
3102 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3103 #executor_where
3104 {
3105 let _query = ::rustango::core::UpdateQuery {
3106 model: <Self as ::rustango::core::Model>::SCHEMA,
3107 set: ::std::vec![
3108 ::rustango::core::Assignment {
3109 column: #col_lit,
3110 value: ::core::convert::Into::<::rustango::core::Expr>::into(
3111 ::rustango::core::SqlValue::Null
3112 ),
3113 },
3114 ],
3115 where_clause: ::rustango::core::WhereExpr::Predicate(
3116 ::rustango::core::Filter {
3117 column: #pk_column_lit,
3118 op: ::rustango::core::Op::Eq,
3119 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3120 ::core::clone::Clone::clone(&self.#pk_ident)
3121 ),
3122 }
3123 ),
3124 };
3125 let _affected = ::rustango::sql::update_on(
3126 #executor_passes_to_data_write,
3127 &_query,
3128 ).await?;
3129 #audit_restore_emit
3130 ::core::result::Result::Ok(_affected)
3131 }
3132 }
3133 } else {
3134 quote!()
3135 };
3136 quote! {
3137 #[cfg(feature = "postgres")]
3145 pub async fn delete(
3146 &self,
3147 pool: &::rustango::sql::sqlx::PgPool,
3148 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
3149 #pool_to_delete_on
3150 }
3151
3152 #[cfg(feature = "postgres")]
3159 pub async fn delete_on #executor_generics (
3160 &self,
3161 #executor_param,
3162 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3163 #executor_where
3164 {
3165 let query = ::rustango::core::DeleteQuery {
3166 model: <Self as ::rustango::core::Model>::SCHEMA,
3167 where_clause: ::rustango::core::WhereExpr::Predicate(
3168 ::rustango::core::Filter {
3169 column: #pk_column_lit,
3170 op: ::rustango::core::Op::Eq,
3171 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
3172 ::core::clone::Clone::clone(&self.#pk_ident)
3173 ),
3174 }
3175 ),
3176 };
3177 let _affected = ::rustango::sql::delete_on(
3178 #executor_passes_to_data_write,
3179 &query,
3180 ).await?;
3181 #audit_delete_emit
3182 ::core::result::Result::Ok(_affected)
3183 }
3184
3185 #[cfg(feature = "postgres")]
3191 pub async fn delete_on_with #executor_generics (
3192 &self,
3193 #executor_param,
3194 source: ::rustango::audit::AuditSource,
3195 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
3196 #executor_where
3197 {
3198 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
3199 }
3200 #pool_delete_method
3201 #pool_insert_method
3202 #pool_save_method
3203 #tx_delete_method
3204 #tx_insert_method
3205 #tx_save_method
3206 #soft_delete_methods
3207 }
3208 });
3209
3210 let insert_method = if fields.has_auto {
3211 let pushes = &fields.insert_pushes;
3212 let returning_cols = &fields.returning_cols;
3213 let auto_assigns = &fields.auto_assigns;
3214 quote! {
3215 #[cfg(feature = "postgres")]
3224 pub async fn insert(
3225 &mut self,
3226 pool: &::rustango::sql::sqlx::PgPool,
3227 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3228 #pool_to_insert_on
3229 }
3230
3231 #[cfg(feature = "postgres")]
3237 pub async fn insert_on #executor_generics (
3238 &mut self,
3239 #executor_param,
3240 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3241 #executor_where
3242 {
3243 let mut _columns: ::std::vec::Vec<&'static str> =
3244 ::std::vec::Vec::new();
3245 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
3246 ::std::vec::Vec::new();
3247 #( #pushes )*
3248 let query = ::rustango::core::InsertQuery {
3249 model: <Self as ::rustango::core::Model>::SCHEMA,
3250 columns: _columns,
3251 values: _values,
3252 returning: ::std::vec![ #( #returning_cols ),* ],
3253 on_conflict: ::core::option::Option::None,
3254 };
3255 let _returning_row_v = ::rustango::sql::insert_returning_on(
3256 #executor_passes_to_data_write,
3257 &query,
3258 ).await?;
3259 let _returning_row = &_returning_row_v;
3260 #( #auto_assigns )*
3261 #audit_insert_emit
3262 ::core::result::Result::Ok(())
3263 }
3264
3265 #[cfg(feature = "postgres")]
3271 pub async fn insert_on_with #executor_generics (
3272 &mut self,
3273 #executor_param,
3274 source: ::rustango::audit::AuditSource,
3275 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3276 #executor_where
3277 {
3278 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
3279 }
3280 }
3281 } else {
3282 let insert_columns = &fields.insert_columns;
3283 let insert_values = &fields.insert_values;
3284 quote! {
3285 #[cfg(feature = "postgres")]
3291 pub async fn insert(
3292 &self,
3293 pool: &::rustango::sql::sqlx::PgPool,
3294 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3295 self.insert_on(pool).await
3296 }
3297
3298 #[cfg(feature = "postgres")]
3304 pub async fn insert_on<'_c, _E>(
3305 &self,
3306 _executor: _E,
3307 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3308 where
3309 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
3310 {
3311 let query = ::rustango::core::InsertQuery {
3312 model: <Self as ::rustango::core::Model>::SCHEMA,
3313 columns: ::std::vec![ #( #insert_columns ),* ],
3314 values: ::std::vec![ #( #insert_values ),* ],
3315 returning: ::std::vec::Vec::new(),
3316 on_conflict: ::core::option::Option::None,
3317 };
3318 ::rustango::sql::insert_on(_executor, &query).await
3319 }
3320 }
3321 };
3322
3323 let bulk_insert_method = if fields.has_auto {
3324 let cols_no_auto = &fields.bulk_columns_no_auto;
3325 let cols_all = &fields.bulk_columns_all;
3326 let pushes_no_auto = &fields.bulk_pushes_no_auto;
3327 let pushes_all = &fields.bulk_pushes_all;
3328 let returning_cols = &fields.returning_cols;
3329 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
3330 let uniformity = &fields.bulk_auto_uniformity;
3331 let first_auto_ident = fields
3332 .first_auto_ident
3333 .as_ref()
3334 .expect("has_auto implies first_auto_ident is Some");
3335 quote! {
3336 #[cfg(feature = "postgres")]
3350 pub async fn bulk_insert(
3351 rows: &mut [Self],
3352 pool: &::rustango::sql::sqlx::PgPool,
3353 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3354 #pool_to_bulk_insert_on
3355 }
3356
3357 #[cfg(feature = "postgres")]
3363 pub async fn bulk_insert_on #executor_generics (
3364 rows: &mut [Self],
3365 #executor_param,
3366 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3367 #executor_where
3368 {
3369 if rows.is_empty() {
3370 return ::core::result::Result::Ok(());
3371 }
3372 let _first_unset = matches!(
3373 rows[0].#first_auto_ident,
3374 ::rustango::sql::Auto::Unset
3375 );
3376 #( #uniformity )*
3377
3378 let mut _all_rows: ::std::vec::Vec<
3379 ::std::vec::Vec<::rustango::core::SqlValue>,
3380 > = ::std::vec::Vec::with_capacity(rows.len());
3381 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
3382 for _row in rows.iter() {
3383 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3384 ::std::vec::Vec::new();
3385 #( #pushes_no_auto )*
3386 _all_rows.push(_row_vals);
3387 }
3388 ::std::vec![ #( #cols_no_auto ),* ]
3389 } else {
3390 for _row in rows.iter() {
3391 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3392 ::std::vec::Vec::new();
3393 #( #pushes_all )*
3394 _all_rows.push(_row_vals);
3395 }
3396 ::std::vec![ #( #cols_all ),* ]
3397 };
3398
3399 let _query = ::rustango::core::BulkInsertQuery {
3400 model: <Self as ::rustango::core::Model>::SCHEMA,
3401 columns: _columns,
3402 rows: _all_rows,
3403 returning: ::std::vec![ #( #returning_cols ),* ],
3404 on_conflict: ::core::option::Option::None,
3405 };
3406 let _returned = ::rustango::sql::bulk_insert_on(
3407 #executor_passes_to_data_write,
3408 &_query,
3409 ).await?;
3410 if _returned.len() != rows.len() {
3411 return ::core::result::Result::Err(
3412 ::rustango::sql::ExecError::Sql(
3413 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
3414 expected: rows.len(),
3415 actual: _returned.len(),
3416 }
3417 )
3418 );
3419 }
3420 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
3421 #auto_assigns_for_row
3422 }
3423 #audit_bulk_insert_emit
3424 ::core::result::Result::Ok(())
3425 }
3426 }
3427 } else {
3428 let cols_all = &fields.bulk_columns_all;
3429 let pushes_all = &fields.bulk_pushes_all;
3430 quote! {
3431 #[cfg(feature = "postgres")]
3441 pub async fn bulk_insert(
3442 rows: &[Self],
3443 pool: &::rustango::sql::sqlx::PgPool,
3444 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3445 Self::bulk_insert_on(rows, pool).await
3446 }
3447
3448 #[cfg(feature = "postgres")]
3454 pub async fn bulk_insert_on<'_c, _E>(
3455 rows: &[Self],
3456 _executor: _E,
3457 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
3458 where
3459 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
3460 {
3461 if rows.is_empty() {
3462 return ::core::result::Result::Ok(());
3463 }
3464 let mut _all_rows: ::std::vec::Vec<
3465 ::std::vec::Vec<::rustango::core::SqlValue>,
3466 > = ::std::vec::Vec::with_capacity(rows.len());
3467 for _row in rows.iter() {
3468 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
3469 ::std::vec::Vec::new();
3470 #( #pushes_all )*
3471 _all_rows.push(_row_vals);
3472 }
3473 let _query = ::rustango::core::BulkInsertQuery {
3474 model: <Self as ::rustango::core::Model>::SCHEMA,
3475 columns: ::std::vec![ #( #cols_all ),* ],
3476 rows: _all_rows,
3477 returning: ::std::vec::Vec::new(),
3478 on_conflict: ::core::option::Option::None,
3479 };
3480 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
3481 ::core::result::Result::Ok(())
3482 }
3483 }
3484 };
3485
3486 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
3487 quote! {
3488 #[doc(hidden)]
3493 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
3494 ::core::convert::Into::<::rustango::core::SqlValue>::into(
3495 ::core::clone::Clone::clone(&self.#pk_ident)
3496 )
3497 }
3498 }
3499 });
3500
3501 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
3502 quote! {
3503 impl ::rustango::sql::HasPkValue for #struct_name {
3504 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
3505 ::core::convert::Into::<::rustango::core::SqlValue>::into(
3506 ::core::clone::Clone::clone(&self.#pk_ident)
3507 )
3508 }
3509 }
3510 }
3511 });
3512
3513 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
3514
3515 let assign_auto_pk_pool_impl = {
3521 let auto_assigns = &fields.auto_assigns;
3522 let auto_assigns_sqlite: Vec<TokenStream2> = fields
3528 .auto_field_idents
3529 .iter()
3530 .map(|(ident, column)| {
3531 quote! {
3532 self.#ident = ::rustango::sql::try_get_returning_sqlite(
3533 _returning_row, #column
3534 )?;
3535 }
3536 })
3537 .collect();
3538 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
3539 let value_ty = fields
3557 .first_auto_value_ty
3558 .as_ref()
3559 .expect("first_auto_value_ty set whenever first_auto_ident is");
3560 quote! {
3561 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
3562 ::rustango_from_mysql_auto_id(_id)?;
3563 self.#first = ::rustango::sql::Auto::Set(_converted);
3564 ::core::result::Result::Ok(())
3565 }
3566 } else {
3567 quote! {
3568 let _ = _id;
3569 ::core::result::Result::Ok(())
3570 }
3571 };
3572 quote! {
3573 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
3574 fn __rustango_assign_from_pg_row(
3575 &mut self,
3576 _returning_row: &::rustango::sql::PgReturningRow,
3577 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3578 #( #auto_assigns )*
3579 ::core::result::Result::Ok(())
3580 }
3581 fn __rustango_assign_from_mysql_id(
3582 &mut self,
3583 _id: i64,
3584 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3585 #mysql_body
3586 }
3587 fn __rustango_assign_from_sqlite_row(
3588 &mut self,
3589 _returning_row: &::rustango::sql::SqliteReturningRow,
3590 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3591 #( #auto_assigns_sqlite )*
3592 ::core::result::Result::Ok(())
3593 }
3594 }
3595 }
3596 };
3597
3598 let from_aliased_row_inits = &fields.from_aliased_row_inits;
3599 let aliased_row_helper = quote! {
3600 #[doc(hidden)]
3606 #[cfg(feature = "postgres")]
3607 pub fn __rustango_from_aliased_row(
3608 row: &::rustango::sql::sqlx::postgres::PgRow,
3609 prefix: &str,
3610 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3611 ::core::result::Result::Ok(Self {
3612 #( #from_aliased_row_inits ),*
3613 })
3614 }
3615 };
3616 let aliased_row_helper_my = quote! {
3619 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
3620 #( #from_aliased_row_inits ),*
3621 });
3622 };
3623
3624 let aliased_row_helper_sqlite = quote! {
3627 ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
3628 #( #from_aliased_row_inits ),*
3629 });
3630 };
3631
3632 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
3633 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
3634 let load_related_impl_sqlite =
3635 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
3636
3637 quote! {
3638 impl #struct_name {
3639 #[must_use]
3641 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
3642 ::rustango::query::QuerySet::new()
3643 }
3644
3645 #insert_method
3646
3647 #bulk_insert_method
3648
3649 #save_method
3650
3651 #pk_methods
3652
3653 #pk_value_helper
3654
3655 #aliased_row_helper
3656
3657 #column_consts
3658 }
3659
3660 #aliased_row_helper_my
3661
3662 #aliased_row_helper_sqlite
3663
3664 #load_related_impl
3665
3666 #load_related_impl_my
3667
3668 #load_related_impl_sqlite
3669
3670 #has_pk_value_impl
3671
3672 #fk_pk_access_impl
3673
3674 #assign_auto_pk_pool_impl
3675 }
3676}
3677
3678fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
3682 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
3683 let col_lit = column.as_str();
3684 quote! {
3685 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
3686 _returning_row,
3687 #col_lit,
3688 )?;
3689 }
3690 });
3691 quote! { #( #lines )* }
3692}
3693
3694fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
3696 let lines = entries.iter().map(|e| {
3697 let ident = &e.ident;
3698 let col_ty = column_type_ident(ident);
3699 quote! {
3700 #[allow(non_upper_case_globals)]
3701 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
3702 }
3703 });
3704 quote! { #(#lines)* }
3705}
3706
3707fn column_module_tokens(
3710 module_ident: &syn::Ident,
3711 struct_name: &syn::Ident,
3712 entries: &[ColumnEntry],
3713) -> TokenStream2 {
3714 let items = entries.iter().map(|e| {
3715 let col_ty = column_type_ident(&e.ident);
3716 let value_ty = &e.value_ty;
3717 let name = &e.name;
3718 let column = &e.column;
3719 let field_type_tokens = &e.field_type_tokens;
3720 quote! {
3721 #[derive(::core::clone::Clone, ::core::marker::Copy)]
3722 pub struct #col_ty;
3723
3724 impl ::rustango::core::Column for #col_ty {
3725 type Model = super::#struct_name;
3726 type Value = #value_ty;
3727 const NAME: &'static str = #name;
3728 const COLUMN: &'static str = #column;
3729 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
3730 }
3731 }
3732 });
3733 quote! {
3734 #[doc(hidden)]
3735 #[allow(non_camel_case_types, non_snake_case)]
3736 pub mod #module_ident {
3737 #[allow(unused_imports)]
3742 use super::*;
3743 #(#items)*
3744 }
3745 }
3746}
3747
3748fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3749 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3750}
3751
3752fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3753 syn::Ident::new(
3754 &format!("__rustango_cols_{struct_name}"),
3755 struct_name.span(),
3756 )
3757}
3758
3759fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3760 quote! {
3771 #[cfg(feature = "postgres")]
3772 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3773 for #struct_name
3774 {
3775 fn from_row(
3776 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3777 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3778 ::core::result::Result::Ok(Self {
3779 #( #from_row_inits ),*
3780 })
3781 }
3782 }
3783
3784 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3785 #( #from_row_inits ),*
3786 });
3787
3788 ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
3789 #( #from_row_inits ),*
3790 });
3791 }
3792}
3793
3794struct ContainerAttrs {
3795 table: Option<String>,
3796 display: Option<(String, proc_macro2::Span)>,
3797 app: Option<String>,
3804 admin: Option<AdminAttrs>,
3809 audit: Option<AuditAttrs>,
3815 permissions: bool,
3819 m2m: Vec<M2MAttr>,
3823 indexes: Vec<IndexAttr>,
3829 checks: Vec<CheckAttr>,
3832 composite_fks: Vec<CompositeFkAttr>,
3836 generic_fks: Vec<GenericFkAttr>,
3840 scope: Option<String>,
3846}
3847
3848struct IndexAttr {
3850 name: Option<String>,
3852 columns: Vec<String>,
3854 unique: bool,
3856 method: String,
3862}
3863
3864struct CheckAttr {
3866 name: String,
3867 expr: String,
3868}
3869
3870struct CompositeFkAttr {
3876 name: String,
3878 to: String,
3880 from: Vec<String>,
3882 on: Vec<String>,
3884}
3885
3886struct GenericFkAttr {
3891 name: String,
3893 ct_column: String,
3895 pk_column: String,
3897}
3898
3899struct M2MAttr {
3901 name: String,
3903 to: String,
3905 through: String,
3907 src: String,
3909 dst: String,
3911}
3912
3913#[derive(Default)]
3919struct AuditAttrs {
3920 track: Option<(Vec<String>, proc_macro2::Span)>,
3924}
3925
3926#[derive(Default)]
3931struct AdminAttrs {
3932 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3933 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3934 list_per_page: Option<usize>,
3935 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3936 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3937 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3938 actions: Option<(Vec<String>, proc_macro2::Span)>,
3941 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3945}
3946
3947fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3948 let mut out = ContainerAttrs {
3949 table: None,
3950 display: None,
3951 app: None,
3952 admin: None,
3953 audit: None,
3954 permissions: true,
3963 m2m: Vec::new(),
3964 indexes: Vec::new(),
3965 checks: Vec::new(),
3966 composite_fks: Vec::new(),
3967 generic_fks: Vec::new(),
3968 scope: None,
3969 };
3970 for attr in &input.attrs {
3971 if !attr.path().is_ident("rustango") {
3972 continue;
3973 }
3974 attr.parse_nested_meta(|meta| {
3975 if meta.path.is_ident("table") {
3976 let s: LitStr = meta.value()?.parse()?;
3977 let name = s.value();
3978 validate_table_name(&name, s.span())?;
3988 out.table = Some(name);
3989 return Ok(());
3990 }
3991 if meta.path.is_ident("display") {
3992 let s: LitStr = meta.value()?.parse()?;
3993 out.display = Some((s.value(), s.span()));
3994 return Ok(());
3995 }
3996 if meta.path.is_ident("app") {
3997 let s: LitStr = meta.value()?.parse()?;
3998 out.app = Some(s.value());
3999 return Ok(());
4000 }
4001 if meta.path.is_ident("scope") {
4002 let s: LitStr = meta.value()?.parse()?;
4003 let val = s.value();
4004 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
4005 return Err(meta.error(format!(
4006 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
4007 )));
4008 }
4009 out.scope = Some(val);
4010 return Ok(());
4011 }
4012 if meta.path.is_ident("admin") {
4013 let mut admin = AdminAttrs::default();
4014 meta.parse_nested_meta(|inner| {
4015 if inner.path.is_ident("list_display") {
4016 let s: LitStr = inner.value()?.parse()?;
4017 admin.list_display =
4018 Some((split_field_list(&s.value()), s.span()));
4019 return Ok(());
4020 }
4021 if inner.path.is_ident("search_fields") {
4022 let s: LitStr = inner.value()?.parse()?;
4023 admin.search_fields =
4024 Some((split_field_list(&s.value()), s.span()));
4025 return Ok(());
4026 }
4027 if inner.path.is_ident("readonly_fields") {
4028 let s: LitStr = inner.value()?.parse()?;
4029 admin.readonly_fields =
4030 Some((split_field_list(&s.value()), s.span()));
4031 return Ok(());
4032 }
4033 if inner.path.is_ident("list_per_page") {
4034 let lit: syn::LitInt = inner.value()?.parse()?;
4035 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
4036 return Ok(());
4037 }
4038 if inner.path.is_ident("ordering") {
4039 let s: LitStr = inner.value()?.parse()?;
4040 admin.ordering = Some((
4041 parse_ordering_list(&s.value()),
4042 s.span(),
4043 ));
4044 return Ok(());
4045 }
4046 if inner.path.is_ident("list_filter") {
4047 let s: LitStr = inner.value()?.parse()?;
4048 admin.list_filter =
4049 Some((split_field_list(&s.value()), s.span()));
4050 return Ok(());
4051 }
4052 if inner.path.is_ident("actions") {
4053 let s: LitStr = inner.value()?.parse()?;
4054 admin.actions =
4055 Some((split_field_list(&s.value()), s.span()));
4056 return Ok(());
4057 }
4058 if inner.path.is_ident("fieldsets") {
4059 let s: LitStr = inner.value()?.parse()?;
4060 admin.fieldsets =
4061 Some((parse_fieldset_list(&s.value()), s.span()));
4062 return Ok(());
4063 }
4064 Err(inner.error(
4065 "unknown admin attribute (supported: \
4066 `list_display`, `search_fields`, `readonly_fields`, \
4067 `list_filter`, `list_per_page`, `ordering`, `actions`, \
4068 `fieldsets`)",
4069 ))
4070 })?;
4071 out.admin = Some(admin);
4072 return Ok(());
4073 }
4074 if meta.path.is_ident("audit") {
4075 let mut audit = AuditAttrs::default();
4076 meta.parse_nested_meta(|inner| {
4077 if inner.path.is_ident("track") {
4078 let s: LitStr = inner.value()?.parse()?;
4079 audit.track =
4080 Some((split_field_list(&s.value()), s.span()));
4081 return Ok(());
4082 }
4083 Err(inner.error(
4084 "unknown audit attribute (supported: `track`)",
4085 ))
4086 })?;
4087 out.audit = Some(audit);
4088 return Ok(());
4089 }
4090 if meta.path.is_ident("permissions") {
4091 if let Ok(v) = meta.value() {
4096 let lit: syn::LitBool = v.parse()?;
4097 out.permissions = lit.value;
4098 } else {
4099 out.permissions = true;
4100 }
4101 return Ok(());
4102 }
4103 if meta.path.is_ident("unique_together") {
4104 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
4113 out.indexes.push(IndexAttr {
4114 name,
4115 columns,
4116 unique: true,
4117 method: "btree".to_owned(),
4118 });
4119 return Ok(());
4120 }
4121 if meta.path.is_ident("index_together") {
4122 let (columns, name) = parse_together_attr(&meta, "index_together")?;
4128 out.indexes.push(IndexAttr {
4129 name,
4130 columns,
4131 unique: false,
4132 method: "btree".to_owned(),
4133 });
4134 return Ok(());
4135 }
4136 if meta.path.is_ident("index") {
4137 let cols_lit: LitStr = meta.value()?.parse()?;
4145 let columns = split_field_list(&cols_lit.value());
4146 out.indexes.push(IndexAttr {
4147 name: None,
4148 columns,
4149 unique: false,
4150 method: "btree".to_owned(),
4151 });
4152 return Ok(());
4153 }
4154 if meta.path.is_ident("check") {
4155 let mut name: Option<String> = None;
4157 let mut expr: Option<String> = None;
4158 meta.parse_nested_meta(|inner| {
4159 if inner.path.is_ident("name") {
4160 let s: LitStr = inner.value()?.parse()?;
4161 name = Some(s.value());
4162 return Ok(());
4163 }
4164 if inner.path.is_ident("expr") {
4165 let s: LitStr = inner.value()?.parse()?;
4166 expr = Some(s.value());
4167 return Ok(());
4168 }
4169 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
4170 })?;
4171 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
4172 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
4173 out.checks.push(CheckAttr { name, expr });
4174 return Ok(());
4175 }
4176 if meta.path.is_ident("generic_fk") {
4177 let mut gfk = GenericFkAttr {
4178 name: String::new(),
4179 ct_column: String::new(),
4180 pk_column: String::new(),
4181 };
4182 meta.parse_nested_meta(|inner| {
4183 if inner.path.is_ident("name") {
4184 let s: LitStr = inner.value()?.parse()?;
4185 gfk.name = s.value();
4186 return Ok(());
4187 }
4188 if inner.path.is_ident("ct_column") {
4189 let s: LitStr = inner.value()?.parse()?;
4190 gfk.ct_column = s.value();
4191 return Ok(());
4192 }
4193 if inner.path.is_ident("pk_column") {
4194 let s: LitStr = inner.value()?.parse()?;
4195 gfk.pk_column = s.value();
4196 return Ok(());
4197 }
4198 Err(inner.error(
4199 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
4200 ))
4201 })?;
4202 if gfk.name.is_empty() {
4203 return Err(meta.error("generic_fk requires `name = \"...\"`"));
4204 }
4205 if gfk.ct_column.is_empty() {
4206 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
4207 }
4208 if gfk.pk_column.is_empty() {
4209 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
4210 }
4211 out.generic_fks.push(gfk);
4212 return Ok(());
4213 }
4214 if meta.path.is_ident("fk_composite") {
4215 let mut fk = CompositeFkAttr {
4216 name: String::new(),
4217 to: String::new(),
4218 from: Vec::new(),
4219 on: Vec::new(),
4220 };
4221 meta.parse_nested_meta(|inner| {
4222 if inner.path.is_ident("name") {
4223 let s: LitStr = inner.value()?.parse()?;
4224 fk.name = s.value();
4225 return Ok(());
4226 }
4227 if inner.path.is_ident("to") {
4228 let s: LitStr = inner.value()?.parse()?;
4229 fk.to = s.value();
4230 return Ok(());
4231 }
4232 if inner.path.is_ident("on") || inner.path.is_ident("from") {
4235 let value = inner.value()?;
4236 let content;
4237 syn::parenthesized!(content in value);
4238 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
4239 content.parse_terminated(
4240 |p| p.parse::<syn::LitStr>(),
4241 syn::Token![,],
4242 )?;
4243 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
4244 if inner.path.is_ident("on") {
4245 fk.on = cols;
4246 } else {
4247 fk.from = cols;
4248 }
4249 return Ok(());
4250 }
4251 Err(inner.error(
4252 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
4253 ))
4254 })?;
4255 if fk.name.is_empty() {
4256 return Err(meta.error("fk_composite requires `name = \"...\"`"));
4257 }
4258 if fk.to.is_empty() {
4259 return Err(meta.error("fk_composite requires `to = \"...\"`"));
4260 }
4261 if fk.from.is_empty() || fk.on.is_empty() {
4262 return Err(meta.error(
4263 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
4264 ));
4265 }
4266 if fk.from.len() != fk.on.len() {
4267 return Err(meta.error(format!(
4268 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
4269 fk.from.len(),
4270 fk.on.len(),
4271 )));
4272 }
4273 out.composite_fks.push(fk);
4274 return Ok(());
4275 }
4276 if meta.path.is_ident("m2m") {
4277 let mut m2m = M2MAttr {
4278 name: String::new(),
4279 to: String::new(),
4280 through: String::new(),
4281 src: String::new(),
4282 dst: String::new(),
4283 };
4284 meta.parse_nested_meta(|inner| {
4285 if inner.path.is_ident("name") {
4286 let s: LitStr = inner.value()?.parse()?;
4287 m2m.name = s.value();
4288 return Ok(());
4289 }
4290 if inner.path.is_ident("to") {
4291 let s: LitStr = inner.value()?.parse()?;
4292 m2m.to = s.value();
4293 return Ok(());
4294 }
4295 if inner.path.is_ident("through") {
4296 let s: LitStr = inner.value()?.parse()?;
4297 m2m.through = s.value();
4298 return Ok(());
4299 }
4300 if inner.path.is_ident("src") {
4301 let s: LitStr = inner.value()?.parse()?;
4302 m2m.src = s.value();
4303 return Ok(());
4304 }
4305 if inner.path.is_ident("dst") {
4306 let s: LitStr = inner.value()?.parse()?;
4307 m2m.dst = s.value();
4308 return Ok(());
4309 }
4310 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
4311 })?;
4312 if m2m.name.is_empty() {
4313 return Err(meta.error("m2m requires `name = \"...\"`"));
4314 }
4315 if m2m.to.is_empty() {
4316 return Err(meta.error("m2m requires `to = \"...\"`"));
4317 }
4318 if m2m.through.is_empty() {
4319 return Err(meta.error("m2m requires `through = \"...\"`"));
4320 }
4321 if m2m.src.is_empty() {
4322 return Err(meta.error("m2m requires `src = \"...\"`"));
4323 }
4324 if m2m.dst.is_empty() {
4325 return Err(meta.error("m2m requires `dst = \"...\"`"));
4326 }
4327 out.m2m.push(m2m);
4328 return Ok(());
4329 }
4330 Err(meta.error("unknown rustango container attribute"))
4331 })?;
4332 }
4333 Ok(out)
4334}
4335
4336fn split_field_list(raw: &str) -> Vec<String> {
4340 raw.split(',')
4341 .map(str::trim)
4342 .filter(|s| !s.is_empty())
4343 .map(str::to_owned)
4344 .collect()
4345}
4346
4347fn parse_together_attr(
4355 meta: &syn::meta::ParseNestedMeta<'_>,
4356 attr: &str,
4357) -> syn::Result<(Vec<String>, Option<String>)> {
4358 if meta.input.peek(syn::Token![=]) {
4361 let cols_lit: LitStr = meta.value()?.parse()?;
4362 let columns = split_field_list(&cols_lit.value());
4363 check_together_columns(meta, attr, &columns)?;
4364 return Ok((columns, None));
4365 }
4366 let mut columns: Option<Vec<String>> = None;
4367 let mut name: Option<String> = None;
4368 meta.parse_nested_meta(|inner| {
4369 if inner.path.is_ident("columns") {
4370 let s: LitStr = inner.value()?.parse()?;
4371 columns = Some(split_field_list(&s.value()));
4372 return Ok(());
4373 }
4374 if inner.path.is_ident("name") {
4375 let s: LitStr = inner.value()?.parse()?;
4376 name = Some(s.value());
4377 return Ok(());
4378 }
4379 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
4380 })?;
4381 let columns = columns.ok_or_else(|| {
4382 meta.error(format!(
4383 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
4384 ))
4385 })?;
4386 check_together_columns(meta, attr, &columns)?;
4387 Ok((columns, name))
4388}
4389
4390fn check_together_columns(
4391 meta: &syn::meta::ParseNestedMeta<'_>,
4392 attr: &str,
4393 columns: &[String],
4394) -> syn::Result<()> {
4395 if columns.len() < 2 {
4396 let single = if attr == "unique_together" {
4397 "#[rustango(unique)] on the field"
4398 } else {
4399 "#[rustango(index)] on the field"
4400 };
4401 return Err(meta.error(format!(
4402 "{attr} expects two or more columns; for a single-column equivalent use {single}",
4403 )));
4404 }
4405 Ok(())
4406}
4407
4408fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
4417 raw.split('|')
4418 .map(str::trim)
4419 .filter(|s| !s.is_empty())
4420 .map(|section| {
4421 let (title, rest) = match section.split_once(':') {
4423 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
4424 _ => (String::new(), section),
4425 };
4426 let fields = split_field_list(rest);
4427 (title, fields)
4428 })
4429 .collect()
4430}
4431
4432fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
4435 raw.split(',')
4436 .map(str::trim)
4437 .filter(|s| !s.is_empty())
4438 .map(|spec| {
4439 spec.strip_prefix('-')
4440 .map_or((spec.to_owned(), false), |rest| {
4441 (rest.trim().to_owned(), true)
4442 })
4443 })
4444 .collect()
4445}
4446
4447struct FieldAttrs {
4448 column: Option<String>,
4449 primary_key: bool,
4450 fk: Option<String>,
4451 o2o: Option<String>,
4452 on: Option<String>,
4453 max_length: Option<u32>,
4454 min: Option<i64>,
4455 max: Option<i64>,
4456 default: Option<String>,
4457 auto_uuid: bool,
4463 auto_now_add: bool,
4468 auto_now: bool,
4474 soft_delete: bool,
4479 unique: bool,
4482 index: bool,
4486 index_unique: bool,
4487 index_name: Option<String>,
4488 index_method: String,
4491 generated_as: Option<String>,
4497 help_text: Option<String>,
4502}
4503
4504fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
4505 let mut out = FieldAttrs {
4506 column: None,
4507 primary_key: false,
4508 fk: None,
4509 o2o: None,
4510 on: None,
4511 max_length: None,
4512 min: None,
4513 max: None,
4514 default: None,
4515 auto_uuid: false,
4516 auto_now_add: false,
4517 auto_now: false,
4518 soft_delete: false,
4519 unique: false,
4520 index: false,
4521 index_unique: false,
4522 index_name: None,
4523 index_method: "btree".to_owned(),
4524 generated_as: None,
4525 help_text: None,
4526 };
4527 for attr in &field.attrs {
4528 if !attr.path().is_ident("rustango") {
4529 continue;
4530 }
4531 attr.parse_nested_meta(|meta| {
4532 if meta.path.is_ident("column") {
4533 let s: LitStr = meta.value()?.parse()?;
4534 let name = s.value();
4535 validate_sql_identifier(&name, "column", s.span())?;
4536 out.column = Some(name);
4537 return Ok(());
4538 }
4539 if meta.path.is_ident("primary_key") {
4540 out.primary_key = true;
4541 return Ok(());
4542 }
4543 if meta.path.is_ident("fk") {
4544 let s: LitStr = meta.value()?.parse()?;
4545 out.fk = Some(s.value());
4546 return Ok(());
4547 }
4548 if meta.path.is_ident("o2o") {
4549 let s: LitStr = meta.value()?.parse()?;
4550 out.o2o = Some(s.value());
4551 return Ok(());
4552 }
4553 if meta.path.is_ident("on") {
4554 let s: LitStr = meta.value()?.parse()?;
4555 out.on = Some(s.value());
4556 return Ok(());
4557 }
4558 if meta.path.is_ident("max_length") {
4559 let lit: syn::LitInt = meta.value()?.parse()?;
4560 out.max_length = Some(lit.base10_parse::<u32>()?);
4561 return Ok(());
4562 }
4563 if meta.path.is_ident("min") {
4564 out.min = Some(parse_signed_i64(&meta)?);
4565 return Ok(());
4566 }
4567 if meta.path.is_ident("max") {
4568 out.max = Some(parse_signed_i64(&meta)?);
4569 return Ok(());
4570 }
4571 if meta.path.is_ident("default") {
4572 let s: LitStr = meta.value()?.parse()?;
4573 out.default = Some(s.value());
4574 return Ok(());
4575 }
4576 if meta.path.is_ident("generated_as") {
4577 let s: LitStr = meta.value()?.parse()?;
4578 out.generated_as = Some(s.value());
4579 return Ok(());
4580 }
4581 if meta.path.is_ident("help_text") {
4582 let s: LitStr = meta.value()?.parse()?;
4583 out.help_text = Some(s.value());
4584 return Ok(());
4585 }
4586 if meta.path.is_ident("auto_uuid") {
4587 out.auto_uuid = true;
4588 out.primary_key = true;
4592 if out.default.is_none() {
4593 out.default = Some("gen_random_uuid()".into());
4594 }
4595 return Ok(());
4596 }
4597 if meta.path.is_ident("auto_now_add") {
4598 out.auto_now_add = true;
4599 if out.default.is_none() {
4600 out.default = Some("now()".into());
4601 }
4602 return Ok(());
4603 }
4604 if meta.path.is_ident("auto_now") {
4605 out.auto_now = true;
4606 if out.default.is_none() {
4607 out.default = Some("now()".into());
4608 }
4609 return Ok(());
4610 }
4611 if meta.path.is_ident("soft_delete") {
4612 out.soft_delete = true;
4613 return Ok(());
4614 }
4615 if meta.path.is_ident("unique") {
4616 out.unique = true;
4617 return Ok(());
4618 }
4619 if meta.path.is_ident("index") {
4620 out.index = true;
4621 if meta.input.peek(syn::token::Paren) {
4623 meta.parse_nested_meta(|inner| {
4624 if inner.path.is_ident("unique") {
4625 out.index_unique = true;
4626 return Ok(());
4627 }
4628 if inner.path.is_ident("name") {
4629 let s: LitStr = inner.value()?.parse()?;
4630 out.index_name = Some(s.value());
4631 return Ok(());
4632 }
4633 if inner.path.is_ident("method") {
4634 let s: LitStr = inner.value()?.parse()?;
4635 let v = s.value();
4636 match v.as_str() {
4637 "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
4638 out.index_method = v;
4639 }
4640 other => {
4641 return Err(inner.error(format!(
4642 "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
4643 )));
4644 }
4645 }
4646 return Ok(());
4647 }
4648 Err(inner.error(
4649 "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
4650 ))
4651 })?;
4652 }
4653 return Ok(());
4654 }
4655 Err(meta.error("unknown rustango field attribute"))
4656 })?;
4657 }
4658 Ok(out)
4659}
4660
4661fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
4663 let expr: syn::Expr = meta.value()?.parse()?;
4664 match expr {
4665 syn::Expr::Lit(syn::ExprLit {
4666 lit: syn::Lit::Int(lit),
4667 ..
4668 }) => lit.base10_parse::<i64>(),
4669 syn::Expr::Unary(syn::ExprUnary {
4670 op: syn::UnOp::Neg(_),
4671 expr,
4672 ..
4673 }) => {
4674 if let syn::Expr::Lit(syn::ExprLit {
4675 lit: syn::Lit::Int(lit),
4676 ..
4677 }) = *expr
4678 {
4679 let v: i64 = lit.base10_parse()?;
4680 Ok(-v)
4681 } else {
4682 Err(syn::Error::new_spanned(expr, "expected integer literal"))
4683 }
4684 }
4685 other => Err(syn::Error::new_spanned(
4686 other,
4687 "expected integer literal (signed)",
4688 )),
4689 }
4690}
4691
4692struct FieldInfo<'a> {
4693 ident: &'a syn::Ident,
4694 column: String,
4695 primary_key: bool,
4696 auto: bool,
4700 value_ty: &'a Type,
4703 field_type_tokens: TokenStream2,
4705 schema: TokenStream2,
4706 from_row_init: TokenStream2,
4707 from_aliased_row_init: TokenStream2,
4713 fk_inner: Option<Type>,
4717 fk_pk_kind: DetectedKind,
4723 nullable: bool,
4731 auto_now: bool,
4737 auto_now_add: bool,
4743 soft_delete: bool,
4748 generated_as: Option<String>,
4753}
4754
4755fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
4769 validate_sql_identifier(name, "table", span)
4770}
4771
4772fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
4777 if name.is_empty() {
4778 return Err(syn::Error::new(
4779 span,
4780 format!("`{kind} = \"\"` is not a valid SQL identifier"),
4781 ));
4782 }
4783 let mut chars = name.chars();
4784 let first = chars.next().unwrap();
4785 if !(first.is_ascii_alphabetic() || first == '_') {
4786 return Err(syn::Error::new(
4787 span,
4788 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
4789 ));
4790 }
4791 for c in chars {
4792 if !(c.is_ascii_alphanumeric() || c == '_') {
4793 return Err(syn::Error::new(
4794 span,
4795 format!(
4796 "{kind} name `{name}` contains invalid character {c:?} — \
4797 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
4798 Hyphens in particular break FK / index name derivation \
4799 downstream; use underscores instead (e.g. `{}`)",
4800 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
4801 ),
4802 ));
4803 }
4804 }
4805 Ok(())
4806}
4807
4808fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
4809 let attrs = parse_field_attrs(field)?;
4810 let ident = field
4811 .ident
4812 .as_ref()
4813 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4814 let name = ident.to_string();
4815 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
4816 let primary_key = attrs.primary_key;
4817 let DetectedType {
4818 kind,
4819 nullable,
4820 auto: detected_auto,
4821 fk_inner,
4822 } = detect_type(&field.ty)?;
4823 check_bound_compatibility(field, &attrs, kind)?;
4824 let auto = detected_auto;
4825 if attrs.auto_uuid {
4831 if kind != DetectedKind::Uuid {
4832 return Err(syn::Error::new_spanned(
4833 field,
4834 "`#[rustango(auto_uuid)]` requires the field type to be \
4835 `Auto<uuid::Uuid>`",
4836 ));
4837 }
4838 if !detected_auto {
4839 return Err(syn::Error::new_spanned(
4840 field,
4841 "`#[rustango(auto_uuid)]` requires the field type to be \
4842 wrapped in `Auto<...>` so the macro skips the column on \
4843 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
4844 ));
4845 }
4846 }
4847 if attrs.auto_now_add || attrs.auto_now {
4848 if kind != DetectedKind::DateTime {
4849 return Err(syn::Error::new_spanned(
4850 field,
4851 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4852 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
4853 ));
4854 }
4855 if !detected_auto {
4856 return Err(syn::Error::new_spanned(
4857 field,
4858 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4859 the field type to be wrapped in `Auto<...>` so the macro skips \
4860 the column on INSERT and the DB DEFAULT (`now()`) fires",
4861 ));
4862 }
4863 }
4864 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
4865 return Err(syn::Error::new_spanned(
4866 field,
4867 "`#[rustango(soft_delete)]` requires the field type to be \
4868 `Option<chrono::DateTime<chrono::Utc>>`",
4869 ));
4870 }
4871 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
4872 if detected_auto && !primary_key && !is_mixin_auto {
4873 return Err(syn::Error::new_spanned(
4874 field,
4875 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
4876 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
4877 `auto_now`",
4878 ));
4879 }
4880 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
4881 return Err(syn::Error::new_spanned(
4882 field,
4883 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
4884 SERIAL / BIGSERIAL already supplies a default sequence.",
4885 ));
4886 }
4887 if fk_inner.is_some() && primary_key {
4888 return Err(syn::Error::new_spanned(
4889 field,
4890 "`ForeignKey<T>` is not allowed on a primary-key field — \
4891 a row's PK is its own identity, not a reference to a parent.",
4892 ));
4893 }
4894 if attrs.generated_as.is_some() {
4895 if primary_key {
4896 return Err(syn::Error::new_spanned(
4897 field,
4898 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4899 primary-key field — a PK must be writable so the row \
4900 has an identity at INSERT time.",
4901 ));
4902 }
4903 if attrs.default.is_some() {
4904 return Err(syn::Error::new_spanned(
4905 field,
4906 "`#[rustango(generated_as = \"…\")]` cannot combine with \
4907 `default = \"…\"` — Postgres rejects DEFAULT on \
4908 generated columns. The expression IS the default.",
4909 ));
4910 }
4911 if detected_auto {
4912 return Err(syn::Error::new_spanned(
4913 field,
4914 "`#[rustango(generated_as = \"…\")]` is not allowed on \
4915 an `Auto<T>` field — generated columns are computed \
4916 by the DB, not server-assigned via a sequence. Use a \
4917 plain Rust type (e.g. `f64`).",
4918 ));
4919 }
4920 if fk_inner.is_some() {
4921 return Err(syn::Error::new_spanned(
4922 field,
4923 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4924 ForeignKey field.",
4925 ));
4926 }
4927 }
4928 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
4929 let column_lit = column.as_str();
4930 let field_type_tokens = kind.variant_tokens();
4931 let max_length = optional_u32(attrs.max_length);
4932 let min = optional_i64(attrs.min);
4933 let max = optional_i64(attrs.max);
4934 let default = optional_str(attrs.default.as_deref());
4935
4936 let unique = attrs.unique;
4937 let generated_as = optional_str(attrs.generated_as.as_deref());
4938 let help_text = optional_str(attrs.help_text.as_deref());
4939 let schema = quote! {
4940 ::rustango::core::FieldSchema {
4941 name: #name,
4942 column: #column_lit,
4943 ty: #field_type_tokens,
4944 nullable: #nullable,
4945 primary_key: #primary_key,
4946 relation: #relation,
4947 max_length: #max_length,
4948 min: #min,
4949 max: #max,
4950 default: #default,
4951 auto: #auto,
4952 unique: #unique,
4953 generated_as: #generated_as,
4954 help_text: #help_text,
4955 }
4956 };
4957
4958 let from_row_init = quote! {
4959 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4960 };
4961 let from_aliased_row_init = quote! {
4962 #ident: ::rustango::sql::sqlx::Row::try_get(
4963 row,
4964 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4965 )?
4966 };
4967
4968 Ok(FieldInfo {
4969 ident,
4970 column,
4971 primary_key,
4972 auto,
4973 value_ty: &field.ty,
4974 field_type_tokens,
4975 schema,
4976 from_row_init,
4977 from_aliased_row_init,
4978 fk_inner: fk_inner.cloned(),
4979 fk_pk_kind: kind,
4980 nullable,
4981 auto_now: attrs.auto_now,
4982 auto_now_add: attrs.auto_now_add,
4983 soft_delete: attrs.soft_delete,
4984 generated_as: attrs.generated_as.clone(),
4985 })
4986}
4987
4988fn check_bound_compatibility(
4989 field: &syn::Field,
4990 attrs: &FieldAttrs,
4991 kind: DetectedKind,
4992) -> syn::Result<()> {
4993 if attrs.max_length.is_some() && kind != DetectedKind::String {
4994 return Err(syn::Error::new_spanned(
4995 field,
4996 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4997 ));
4998 }
4999 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
5000 return Err(syn::Error::new_spanned(
5001 field,
5002 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
5003 ));
5004 }
5005 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
5006 if min > max {
5007 return Err(syn::Error::new_spanned(
5008 field,
5009 format!("`min` ({min}) is greater than `max` ({max})"),
5010 ));
5011 }
5012 }
5013 Ok(())
5014}
5015
5016fn optional_u32(value: Option<u32>) -> TokenStream2 {
5017 if let Some(v) = value {
5018 quote!(::core::option::Option::Some(#v))
5019 } else {
5020 quote!(::core::option::Option::None)
5021 }
5022}
5023
5024fn optional_i64(value: Option<i64>) -> TokenStream2 {
5025 if let Some(v) = value {
5026 quote!(::core::option::Option::Some(#v))
5027 } else {
5028 quote!(::core::option::Option::None)
5029 }
5030}
5031
5032fn optional_str(value: Option<&str>) -> TokenStream2 {
5033 if let Some(v) = value {
5034 quote!(::core::option::Option::Some(#v))
5035 } else {
5036 quote!(::core::option::Option::None)
5037 }
5038}
5039
5040fn relation_tokens(
5041 field: &syn::Field,
5042 attrs: &FieldAttrs,
5043 fk_inner: Option<&syn::Type>,
5044 table: &str,
5045) -> syn::Result<TokenStream2> {
5046 if let Some(inner) = fk_inner {
5047 if attrs.fk.is_some() || attrs.o2o.is_some() {
5048 return Err(syn::Error::new_spanned(
5049 field,
5050 "`ForeignKey<T>` already declares the FK target via the type parameter — \
5051 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
5052 ));
5053 }
5054 let on = attrs.on.as_deref().unwrap_or("id");
5055 return Ok(quote! {
5056 ::core::option::Option::Some(::rustango::core::Relation::Fk {
5057 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
5058 on: #on,
5059 })
5060 });
5061 }
5062 match (&attrs.fk, &attrs.o2o) {
5063 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
5064 field,
5065 "`fk` and `o2o` are mutually exclusive",
5066 )),
5067 (Some(to), None) => {
5068 let on = attrs.on.as_deref().unwrap_or("id");
5069 let resolved = if to == "self" { table } else { to };
5075 Ok(quote! {
5076 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
5077 })
5078 }
5079 (None, Some(to)) => {
5080 let on = attrs.on.as_deref().unwrap_or("id");
5081 let resolved = if to == "self" { table } else { to };
5082 Ok(quote! {
5083 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
5084 })
5085 }
5086 (None, None) => {
5087 if attrs.on.is_some() {
5088 return Err(syn::Error::new_spanned(
5089 field,
5090 "`on` requires `fk` or `o2o`",
5091 ));
5092 }
5093 Ok(quote!(::core::option::Option::None))
5094 }
5095 }
5096}
5097
5098#[derive(Clone, Copy, PartialEq, Eq)]
5102enum DetectedKind {
5103 I16,
5104 I32,
5105 I64,
5106 F32,
5107 F64,
5108 Bool,
5109 String,
5110 DateTime,
5111 Date,
5112 Uuid,
5113 Json,
5114}
5115
5116impl DetectedKind {
5117 fn variant_tokens(self) -> TokenStream2 {
5118 match self {
5119 Self::I16 => quote!(::rustango::core::FieldType::I16),
5120 Self::I32 => quote!(::rustango::core::FieldType::I32),
5121 Self::I64 => quote!(::rustango::core::FieldType::I64),
5122 Self::F32 => quote!(::rustango::core::FieldType::F32),
5123 Self::F64 => quote!(::rustango::core::FieldType::F64),
5124 Self::Bool => quote!(::rustango::core::FieldType::Bool),
5125 Self::String => quote!(::rustango::core::FieldType::String),
5126 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
5127 Self::Date => quote!(::rustango::core::FieldType::Date),
5128 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
5129 Self::Json => quote!(::rustango::core::FieldType::Json),
5130 }
5131 }
5132
5133 fn is_integer(self) -> bool {
5134 matches!(self, Self::I16 | Self::I32 | Self::I64)
5135 }
5136
5137 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
5145 match self {
5146 Self::I16 => (quote!(I16), quote!(0i16)),
5147 Self::I32 => (quote!(I32), quote!(0i32)),
5148 Self::I64 => (quote!(I64), quote!(0i64)),
5149 Self::F32 => (quote!(F32), quote!(0f32)),
5150 Self::F64 => (quote!(F64), quote!(0f64)),
5151 Self::Bool => (quote!(Bool), quote!(false)),
5152 Self::String => (quote!(String), quote!(::std::string::String::new())),
5153 Self::DateTime => (
5154 quote!(DateTime),
5155 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
5156 ),
5157 Self::Date => (
5158 quote!(Date),
5159 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
5160 ),
5161 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
5162 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
5163 }
5164 }
5165}
5166
5167#[derive(Clone, Copy)]
5173struct DetectedType<'a> {
5174 kind: DetectedKind,
5175 nullable: bool,
5176 auto: bool,
5177 fk_inner: Option<&'a syn::Type>,
5178}
5179
5180fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
5185 let Type::Path(TypePath { path, qself: None }) = ty else {
5186 return None;
5187 };
5188 let last = path.segments.last()?;
5189 if last.ident != "Auto" {
5190 return None;
5191 }
5192 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
5193 return None;
5194 };
5195 args.args.iter().find_map(|a| match a {
5196 syn::GenericArgument::Type(t) => Some(t),
5197 _ => None,
5198 })
5199}
5200
5201fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
5202 let Type::Path(TypePath { path, qself: None }) = ty else {
5203 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
5204 };
5205 let last = path
5206 .segments
5207 .last()
5208 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
5209
5210 if last.ident == "Option" {
5211 let inner = generic_inner(ty, &last.arguments, "Option")?;
5212 let inner_det = detect_type(inner)?;
5213 if inner_det.nullable {
5214 return Err(syn::Error::new_spanned(
5215 ty,
5216 "nested Option is not supported",
5217 ));
5218 }
5219 if inner_det.auto {
5220 return Err(syn::Error::new_spanned(
5221 ty,
5222 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
5223 ));
5224 }
5225 return Ok(DetectedType {
5226 nullable: true,
5227 ..inner_det
5228 });
5229 }
5230
5231 if last.ident == "Auto" {
5232 let inner = generic_inner(ty, &last.arguments, "Auto")?;
5233 let inner_det = detect_type(inner)?;
5234 if inner_det.auto {
5235 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
5236 }
5237 if inner_det.nullable {
5238 return Err(syn::Error::new_spanned(
5239 ty,
5240 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
5241 ));
5242 }
5243 if inner_det.fk_inner.is_some() {
5244 return Err(syn::Error::new_spanned(
5245 ty,
5246 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
5247 ));
5248 }
5249 if !matches!(
5250 inner_det.kind,
5251 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
5252 ) {
5253 return Err(syn::Error::new_spanned(
5254 ty,
5255 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
5256 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
5257 (DEFAULT now())",
5258 ));
5259 }
5260 return Ok(DetectedType {
5261 auto: true,
5262 ..inner_det
5263 });
5264 }
5265
5266 if last.ident == "ForeignKey" {
5267 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
5268 let kind = match key_ty {
5276 Some(k) => detect_type(k)?.kind,
5277 None => DetectedKind::I64,
5278 };
5279 return Ok(DetectedType {
5280 kind,
5281 nullable: false,
5282 auto: false,
5283 fk_inner: Some(inner),
5284 });
5285 }
5286
5287 let kind = match last.ident.to_string().as_str() {
5288 "i16" => DetectedKind::I16,
5289 "i32" => DetectedKind::I32,
5290 "i64" => DetectedKind::I64,
5291 "f32" => DetectedKind::F32,
5292 "f64" => DetectedKind::F64,
5293 "bool" => DetectedKind::Bool,
5294 "String" => DetectedKind::String,
5295 "DateTime" => DetectedKind::DateTime,
5296 "NaiveDate" => DetectedKind::Date,
5297 "Uuid" => DetectedKind::Uuid,
5298 "Value" => DetectedKind::Json,
5299 other => {
5300 return Err(syn::Error::new_spanned(
5301 ty,
5302 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)"),
5303 ));
5304 }
5305 };
5306 Ok(DetectedType {
5307 kind,
5308 nullable: false,
5309 auto: false,
5310 fk_inner: None,
5311 })
5312}
5313
5314fn generic_inner<'a>(
5315 ty: &'a Type,
5316 arguments: &'a PathArguments,
5317 wrapper: &str,
5318) -> syn::Result<&'a Type> {
5319 let PathArguments::AngleBracketed(args) = arguments else {
5320 return Err(syn::Error::new_spanned(
5321 ty,
5322 format!("{wrapper} requires a generic argument"),
5323 ));
5324 };
5325 args.args
5326 .iter()
5327 .find_map(|a| match a {
5328 GenericArgument::Type(t) => Some(t),
5329 _ => None,
5330 })
5331 .ok_or_else(|| {
5332 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
5333 })
5334}
5335
5336fn generic_pair<'a>(
5340 ty: &'a Type,
5341 arguments: &'a PathArguments,
5342 wrapper: &str,
5343) -> syn::Result<(&'a Type, Option<&'a Type>)> {
5344 let PathArguments::AngleBracketed(args) = arguments else {
5345 return Err(syn::Error::new_spanned(
5346 ty,
5347 format!("{wrapper} requires a generic argument"),
5348 ));
5349 };
5350 let mut types = args.args.iter().filter_map(|a| match a {
5351 GenericArgument::Type(t) => Some(t),
5352 _ => None,
5353 });
5354 let first = types.next().ok_or_else(|| {
5355 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
5356 })?;
5357 let second = types.next();
5358 Ok((first, second))
5359}
5360
5361fn to_snake_case(s: &str) -> String {
5362 let mut out = String::with_capacity(s.len() + 4);
5363 for (i, ch) in s.chars().enumerate() {
5364 if ch.is_ascii_uppercase() {
5365 if i > 0 {
5366 out.push('_');
5367 }
5368 out.push(ch.to_ascii_lowercase());
5369 } else {
5370 out.push(ch);
5371 }
5372 }
5373 out
5374}
5375
5376#[derive(Default)]
5382struct FormFieldAttrs {
5383 min: Option<i64>,
5384 max: Option<i64>,
5385 min_length: Option<u32>,
5386 max_length: Option<u32>,
5387}
5388
5389#[derive(Clone, Copy)]
5391enum FormFieldKind {
5392 String,
5393 I16,
5394 I32,
5395 I64,
5396 F32,
5397 F64,
5398 Bool,
5399}
5400
5401impl FormFieldKind {
5402 fn parse_method(self) -> &'static str {
5403 match self {
5404 Self::I16 => "i16",
5405 Self::I32 => "i32",
5406 Self::I64 => "i64",
5407 Self::F32 => "f32",
5408 Self::F64 => "f64",
5409 Self::String | Self::Bool => "",
5412 }
5413 }
5414}
5415
5416fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
5417 let struct_name = &input.ident;
5418
5419 let Data::Struct(data) = &input.data else {
5420 return Err(syn::Error::new_spanned(
5421 struct_name,
5422 "Form can only be derived on structs",
5423 ));
5424 };
5425 let Fields::Named(named) = &data.fields else {
5426 return Err(syn::Error::new_spanned(
5427 struct_name,
5428 "Form requires a struct with named fields",
5429 ));
5430 };
5431
5432 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
5433 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
5434
5435 for field in &named.named {
5436 let ident = field
5437 .ident
5438 .as_ref()
5439 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
5440 let attrs = parse_form_field_attrs(field)?;
5441 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
5442
5443 let name_lit = ident.to_string();
5444 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
5445 field_blocks.push(parse_block);
5446 field_idents.push(ident);
5447 }
5448
5449 Ok(quote! {
5450 impl ::rustango::forms::Form for #struct_name {
5451 fn parse(
5452 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
5453 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
5454 let mut __errors = ::rustango::forms::FormErrors::default();
5455 #( #field_blocks )*
5456 if !__errors.is_empty() {
5457 return ::core::result::Result::Err(__errors);
5458 }
5459 ::core::result::Result::Ok(Self {
5460 #( #field_idents ),*
5461 })
5462 }
5463 }
5464 })
5465}
5466
5467fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
5468 let mut out = FormFieldAttrs::default();
5469 for attr in &field.attrs {
5470 if !attr.path().is_ident("form") {
5471 continue;
5472 }
5473 attr.parse_nested_meta(|meta| {
5474 if meta.path.is_ident("min") {
5475 let lit: syn::LitInt = meta.value()?.parse()?;
5476 out.min = Some(lit.base10_parse::<i64>()?);
5477 return Ok(());
5478 }
5479 if meta.path.is_ident("max") {
5480 let lit: syn::LitInt = meta.value()?.parse()?;
5481 out.max = Some(lit.base10_parse::<i64>()?);
5482 return Ok(());
5483 }
5484 if meta.path.is_ident("min_length") {
5485 let lit: syn::LitInt = meta.value()?.parse()?;
5486 out.min_length = Some(lit.base10_parse::<u32>()?);
5487 return Ok(());
5488 }
5489 if meta.path.is_ident("max_length") {
5490 let lit: syn::LitInt = meta.value()?.parse()?;
5491 out.max_length = Some(lit.base10_parse::<u32>()?);
5492 return Ok(());
5493 }
5494 Err(meta.error(
5495 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
5496 ))
5497 })?;
5498 }
5499 Ok(out)
5500}
5501
5502fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
5503 let Type::Path(TypePath { path, qself: None }) = ty else {
5504 return Err(syn::Error::new(
5505 span,
5506 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
5507 ));
5508 };
5509 let last = path
5510 .segments
5511 .last()
5512 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
5513
5514 if last.ident == "Option" {
5515 let inner = generic_inner(ty, &last.arguments, "Option")?;
5516 let (kind, nested) = detect_form_field(inner, span)?;
5517 if nested {
5518 return Err(syn::Error::new(
5519 span,
5520 "nested Option in Form fields is not supported",
5521 ));
5522 }
5523 return Ok((kind, true));
5524 }
5525
5526 let kind = match last.ident.to_string().as_str() {
5527 "String" => FormFieldKind::String,
5528 "i16" => FormFieldKind::I16,
5529 "i32" => FormFieldKind::I32,
5530 "i64" => FormFieldKind::I64,
5531 "f32" => FormFieldKind::F32,
5532 "f64" => FormFieldKind::F64,
5533 "bool" => FormFieldKind::Bool,
5534 other => {
5535 return Err(syn::Error::new(
5536 span,
5537 format!(
5538 "Form field type `{other}` is not supported in v0.8 — use String / \
5539 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
5540 ),
5541 ));
5542 }
5543 };
5544 Ok((kind, false))
5545}
5546
5547#[allow(clippy::too_many_lines)]
5548fn render_form_field_parse(
5549 ident: &syn::Ident,
5550 name_lit: &str,
5551 kind: FormFieldKind,
5552 nullable: bool,
5553 attrs: &FormFieldAttrs,
5554) -> TokenStream2 {
5555 let lookup = quote! {
5558 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
5559 };
5560
5561 let parsed_value = match kind {
5562 FormFieldKind::Bool => quote! {
5563 let __v: bool = match __raw {
5564 ::core::option::Option::None => false,
5565 ::core::option::Option::Some(__s) => !matches!(
5566 __s.to_ascii_lowercase().as_str(),
5567 "" | "false" | "0" | "off" | "no"
5568 ),
5569 };
5570 },
5571 FormFieldKind::String => {
5572 if nullable {
5573 quote! {
5574 let __v: ::core::option::Option<::std::string::String> = match __raw {
5575 ::core::option::Option::None => ::core::option::Option::None,
5576 ::core::option::Option::Some(__s) if __s.is_empty() => {
5577 ::core::option::Option::None
5578 }
5579 ::core::option::Option::Some(__s) => {
5580 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
5581 }
5582 };
5583 }
5584 } else {
5585 quote! {
5586 let __v: ::std::string::String = match __raw {
5587 ::core::option::Option::Some(__s) if !__s.is_empty() => {
5588 ::core::clone::Clone::clone(__s)
5589 }
5590 _ => {
5591 __errors.add(#name_lit, "This field is required.");
5592 ::std::string::String::new()
5593 }
5594 };
5595 }
5596 }
5597 }
5598 FormFieldKind::I16
5599 | FormFieldKind::I32
5600 | FormFieldKind::I64
5601 | FormFieldKind::F32
5602 | FormFieldKind::F64 => {
5603 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
5604 let ty_lit = kind.parse_method();
5605 let default_val = match kind {
5606 FormFieldKind::I16 => quote! { 0i16 },
5607 FormFieldKind::I32 => quote! { 0i32 },
5608 FormFieldKind::I64 => quote! { 0i64 },
5609 FormFieldKind::F32 => quote! { 0f32 },
5610 FormFieldKind::F64 => quote! { 0f64 },
5611 _ => quote! { Default::default() },
5612 };
5613 if nullable {
5614 quote! {
5615 let __v: ::core::option::Option<#parse_ty> = match __raw {
5616 ::core::option::Option::None => ::core::option::Option::None,
5617 ::core::option::Option::Some(__s) if __s.is_empty() => {
5618 ::core::option::Option::None
5619 }
5620 ::core::option::Option::Some(__s) => {
5621 match __s.parse::<#parse_ty>() {
5622 ::core::result::Result::Ok(__n) => {
5623 ::core::option::Option::Some(__n)
5624 }
5625 ::core::result::Result::Err(__e) => {
5626 __errors.add(
5627 #name_lit,
5628 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5629 );
5630 ::core::option::Option::None
5631 }
5632 }
5633 }
5634 };
5635 }
5636 } else {
5637 quote! {
5638 let __v: #parse_ty = match __raw {
5639 ::core::option::Option::Some(__s) if !__s.is_empty() => {
5640 match __s.parse::<#parse_ty>() {
5641 ::core::result::Result::Ok(__n) => __n,
5642 ::core::result::Result::Err(__e) => {
5643 __errors.add(
5644 #name_lit,
5645 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5646 );
5647 #default_val
5648 }
5649 }
5650 }
5651 _ => {
5652 __errors.add(#name_lit, "This field is required.");
5653 #default_val
5654 }
5655 };
5656 }
5657 }
5658 }
5659 };
5660
5661 let validators = render_form_validators(name_lit, kind, nullable, attrs);
5662
5663 quote! {
5664 let #ident = {
5665 #lookup
5666 #parsed_value
5667 #validators
5668 __v
5669 };
5670 }
5671}
5672
5673fn render_form_validators(
5674 name_lit: &str,
5675 kind: FormFieldKind,
5676 nullable: bool,
5677 attrs: &FormFieldAttrs,
5678) -> TokenStream2 {
5679 let mut checks: Vec<TokenStream2> = Vec::new();
5680
5681 let val_ref = if nullable {
5682 quote! { __v.as_ref() }
5683 } else {
5684 quote! { ::core::option::Option::Some(&__v) }
5685 };
5686
5687 let is_string = matches!(kind, FormFieldKind::String);
5688 let is_numeric = matches!(
5689 kind,
5690 FormFieldKind::I16
5691 | FormFieldKind::I32
5692 | FormFieldKind::I64
5693 | FormFieldKind::F32
5694 | FormFieldKind::F64
5695 );
5696
5697 if is_string {
5698 if let Some(min_len) = attrs.min_length {
5699 let min_len_usize = min_len as usize;
5700 checks.push(quote! {
5701 if let ::core::option::Option::Some(__s) = #val_ref {
5702 if __s.len() < #min_len_usize {
5703 __errors.add(
5704 #name_lit,
5705 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
5706 );
5707 }
5708 }
5709 });
5710 }
5711 if let Some(max_len) = attrs.max_length {
5712 let max_len_usize = max_len as usize;
5713 checks.push(quote! {
5714 if let ::core::option::Option::Some(__s) = #val_ref {
5715 if __s.len() > #max_len_usize {
5716 __errors.add(
5717 #name_lit,
5718 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
5719 );
5720 }
5721 }
5722 });
5723 }
5724 }
5725
5726 if is_numeric {
5727 if let Some(min) = attrs.min {
5728 checks.push(quote! {
5729 if let ::core::option::Option::Some(__n) = #val_ref {
5730 if (*__n as f64) < (#min as f64) {
5731 __errors.add(
5732 #name_lit,
5733 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
5734 );
5735 }
5736 }
5737 });
5738 }
5739 if let Some(max) = attrs.max {
5740 checks.push(quote! {
5741 if let ::core::option::Option::Some(__n) = #val_ref {
5742 if (*__n as f64) > (#max as f64) {
5743 __errors.add(
5744 #name_lit,
5745 ::std::format!("Ensure this value is less than or equal to {}.", #max),
5746 );
5747 }
5748 }
5749 });
5750 }
5751 }
5752
5753 quote! { #( #checks )* }
5754}
5755
5756struct ViewSetAttrs {
5761 model: syn::Path,
5762 fields: Option<Vec<String>>,
5763 filter_fields: Vec<String>,
5764 search_fields: Vec<String>,
5765 ordering: Vec<(String, bool)>,
5767 page_size: Option<usize>,
5768 read_only: bool,
5769 perms: ViewSetPermsAttrs,
5770}
5771
5772#[derive(Default)]
5773struct ViewSetPermsAttrs {
5774 list: Vec<String>,
5775 retrieve: Vec<String>,
5776 create: Vec<String>,
5777 update: Vec<String>,
5778 destroy: Vec<String>,
5779}
5780
5781fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
5782 let struct_name = &input.ident;
5783
5784 match &input.data {
5786 Data::Struct(s) => match &s.fields {
5787 Fields::Unit | Fields::Named(_) => {}
5788 Fields::Unnamed(_) => {
5789 return Err(syn::Error::new_spanned(
5790 struct_name,
5791 "ViewSet can only be derived on a unit struct or an empty named struct",
5792 ));
5793 }
5794 },
5795 _ => {
5796 return Err(syn::Error::new_spanned(
5797 struct_name,
5798 "ViewSet can only be derived on a struct",
5799 ));
5800 }
5801 }
5802
5803 let attrs = parse_viewset_attrs(input)?;
5804 let model_path = &attrs.model;
5805
5806 let fields_call = if let Some(ref fields) = attrs.fields {
5808 let lits = fields.iter().map(|f| f.as_str());
5809 quote!(.fields(&[ #(#lits),* ]))
5810 } else {
5811 quote!()
5812 };
5813
5814 let filter_fields_call = if attrs.filter_fields.is_empty() {
5815 quote!()
5816 } else {
5817 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
5818 quote!(.filter_fields(&[ #(#lits),* ]))
5819 };
5820
5821 let search_fields_call = if attrs.search_fields.is_empty() {
5822 quote!()
5823 } else {
5824 let lits = attrs.search_fields.iter().map(|f| f.as_str());
5825 quote!(.search_fields(&[ #(#lits),* ]))
5826 };
5827
5828 let ordering_call = if attrs.ordering.is_empty() {
5829 quote!()
5830 } else {
5831 let pairs = attrs.ordering.iter().map(|(f, desc)| {
5832 let f = f.as_str();
5833 quote!((#f, #desc))
5834 });
5835 quote!(.ordering(&[ #(#pairs),* ]))
5836 };
5837
5838 let page_size_call = if let Some(n) = attrs.page_size {
5839 quote!(.page_size(#n))
5840 } else {
5841 quote!()
5842 };
5843
5844 let read_only_call = if attrs.read_only {
5845 quote!(.read_only())
5846 } else {
5847 quote!()
5848 };
5849
5850 let perms = &attrs.perms;
5851 let perms_call = if perms.list.is_empty()
5852 && perms.retrieve.is_empty()
5853 && perms.create.is_empty()
5854 && perms.update.is_empty()
5855 && perms.destroy.is_empty()
5856 {
5857 quote!()
5858 } else {
5859 let list_lits = perms.list.iter().map(|s| s.as_str());
5860 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
5861 let create_lits = perms.create.iter().map(|s| s.as_str());
5862 let update_lits = perms.update.iter().map(|s| s.as_str());
5863 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
5864 quote! {
5865 .permissions(::rustango::viewset::ViewSetPerms {
5866 list: ::std::vec![ #(#list_lits.to_owned()),* ],
5867 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
5868 create: ::std::vec![ #(#create_lits.to_owned()),* ],
5869 update: ::std::vec![ #(#update_lits.to_owned()),* ],
5870 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
5871 })
5872 }
5873 };
5874
5875 Ok(quote! {
5876 impl #struct_name {
5877 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
5880 ::rustango::viewset::ViewSet::for_model(
5881 <#model_path as ::rustango::core::Model>::SCHEMA
5882 )
5883 #fields_call
5884 #filter_fields_call
5885 #search_fields_call
5886 #ordering_call
5887 #page_size_call
5888 #perms_call
5889 #read_only_call
5890 .router(prefix, pool)
5891 }
5892 }
5893 })
5894}
5895
5896fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
5897 let mut model: Option<syn::Path> = None;
5898 let mut fields: Option<Vec<String>> = None;
5899 let mut filter_fields: Vec<String> = Vec::new();
5900 let mut search_fields: Vec<String> = Vec::new();
5901 let mut ordering: Vec<(String, bool)> = Vec::new();
5902 let mut page_size: Option<usize> = None;
5903 let mut read_only = false;
5904 let mut perms = ViewSetPermsAttrs::default();
5905
5906 for attr in &input.attrs {
5907 if !attr.path().is_ident("viewset") {
5908 continue;
5909 }
5910 attr.parse_nested_meta(|meta| {
5911 if meta.path.is_ident("model") {
5912 let path: syn::Path = meta.value()?.parse()?;
5913 model = Some(path);
5914 return Ok(());
5915 }
5916 if meta.path.is_ident("fields") {
5917 let s: LitStr = meta.value()?.parse()?;
5918 fields = Some(split_field_list(&s.value()));
5919 return Ok(());
5920 }
5921 if meta.path.is_ident("filter_fields") {
5922 let s: LitStr = meta.value()?.parse()?;
5923 filter_fields = split_field_list(&s.value());
5924 return Ok(());
5925 }
5926 if meta.path.is_ident("search_fields") {
5927 let s: LitStr = meta.value()?.parse()?;
5928 search_fields = split_field_list(&s.value());
5929 return Ok(());
5930 }
5931 if meta.path.is_ident("ordering") {
5932 let s: LitStr = meta.value()?.parse()?;
5933 ordering = parse_ordering_list(&s.value());
5934 return Ok(());
5935 }
5936 if meta.path.is_ident("page_size") {
5937 let lit: syn::LitInt = meta.value()?.parse()?;
5938 page_size = Some(lit.base10_parse::<usize>()?);
5939 return Ok(());
5940 }
5941 if meta.path.is_ident("read_only") {
5942 read_only = true;
5943 return Ok(());
5944 }
5945 if meta.path.is_ident("permissions") {
5946 meta.parse_nested_meta(|inner| {
5947 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5948 let s: LitStr = inner.value()?.parse()?;
5949 Ok(split_field_list(&s.value()))
5950 };
5951 if inner.path.is_ident("list") {
5952 perms.list = parse_codenames(&inner)?;
5953 } else if inner.path.is_ident("retrieve") {
5954 perms.retrieve = parse_codenames(&inner)?;
5955 } else if inner.path.is_ident("create") {
5956 perms.create = parse_codenames(&inner)?;
5957 } else if inner.path.is_ident("update") {
5958 perms.update = parse_codenames(&inner)?;
5959 } else if inner.path.is_ident("destroy") {
5960 perms.destroy = parse_codenames(&inner)?;
5961 } else {
5962 return Err(inner.error(
5963 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5964 ));
5965 }
5966 Ok(())
5967 })?;
5968 return Ok(());
5969 }
5970 Err(meta.error(
5971 "unknown viewset attribute (supported: model, fields, filter_fields, \
5972 search_fields, ordering, page_size, read_only, permissions(...))",
5973 ))
5974 })?;
5975 }
5976
5977 let model = model.ok_or_else(|| {
5978 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
5979 })?;
5980
5981 Ok(ViewSetAttrs {
5982 model,
5983 fields,
5984 filter_fields,
5985 search_fields,
5986 ordering,
5987 page_size,
5988 read_only,
5989 perms,
5990 })
5991}
5992
5993struct SerializerContainerAttrs {
5996 model: syn::Path,
5997}
5998
5999#[derive(Default)]
6000struct SerializerFieldAttrs {
6001 read_only: bool,
6002 write_only: bool,
6003 source: Option<String>,
6004 skip: bool,
6005 method: Option<String>,
6009 validate: Option<String>,
6014 nested: bool,
6024 nested_strict: bool,
6029 many: Option<syn::Type>,
6038 slug: Option<String>,
6048}
6049
6050fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
6051 let mut model: Option<syn::Path> = None;
6052 for attr in &input.attrs {
6053 if !attr.path().is_ident("serializer") {
6054 continue;
6055 }
6056 attr.parse_nested_meta(|meta| {
6057 if meta.path.is_ident("model") {
6058 let _eq: syn::Token![=] = meta.input.parse()?;
6059 model = Some(meta.input.parse()?);
6060 return Ok(());
6061 }
6062 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
6063 })?;
6064 }
6065 let model = model.ok_or_else(|| {
6066 syn::Error::new_spanned(
6067 &input.ident,
6068 "`#[serializer(model = SomeModel)]` is required",
6069 )
6070 })?;
6071 Ok(SerializerContainerAttrs { model })
6072}
6073
6074fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
6075 let mut out = SerializerFieldAttrs::default();
6076 for attr in &field.attrs {
6077 if !attr.path().is_ident("serializer") {
6078 continue;
6079 }
6080 attr.parse_nested_meta(|meta| {
6081 if meta.path.is_ident("read_only") {
6082 out.read_only = true;
6083 return Ok(());
6084 }
6085 if meta.path.is_ident("write_only") {
6086 out.write_only = true;
6087 return Ok(());
6088 }
6089 if meta.path.is_ident("skip") {
6090 out.skip = true;
6091 return Ok(());
6092 }
6093 if meta.path.is_ident("source") {
6094 let s: LitStr = meta.value()?.parse()?;
6095 out.source = Some(s.value());
6096 return Ok(());
6097 }
6098 if meta.path.is_ident("method") {
6099 let s: LitStr = meta.value()?.parse()?;
6100 out.method = Some(s.value());
6101 return Ok(());
6102 }
6103 if meta.path.is_ident("validate") {
6104 let s: LitStr = meta.value()?.parse()?;
6105 out.validate = Some(s.value());
6106 return Ok(());
6107 }
6108 if meta.path.is_ident("many") {
6109 let _eq: syn::Token![=] = meta.input.parse()?;
6110 out.many = Some(meta.input.parse()?);
6111 return Ok(());
6112 }
6113 if meta.path.is_ident("nested") {
6114 out.nested = true;
6115 if meta.input.peek(syn::token::Paren) {
6118 meta.parse_nested_meta(|inner| {
6119 if inner.path.is_ident("strict") {
6120 out.nested_strict = true;
6121 return Ok(());
6122 }
6123 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
6124 })?;
6125 }
6126 return Ok(());
6127 }
6128 if meta.path.is_ident("slug") {
6129 let s: LitStr = meta.value()?.parse()?;
6130 out.slug = Some(s.value());
6131 return Ok(());
6132 }
6133 Err(meta.error(
6134 "unknown serializer field attribute (supported: \
6135 `read_only`, `write_only`, `source`, `skip`, `method`, \
6136 `validate`, `nested`, `many`, `slug`)",
6137 ))
6138 })?;
6139 }
6140 if out.read_only && out.write_only {
6142 return Err(syn::Error::new_spanned(
6143 field,
6144 "a field cannot be both `read_only` and `write_only`",
6145 ));
6146 }
6147 if out.method.is_some() && out.source.is_some() {
6148 return Err(syn::Error::new_spanned(
6149 field,
6150 "`method` and `source` are mutually exclusive — `method` computes \
6151 the value from a method, `source` reads it from a different model field",
6152 ));
6153 }
6154 if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
6155 return Err(syn::Error::new_spanned(
6156 field,
6157 "`slug` is mutually exclusive with `method`, `nested`, and `many` \
6158 — pick one strategy for populating the field",
6159 ));
6160 }
6161 Ok(out)
6162}
6163
6164fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
6165 let struct_name = &input.ident;
6166 let struct_name_lit = struct_name.to_string();
6167
6168 let Data::Struct(data) = &input.data else {
6169 return Err(syn::Error::new_spanned(
6170 struct_name,
6171 "Serializer can only be derived on structs",
6172 ));
6173 };
6174 let Fields::Named(named) = &data.fields else {
6175 return Err(syn::Error::new_spanned(
6176 struct_name,
6177 "Serializer requires a struct with named fields",
6178 ));
6179 };
6180
6181 let container = parse_serializer_container_attrs(input)?;
6182 let model_path = &container.model;
6183
6184 #[allow(dead_code)]
6188 struct FieldInfo {
6189 ident: syn::Ident,
6190 ty: syn::Type,
6191 attrs: SerializerFieldAttrs,
6192 }
6193 let mut fields_info: Vec<FieldInfo> = Vec::new();
6194 for field in &named.named {
6195 let ident = field.ident.clone().expect("named field has ident");
6196 let attrs = parse_serializer_field_attrs(field)?;
6197 fields_info.push(FieldInfo {
6198 ident,
6199 ty: field.ty.clone(),
6200 attrs,
6201 });
6202 }
6203
6204 let from_model_fields = fields_info.iter().map(|fi| {
6206 let ident = &fi.ident;
6207 let ty = &fi.ty;
6208 if let Some(_inner) = &fi.attrs.many {
6209 quote! { #ident: ::std::vec::Vec::new() }
6213 } else if let Some(method) = &fi.attrs.method {
6214 let method_ident = syn::Ident::new(method, ident.span());
6218 quote! { #ident: Self::#method_ident(model) }
6219 } else if let Some(slug_field) = &fi.attrs.slug {
6220 let src_name = fi
6228 .attrs
6229 .source
6230 .as_deref()
6231 .unwrap_or(&fi.ident.to_string())
6232 .to_owned();
6233 let src_ident = syn::Ident::new(&src_name, ident.span());
6234 let slug_ident = syn::Ident::new(slug_field, ident.span());
6235 quote! {
6236 #ident: match model.#src_ident.value() {
6237 ::core::option::Option::Some(__loaded) =>
6238 ::core::clone::Clone::clone(&__loaded.#slug_ident),
6239 ::core::option::Option::None =>
6240 ::core::default::Default::default(),
6241 }
6242 }
6243 } else if fi.attrs.nested {
6244 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
6260 let src_ident = syn::Ident::new(&src_name, ident.span());
6261 if fi.attrs.nested_strict {
6262 let panic_msg = format!(
6263 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
6264 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
6265 );
6266 quote! {
6267 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
6268 model.#src_ident.value().expect(#panic_msg),
6269 )
6270 }
6271 } else {
6272 quote! {
6273 #ident: match model.#src_ident.value() {
6274 ::core::option::Option::Some(__loaded) =>
6275 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
6276 ::core::option::Option::None =>
6277 ::core::default::Default::default(),
6278 }
6279 }
6280 }
6281 } else if fi.attrs.write_only || fi.attrs.skip {
6282 quote! { #ident: ::core::default::Default::default() }
6284 } else if let Some(src) = &fi.attrs.source {
6285 let src_ident = syn::Ident::new(src, ident.span());
6286 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
6287 } else {
6288 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
6289 }
6290 });
6291
6292 let validator_calls: Vec<_> = fields_info
6296 .iter()
6297 .filter_map(|fi| {
6298 let ident = &fi.ident;
6299 let name_lit = ident.to_string();
6300 let method = fi.attrs.validate.as_ref()?;
6301 let method_ident = syn::Ident::new(method, ident.span());
6302 Some(quote! {
6303 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
6304 __errors.add(#name_lit.to_owned(), __e);
6305 }
6306 })
6307 })
6308 .collect();
6309 let validate_method = if validator_calls.is_empty() {
6310 quote! {}
6311 } else {
6312 quote! {
6313 impl #struct_name {
6314 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
6318 let mut __errors = ::rustango::forms::FormErrors::default();
6319 #( #validator_calls )*
6320 if __errors.is_empty() {
6321 ::core::result::Result::Ok(())
6322 } else {
6323 ::core::result::Result::Err(__errors)
6324 }
6325 }
6326 }
6327 }
6328 };
6329
6330 let many_setters: Vec<_> = fields_info
6334 .iter()
6335 .filter_map(|fi| {
6336 let many_ty = fi.attrs.many.as_ref()?;
6337 let ident = &fi.ident;
6338 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
6339 Some(quote! {
6340 pub fn #setter(
6345 &mut self,
6346 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
6347 ) -> &mut Self {
6348 self.#ident = models.iter()
6349 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
6350 .collect();
6351 self
6352 }
6353 })
6354 })
6355 .collect();
6356 let many_setters_impl = if many_setters.is_empty() {
6357 quote! {}
6358 } else {
6359 quote! {
6360 impl #struct_name {
6361 #( #many_setters )*
6362 }
6363 }
6364 };
6365
6366 let output_fields: Vec<_> = fields_info
6368 .iter()
6369 .filter(|fi| !fi.attrs.write_only)
6370 .collect();
6371 let output_field_count = output_fields.len();
6372 let serialize_fields = output_fields.iter().map(|fi| {
6373 let ident = &fi.ident;
6374 let name_lit = ident.to_string();
6375 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
6376 });
6377
6378 let writable_lits: Vec<_> = fields_info
6392 .iter()
6393 .filter(|fi| {
6394 !fi.attrs.read_only
6395 && !fi.attrs.skip
6396 && fi.attrs.method.is_none()
6397 && !fi.attrs.nested
6398 && fi.attrs.many.is_none()
6399 && fi.attrs.slug.is_none()
6400 })
6401 .map(|fi| fi.ident.to_string())
6402 .collect();
6403
6404 let openapi_impl = {
6408 #[cfg(feature = "openapi")]
6409 {
6410 let property_calls = output_fields.iter().map(|fi| {
6411 let ident = &fi.ident;
6412 let name_lit = ident.to_string();
6413 let ty = &fi.ty;
6414 let nullable_call = if is_option(ty) {
6415 quote! { .nullable() }
6416 } else {
6417 quote! {}
6418 };
6419 quote! {
6420 .property(
6421 #name_lit,
6422 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
6423 #nullable_call,
6424 )
6425 }
6426 });
6427 let required_lits: Vec<_> = output_fields
6428 .iter()
6429 .filter(|fi| !is_option(&fi.ty))
6430 .map(|fi| fi.ident.to_string())
6431 .collect();
6432 quote! {
6433 impl ::rustango::openapi::OpenApiSchema for #struct_name {
6434 fn openapi_schema() -> ::rustango::openapi::Schema {
6435 ::rustango::openapi::Schema::object()
6436 #( #property_calls )*
6437 .required([ #( #required_lits ),* ])
6438 }
6439 }
6440 }
6441 }
6442 #[cfg(not(feature = "openapi"))]
6443 {
6444 quote! {}
6445 }
6446 };
6447
6448 Ok(quote! {
6449 impl ::rustango::serializer::ModelSerializer for #struct_name {
6450 type Model = #model_path;
6451
6452 fn from_model(model: &Self::Model) -> Self {
6453 Self {
6454 #( #from_model_fields ),*
6455 }
6456 }
6457
6458 fn writable_fields() -> &'static [&'static str] {
6459 &[ #( #writable_lits ),* ]
6460 }
6461 }
6462
6463 impl ::serde::Serialize for #struct_name {
6464 fn serialize<S>(&self, serializer: S)
6465 -> ::core::result::Result<S::Ok, S::Error>
6466 where
6467 S: ::serde::Serializer,
6468 {
6469 use ::serde::ser::SerializeStruct;
6470 let mut __state = serializer.serialize_struct(
6471 #struct_name_lit,
6472 #output_field_count,
6473 )?;
6474 #( #serialize_fields )*
6475 __state.end()
6476 }
6477 }
6478
6479 #openapi_impl
6480
6481 #validate_method
6482
6483 #many_setters_impl
6484 })
6485}
6486
6487#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
6491fn is_option(ty: &syn::Type) -> bool {
6492 if let syn::Type::Path(p) = ty {
6493 if let Some(last) = p.path.segments.last() {
6494 return last.ident == "Option";
6495 }
6496 }
6497 false
6498}