1use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13 parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14 PathArguments, Type, TypePath,
15};
16
17#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20 let input = parse_macro_input!(input as DeriveInput);
21 expand(&input)
22 .unwrap_or_else(syn::Error::into_compile_error)
23 .into()
24}
25
26#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60 let input = parse_macro_input!(input as DeriveInput);
61 expand_viewset(&input)
62 .unwrap_or_else(syn::Error::into_compile_error)
63 .into()
64}
65
66#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95 let input = parse_macro_input!(input as DeriveInput);
96 expand_form(&input)
97 .unwrap_or_else(syn::Error::into_compile_error)
98 .into()
99}
100
101#[proc_macro_derive(Serializer, attributes(serializer))]
116pub fn derive_serializer(input: TokenStream) -> TokenStream {
117 let input = parse_macro_input!(input as DeriveInput);
118 expand_serializer(&input)
119 .unwrap_or_else(syn::Error::into_compile_error)
120 .into()
121}
122
123#[proc_macro]
158pub fn embed_migrations(input: TokenStream) -> TokenStream {
159 expand_embed_migrations(input.into())
160 .unwrap_or_else(syn::Error::into_compile_error)
161 .into()
162}
163
164#[proc_macro_attribute]
187pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
188 expand_main(args.into(), item.into())
189 .unwrap_or_else(syn::Error::into_compile_error)
190 .into()
191}
192
193fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
194 let mut input: syn::ItemFn = syn::parse2(item)?;
195 if input.sig.asyncness.is_none() {
196 return Err(syn::Error::new(
197 input.sig.ident.span(),
198 "`#[rustango::main]` must wrap an `async fn`",
199 ));
200 }
201
202 let flavor = parse_flavor(&args);
214 let builder_call = match flavor {
215 Flavor::CurrentThread => quote! {
216 ::rustango::__private_runtime::tokio::runtime::Builder::new_current_thread()
217 },
218 Flavor::MultiThread => quote! {
219 ::rustango::__private_runtime::tokio::runtime::Builder::new_multi_thread()
220 },
221 };
222
223 let user_body = input.block.clone();
226 input.sig.asyncness = None;
227 input.block = syn::parse2(quote! {{
228 {
229 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
230 let _ = tracing_subscriber::fmt()
233 .with_env_filter(
234 EnvFilter::try_from_default_env()
235 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
236 )
237 .try_init();
238 }
239 let __rt = #builder_call
240 .enable_all()
241 .build()
242 .expect("failed to build tokio runtime");
243 __rt.block_on(async move #user_body)
244 }})?;
245
246 Ok(quote! {
247 #input
248 })
249}
250
251enum Flavor {
252 MultiThread,
253 CurrentThread,
254}
255
256fn parse_flavor(args: &TokenStream2) -> Flavor {
257 let s = args.to_string();
261 if s.contains("current_thread") {
262 Flavor::CurrentThread
263 } else {
264 Flavor::MultiThread
265 }
266}
267
268fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
269 let path_str = if input.is_empty() {
271 "./migrations".to_string()
272 } else {
273 let lit: LitStr = syn::parse2(input)?;
274 lit.value()
275 };
276
277 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
278 syn::Error::new(
279 proc_macro2::Span::call_site(),
280 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
281 )
282 })?;
283 let abs = std::path::Path::new(&manifest).join(&path_str);
284
285 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
286 if abs.is_dir() {
287 let read = std::fs::read_dir(&abs).map_err(|e| {
288 syn::Error::new(
289 proc_macro2::Span::call_site(),
290 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
291 )
292 })?;
293 for entry in read.flatten() {
294 let path = entry.path();
295 if !path.is_file() {
296 continue;
297 }
298 if path.extension().and_then(|s| s.to_str()) != Some("json") {
299 continue;
300 }
301 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
302 continue;
303 };
304 entries.push((stem.to_owned(), path));
305 }
306 }
307 entries.sort_by(|a, b| a.0.cmp(&b.0));
308
309 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
322 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
323 for (stem, path) in &entries {
324 let raw = std::fs::read_to_string(path).map_err(|e| {
325 syn::Error::new(
326 proc_macro2::Span::call_site(),
327 format!(
328 "embed_migrations!: cannot read {} for chain validation: {e}",
329 path.display()
330 ),
331 )
332 })?;
333 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
334 syn::Error::new(
335 proc_macro2::Span::call_site(),
336 format!(
337 "embed_migrations!: {} is not valid JSON: {e}",
338 path.display()
339 ),
340 )
341 })?;
342 let name = json
343 .get("name")
344 .and_then(|v| v.as_str())
345 .ok_or_else(|| {
346 syn::Error::new(
347 proc_macro2::Span::call_site(),
348 format!(
349 "embed_migrations!: {} is missing the `name` field",
350 path.display()
351 ),
352 )
353 })?
354 .to_owned();
355 if name != *stem {
356 return Err(syn::Error::new(
357 proc_macro2::Span::call_site(),
358 format!(
359 "embed_migrations!: file stem `{stem}` does not match the migration's \
360 `name` field `{name}` — rename the file or fix the JSON",
361 ),
362 ));
363 }
364 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
365 chain_names.push(name.clone());
366 prev_refs.push((name, prev));
367 }
368
369 let name_set: std::collections::HashSet<&str> =
370 chain_names.iter().map(String::as_str).collect();
371 for (name, prev) in &prev_refs {
372 if let Some(p) = prev {
373 if !name_set.contains(p.as_str()) {
374 return Err(syn::Error::new(
375 proc_macro2::Span::call_site(),
376 format!(
377 "embed_migrations!: broken migration chain — `{name}` declares \
378 prev=`{p}` but no migration with that name exists in {}",
379 abs.display()
380 ),
381 ));
382 }
383 }
384 }
385
386 let pairs: Vec<TokenStream2> = entries
387 .iter()
388 .map(|(name, path)| {
389 let path_lit = path.display().to_string();
390 quote! { (#name, ::core::include_str!(#path_lit)) }
391 })
392 .collect();
393
394 Ok(quote! {
395 {
396 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
397 __RUSTANGO_EMBEDDED
398 }
399 })
400}
401
402fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
403 let struct_name = &input.ident;
404
405 let Data::Struct(data) = &input.data else {
406 return Err(syn::Error::new_spanned(
407 struct_name,
408 "Model can only be derived on structs",
409 ));
410 };
411 let Fields::Named(named) = &data.fields else {
412 return Err(syn::Error::new_spanned(
413 struct_name,
414 "Model requires a struct with named fields",
415 ));
416 };
417
418 let container = parse_container_attrs(input)?;
419 let table = container
420 .table
421 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
422 let model_name = struct_name.to_string();
423
424 let collected = collect_fields(named, &table)?;
425
426 if let Some((ref display, span)) = container.display {
428 if !collected.field_names.iter().any(|n| n == display) {
429 return Err(syn::Error::new(
430 span,
431 format!("`display = \"{display}\"` does not match any field on this struct"),
432 ));
433 }
434 }
435 let display = container.display.map(|(name, _)| name);
436 let app_label = container.app.clone();
437
438 if let Some(admin) = &container.admin {
440 for (label, list) in [
441 ("list_display", &admin.list_display),
442 ("search_fields", &admin.search_fields),
443 ("readonly_fields", &admin.readonly_fields),
444 ("list_filter", &admin.list_filter),
445 ] {
446 if let Some((names, span)) = list {
447 for name in names {
448 if !collected.field_names.iter().any(|n| n == name) {
449 return Err(syn::Error::new(
450 *span,
451 format!(
452 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
453 ),
454 ));
455 }
456 }
457 }
458 }
459 if let Some((pairs, span)) = &admin.ordering {
460 for (name, _) in pairs {
461 if !collected.field_names.iter().any(|n| n == name) {
462 return Err(syn::Error::new(
463 *span,
464 format!(
465 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
466 ),
467 ));
468 }
469 }
470 }
471 if let Some((groups, span)) = &admin.fieldsets {
472 for (_, fields) in groups {
473 for name in fields {
474 if !collected.field_names.iter().any(|n| n == name) {
475 return Err(syn::Error::new(
476 *span,
477 format!(
478 "`fieldsets`: \"{name}\" is not a declared field on this struct"
479 ),
480 ));
481 }
482 }
483 }
484 }
485 }
486 if let Some(audit) = &container.audit {
487 if let Some((names, span)) = &audit.track {
488 for name in names {
489 if !collected.field_names.iter().any(|n| n == name) {
490 return Err(syn::Error::new(
491 *span,
492 format!(
493 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
494 ),
495 ));
496 }
497 }
498 }
499 }
500
501 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
504 audit
505 .track
506 .as_ref()
507 .map(|(names, _)| names.clone())
508 .unwrap_or_default()
509 });
510
511 let mut all_indexes: Vec<IndexAttr> = container.indexes;
513 for field in &named.named {
514 let ident = field.ident.as_ref().expect("named");
515 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
518 if fa.index {
519 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
520 let auto_name = if fa.index_unique {
521 format!("{table}_{col_name}_uq_idx")
522 } else {
523 format!("{table}_{col_name}_idx")
524 };
525 all_indexes.push(IndexAttr {
526 name: fa.index_name.or(Some(auto_name)),
527 columns: vec![col_name],
528 unique: fa.index_unique,
529 });
530 }
531 }
532 }
533
534 let model_impl = model_impl_tokens(
535 struct_name,
536 &model_name,
537 &table,
538 display.as_deref(),
539 app_label.as_deref(),
540 container.admin.as_ref(),
541 &collected.field_schemas,
542 collected.soft_delete_column.as_deref(),
543 container.permissions,
544 audit_track_names.as_deref(),
545 &container.m2m,
546 &all_indexes,
547 &container.checks,
548 &container.composite_fks,
549 &container.generic_fks,
550 container.scope.as_deref(),
551 );
552 let module_ident = column_module_ident(struct_name);
553 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
554 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
555 let track_set: Option<std::collections::HashSet<&str>> = audit
556 .track
557 .as_ref()
558 .map(|(names, _)| names.iter().map(String::as_str).collect());
559 collected
560 .column_entries
561 .iter()
562 .filter(|c| {
563 track_set
564 .as_ref()
565 .map_or(true, |s| s.contains(c.name.as_str()))
566 })
567 .collect()
568 });
569 let inherent_impl = inherent_impl_tokens(
570 struct_name,
571 &collected,
572 collected.primary_key.as_ref(),
573 &column_consts,
574 audited_fields.as_deref(),
575 &all_indexes,
576 );
577 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
578 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
579 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
580 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
581
582 Ok(quote! {
583 #model_impl
584 #inherent_impl
585 #from_row_impl
586 #column_module
587 #reverse_helpers
588 #m2m_accessors
589
590 ::rustango::core::inventory::submit! {
591 ::rustango::core::ModelEntry {
592 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
593 module_path: ::core::module_path!(),
598 }
599 }
600 })
601}
602
603fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
614 let arms = fk_relations.iter().map(|rel| {
615 let parent_ty = &rel.parent_type;
616 let fk_col = rel.fk_column.as_str();
617 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
620 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
621 let assign = if rel.nullable {
622 quote! {
623 self.#field_ident = ::core::option::Option::Some(
624 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
625 );
626 }
627 } else {
628 quote! {
629 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
630 }
631 };
632 quote! {
633 #fk_col => {
634 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
635 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
642 ::rustango::core::SqlValue::#variant_ident(v) => v,
643 _other => {
644 ::core::debug_assert!(
645 false,
646 "rustango macro bug: load_related on FK `{}` expected \
647 SqlValue::{} from parent's __rustango_pk_value but got \
648 {:?} — file a bug at https://github.com/ujeenet/rustango",
649 #fk_col,
650 ::core::stringify!(#variant_ident),
651 _other,
652 );
653 #default_expr
654 }
655 };
656 #assign
657 ::core::result::Result::Ok(true)
658 }
659 }
660 });
661 quote! {
662 impl ::rustango::sql::LoadRelated for #struct_name {
663 #[allow(unused_variables)]
664 fn __rustango_load_related(
665 &mut self,
666 row: &::rustango::sql::sqlx::postgres::PgRow,
667 field_name: &str,
668 alias: &str,
669 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
670 match field_name {
671 #( #arms )*
672 _ => ::core::result::Result::Ok(false),
673 }
674 }
675 }
676 }
677}
678
679fn load_related_impl_my_tokens(
687 struct_name: &syn::Ident,
688 fk_relations: &[FkRelation],
689) -> TokenStream2 {
690 let arms = fk_relations.iter().map(|rel| {
691 let parent_ty = &rel.parent_type;
692 let fk_col = rel.fk_column.as_str();
693 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
694 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
695 let assign = if rel.nullable {
696 quote! {
697 __self.#field_ident = ::core::option::Option::Some(
698 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
699 );
700 }
701 } else {
702 quote! {
703 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
704 }
705 };
706 quote! {
711 #fk_col => {
712 let _parent: #parent_ty =
713 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
714 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
717 ::rustango::core::SqlValue::#variant_ident(v) => v,
718 _other => {
719 ::core::debug_assert!(
720 false,
721 "rustango macro bug: load_related on FK `{}` expected \
722 SqlValue::{} from parent's __rustango_pk_value but got \
723 {:?} — file a bug at https://github.com/ujeenet/rustango",
724 #fk_col,
725 ::core::stringify!(#variant_ident),
726 _other,
727 );
728 #default_expr
729 }
730 };
731 #assign
732 ::core::result::Result::Ok(true)
733 }
734 }
735 });
736 quote! {
737 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
738 #( #arms )*
739 });
740 }
741}
742
743fn load_related_impl_sqlite_tokens(
747 struct_name: &syn::Ident,
748 fk_relations: &[FkRelation],
749) -> TokenStream2 {
750 let arms = fk_relations.iter().map(|rel| {
751 let parent_ty = &rel.parent_type;
752 let fk_col = rel.fk_column.as_str();
753 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
754 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
755 let assign = if rel.nullable {
756 quote! {
757 __self.#field_ident = ::core::option::Option::Some(
758 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
759 );
760 }
761 } else {
762 quote! {
763 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
764 }
765 };
766 quote! {
767 #fk_col => {
768 let _parent: #parent_ty =
769 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
770 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
771 ::rustango::core::SqlValue::#variant_ident(v) => v,
772 _other => {
773 ::core::debug_assert!(
774 false,
775 "rustango macro bug: load_related on FK `{}` expected \
776 SqlValue::{} from parent's __rustango_pk_value but got \
777 {:?} — file a bug at https://github.com/ujeenet/rustango",
778 #fk_col,
779 ::core::stringify!(#variant_ident),
780 _other,
781 );
782 #default_expr
783 }
784 };
785 #assign
786 ::core::result::Result::Ok(true)
787 }
788 }
789 });
790 quote! {
791 ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
792 #( #arms )*
793 });
794 }
795}
796
797fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
805 let arms = fk_relations.iter().map(|rel| {
806 let fk_col = rel.fk_column.as_str();
807 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
808 if rel.pk_kind == DetectedKind::I64 {
809 if rel.nullable {
815 quote! {
816 #fk_col => self.#field_ident
817 .as_ref()
818 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
819 }
820 } else {
821 quote! {
822 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
823 }
824 }
825 } else {
826 quote! {
834 #fk_col => ::core::option::Option::None,
835 }
836 }
837 });
838 let value_arms = fk_relations.iter().map(|rel| {
844 let fk_col = rel.fk_column.as_str();
845 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
846 if rel.nullable {
847 quote! {
848 #fk_col => self.#field_ident
849 .as_ref()
850 .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
851 ::rustango::sql::ForeignKey::pk(fk)
852 )),
853 }
854 } else {
855 quote! {
856 #fk_col => ::core::option::Option::Some(
857 ::core::convert::Into::<::rustango::core::SqlValue>::into(
858 self.#field_ident.pk()
859 )
860 ),
861 }
862 }
863 });
864 quote! {
865 impl ::rustango::sql::FkPkAccess for #struct_name {
866 #[allow(unused_variables)]
867 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
868 match field_name {
869 #( #arms )*
870 _ => ::core::option::Option::None,
871 }
872 }
873 #[allow(unused_variables)]
874 fn __rustango_fk_pk_value(
875 &self,
876 field_name: &str,
877 ) -> ::core::option::Option<::rustango::core::SqlValue> {
878 match field_name {
879 #( #value_arms )*
880 _ => ::core::option::Option::None,
881 }
882 }
883 }
884 }
885}
886
887fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
893 if fk_relations.is_empty() {
894 return TokenStream2::new();
895 }
896 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
900 let method_ident = syn::Ident::new(&suffix, child_ident.span());
901 let impls = fk_relations.iter().map(|rel| {
902 let parent_ty = &rel.parent_type;
903 let fk_col = rel.fk_column.as_str();
904 let doc = format!(
905 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
906 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
907 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
908 further `{child_ident}::objects()` filters via direct queryset use."
909 );
910 quote! {
911 impl #parent_ty {
912 #[doc = #doc]
913 pub async fn #method_ident<'_c, _E>(
918 &self,
919 _executor: _E,
920 ) -> ::core::result::Result<
921 ::std::vec::Vec<#child_ident>,
922 ::rustango::sql::ExecError,
923 >
924 where
925 _E: ::rustango::sql::sqlx::Executor<
926 '_c,
927 Database = ::rustango::sql::sqlx::Postgres,
928 >,
929 {
930 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
931 ::rustango::query::QuerySet::<#child_ident>::new()
932 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
933 .fetch_on(_executor)
934 .await
935 }
936 }
937 }
938 });
939 quote! { #( #impls )* }
940}
941
942fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
945 if m2m_relations.is_empty() {
946 return TokenStream2::new();
947 }
948 let methods = m2m_relations.iter().map(|rel| {
949 let method_name = format!("{}_m2m", rel.name);
950 let method_ident = syn::Ident::new(&method_name, struct_name.span());
951 let through = rel.through.as_str();
952 let src_col = rel.src.as_str();
953 let dst_col = rel.dst.as_str();
954 quote! {
955 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
956 ::rustango::sql::M2MManager {
957 src_pk: self.__rustango_pk_value(),
958 through: #through,
959 src_col: #src_col,
960 dst_col: #dst_col,
961 }
962 }
963 }
964 });
965 quote! {
966 impl #struct_name {
967 #( #methods )*
968 }
969 }
970}
971
972struct ColumnEntry {
973 ident: syn::Ident,
976 value_ty: Type,
978 name: String,
980 column: String,
982 field_type_tokens: TokenStream2,
984}
985
986struct CollectedFields {
987 field_schemas: Vec<TokenStream2>,
988 from_row_inits: Vec<TokenStream2>,
989 from_aliased_row_inits: Vec<TokenStream2>,
993 insert_columns: Vec<TokenStream2>,
996 insert_values: Vec<TokenStream2>,
999 insert_pushes: Vec<TokenStream2>,
1004 returning_cols: Vec<TokenStream2>,
1007 auto_assigns: Vec<TokenStream2>,
1010 auto_field_idents: Vec<(syn::Ident, String)>,
1014 first_auto_value_ty: Option<Type>,
1017 bulk_pushes_no_auto: Vec<TokenStream2>,
1021 bulk_pushes_all: Vec<TokenStream2>,
1025 bulk_columns_no_auto: Vec<TokenStream2>,
1028 bulk_columns_all: Vec<TokenStream2>,
1031 bulk_auto_uniformity: Vec<TokenStream2>,
1035 first_auto_ident: Option<syn::Ident>,
1038 has_auto: bool,
1040 pk_is_auto: bool,
1044 update_assignments: Vec<TokenStream2>,
1047 upsert_update_columns: Vec<TokenStream2>,
1050 primary_key: Option<(syn::Ident, String)>,
1051 column_entries: Vec<ColumnEntry>,
1052 field_names: Vec<String>,
1055 fk_relations: Vec<FkRelation>,
1060 soft_delete_column: Option<String>,
1065}
1066
1067#[derive(Clone)]
1068struct FkRelation {
1069 parent_type: Type,
1072 fk_column: String,
1075 pk_kind: DetectedKind,
1080 nullable: bool,
1085}
1086
1087fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1088 let cap = named.named.len();
1089 let mut out = CollectedFields {
1090 field_schemas: Vec::with_capacity(cap),
1091 from_row_inits: Vec::with_capacity(cap),
1092 from_aliased_row_inits: Vec::with_capacity(cap),
1093 insert_columns: Vec::with_capacity(cap),
1094 insert_values: Vec::with_capacity(cap),
1095 insert_pushes: Vec::with_capacity(cap),
1096 returning_cols: Vec::new(),
1097 auto_assigns: Vec::new(),
1098 auto_field_idents: Vec::new(),
1099 first_auto_value_ty: None,
1100 bulk_pushes_no_auto: Vec::with_capacity(cap),
1101 bulk_pushes_all: Vec::with_capacity(cap),
1102 bulk_columns_no_auto: Vec::with_capacity(cap),
1103 bulk_columns_all: Vec::with_capacity(cap),
1104 bulk_auto_uniformity: Vec::new(),
1105 first_auto_ident: None,
1106 has_auto: false,
1107 pk_is_auto: false,
1108 update_assignments: Vec::with_capacity(cap),
1109 upsert_update_columns: Vec::with_capacity(cap),
1110 primary_key: None,
1111 column_entries: Vec::with_capacity(cap),
1112 field_names: Vec::with_capacity(cap),
1113 fk_relations: Vec::new(),
1114 soft_delete_column: None,
1115 };
1116
1117 for field in &named.named {
1118 let info = process_field(field, table)?;
1119 out.field_names.push(info.ident.to_string());
1120 out.field_schemas.push(info.schema);
1121 out.from_row_inits.push(info.from_row_init);
1122 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1123 if let Some(parent_ty) = info.fk_inner.clone() {
1124 out.fk_relations.push(FkRelation {
1125 parent_type: parent_ty,
1126 fk_column: info.column.clone(),
1127 pk_kind: info.fk_pk_kind,
1128 nullable: info.nullable,
1129 });
1130 }
1131 if info.soft_delete {
1132 if out.soft_delete_column.is_some() {
1133 return Err(syn::Error::new_spanned(
1134 field,
1135 "only one field may be marked `#[rustango(soft_delete)]`",
1136 ));
1137 }
1138 out.soft_delete_column = Some(info.column.clone());
1139 }
1140 let column = info.column.as_str();
1141 let ident = info.ident;
1142 if info.generated_as.is_some() {
1151 out.column_entries.push(ColumnEntry {
1152 ident: ident.clone(),
1153 value_ty: info.value_ty.clone(),
1154 name: ident.to_string(),
1155 column: info.column.clone(),
1156 field_type_tokens: info.field_type_tokens,
1157 });
1158 continue;
1159 }
1160 out.insert_columns.push(quote!(#column));
1161 out.insert_values.push(quote! {
1162 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1163 ::core::clone::Clone::clone(&self.#ident)
1164 )
1165 });
1166 if info.auto {
1167 out.has_auto = true;
1168 if out.first_auto_ident.is_none() {
1169 out.first_auto_ident = Some(ident.clone());
1170 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1171 }
1172 out.returning_cols.push(quote!(#column));
1173 out.auto_field_idents
1174 .push((ident.clone(), info.column.clone()));
1175 out.auto_assigns.push(quote! {
1176 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1177 });
1178 out.insert_pushes.push(quote! {
1179 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1180 _columns.push(#column);
1181 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1182 ::core::clone::Clone::clone(_v)
1183 ));
1184 }
1185 });
1186 out.bulk_columns_all.push(quote!(#column));
1189 out.bulk_pushes_all.push(quote! {
1190 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1191 ::core::clone::Clone::clone(&_row.#ident)
1192 ));
1193 });
1194 let ident_clone = ident.clone();
1198 out.bulk_auto_uniformity.push(quote! {
1199 for _r in rows.iter().skip(1) {
1200 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1201 return ::core::result::Result::Err(
1202 ::rustango::sql::ExecError::Sql(
1203 ::rustango::sql::SqlError::BulkAutoMixed
1204 )
1205 );
1206 }
1207 }
1208 });
1209 } else {
1210 out.insert_pushes.push(quote! {
1211 _columns.push(#column);
1212 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1213 ::core::clone::Clone::clone(&self.#ident)
1214 ));
1215 });
1216 out.bulk_columns_no_auto.push(quote!(#column));
1218 out.bulk_columns_all.push(quote!(#column));
1219 let push_expr = quote! {
1220 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1221 ::core::clone::Clone::clone(&_row.#ident)
1222 ));
1223 };
1224 out.bulk_pushes_no_auto.push(push_expr.clone());
1225 out.bulk_pushes_all.push(push_expr);
1226 }
1227 if info.primary_key {
1228 if out.primary_key.is_some() {
1229 return Err(syn::Error::new_spanned(
1230 field,
1231 "only one field may be marked `#[rustango(primary_key)]`",
1232 ));
1233 }
1234 out.primary_key = Some((ident.clone(), info.column.clone()));
1235 if info.auto {
1236 out.pk_is_auto = true;
1237 }
1238 } else if info.auto_now_add {
1239 } else if info.auto_now {
1241 out.update_assignments.push(quote! {
1246 ::rustango::core::Assignment {
1247 column: #column,
1248 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1249 ::chrono::Utc::now()
1250 ),
1251 }
1252 });
1253 out.upsert_update_columns.push(quote!(#column));
1254 } else {
1255 out.update_assignments.push(quote! {
1256 ::rustango::core::Assignment {
1257 column: #column,
1258 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1259 ::core::clone::Clone::clone(&self.#ident)
1260 ),
1261 }
1262 });
1263 out.upsert_update_columns.push(quote!(#column));
1264 }
1265 out.column_entries.push(ColumnEntry {
1266 ident: ident.clone(),
1267 value_ty: info.value_ty.clone(),
1268 name: ident.to_string(),
1269 column: info.column.clone(),
1270 field_type_tokens: info.field_type_tokens,
1271 });
1272 }
1273 Ok(out)
1274}
1275
1276fn model_impl_tokens(
1277 struct_name: &syn::Ident,
1278 model_name: &str,
1279 table: &str,
1280 display: Option<&str>,
1281 app_label: Option<&str>,
1282 admin: Option<&AdminAttrs>,
1283 field_schemas: &[TokenStream2],
1284 soft_delete_column: Option<&str>,
1285 permissions: bool,
1286 audit_track: Option<&[String]>,
1287 m2m_relations: &[M2MAttr],
1288 indexes: &[IndexAttr],
1289 checks: &[CheckAttr],
1290 composite_fks: &[CompositeFkAttr],
1291 generic_fks: &[GenericFkAttr],
1292 scope: Option<&str>,
1293) -> TokenStream2 {
1294 let display_tokens = if let Some(name) = display {
1295 quote!(::core::option::Option::Some(#name))
1296 } else {
1297 quote!(::core::option::Option::None)
1298 };
1299 let app_label_tokens = if let Some(name) = app_label {
1300 quote!(::core::option::Option::Some(#name))
1301 } else {
1302 quote!(::core::option::Option::None)
1303 };
1304 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1305 quote!(::core::option::Option::Some(#col))
1306 } else {
1307 quote!(::core::option::Option::None)
1308 };
1309 let audit_track_tokens = match audit_track {
1310 None => quote!(::core::option::Option::None),
1311 Some(names) => {
1312 let lits = names.iter().map(|n| n.as_str());
1313 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1314 }
1315 };
1316 let admin_tokens = admin_config_tokens(admin);
1317 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1321 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1322 _ => quote!(::rustango::core::ModelScope::Tenant),
1323 };
1324 let indexes_tokens = indexes.iter().map(|idx| {
1325 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1326 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1327 let unique = idx.unique;
1328 quote! {
1329 ::rustango::core::IndexSchema {
1330 name: #name,
1331 columns: &[ #(#cols),* ],
1332 unique: #unique,
1333 }
1334 }
1335 });
1336 let checks_tokens = checks.iter().map(|c| {
1337 let name = c.name.as_str();
1338 let expr = c.expr.as_str();
1339 quote! {
1340 ::rustango::core::CheckConstraint {
1341 name: #name,
1342 expr: #expr,
1343 }
1344 }
1345 });
1346 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1347 let name = rel.name.as_str();
1348 let to = rel.to.as_str();
1349 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1350 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1351 quote! {
1352 ::rustango::core::CompositeFkRelation {
1353 name: #name,
1354 to: #to,
1355 from: &[ #(#from_cols),* ],
1356 on: &[ #(#on_cols),* ],
1357 }
1358 }
1359 });
1360 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1361 let name = rel.name.as_str();
1362 let ct_col = rel.ct_column.as_str();
1363 let pk_col = rel.pk_column.as_str();
1364 quote! {
1365 ::rustango::core::GenericRelation {
1366 name: #name,
1367 ct_column: #ct_col,
1368 pk_column: #pk_col,
1369 }
1370 }
1371 });
1372 let m2m_tokens = m2m_relations.iter().map(|rel| {
1373 let name = rel.name.as_str();
1374 let to = rel.to.as_str();
1375 let through = rel.through.as_str();
1376 let src = rel.src.as_str();
1377 let dst = rel.dst.as_str();
1378 quote! {
1379 ::rustango::core::M2MRelation {
1380 name: #name,
1381 to: #to,
1382 through: #through,
1383 src_col: #src,
1384 dst_col: #dst,
1385 }
1386 }
1387 });
1388 quote! {
1389 impl ::rustango::core::Model for #struct_name {
1390 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1391 name: #model_name,
1392 table: #table,
1393 fields: &[ #(#field_schemas),* ],
1394 display: #display_tokens,
1395 app_label: #app_label_tokens,
1396 admin: #admin_tokens,
1397 soft_delete_column: #soft_delete_tokens,
1398 permissions: #permissions,
1399 audit_track: #audit_track_tokens,
1400 m2m: &[ #(#m2m_tokens),* ],
1401 indexes: &[ #(#indexes_tokens),* ],
1402 check_constraints: &[ #(#checks_tokens),* ],
1403 composite_relations: &[ #(#composite_fk_tokens),* ],
1404 generic_relations: &[ #(#generic_fk_tokens),* ],
1405 scope: #scope_tokens,
1406 };
1407 }
1408 }
1409}
1410
1411fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1415 let Some(admin) = admin else {
1416 return quote!(::core::option::Option::None);
1417 };
1418
1419 let list_display = admin
1420 .list_display
1421 .as_ref()
1422 .map(|(v, _)| v.as_slice())
1423 .unwrap_or(&[]);
1424 let list_display_lits = list_display.iter().map(|s| s.as_str());
1425
1426 let search_fields = admin
1427 .search_fields
1428 .as_ref()
1429 .map(|(v, _)| v.as_slice())
1430 .unwrap_or(&[]);
1431 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1432
1433 let readonly_fields = admin
1434 .readonly_fields
1435 .as_ref()
1436 .map(|(v, _)| v.as_slice())
1437 .unwrap_or(&[]);
1438 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1439
1440 let list_filter = admin
1441 .list_filter
1442 .as_ref()
1443 .map(|(v, _)| v.as_slice())
1444 .unwrap_or(&[]);
1445 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1446
1447 let actions = admin
1448 .actions
1449 .as_ref()
1450 .map(|(v, _)| v.as_slice())
1451 .unwrap_or(&[]);
1452 let actions_lits = actions.iter().map(|s| s.as_str());
1453
1454 let fieldsets = admin
1455 .fieldsets
1456 .as_ref()
1457 .map(|(v, _)| v.as_slice())
1458 .unwrap_or(&[]);
1459 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1460 let title = title.as_str();
1461 let field_lits = fields.iter().map(|s| s.as_str());
1462 quote!(::rustango::core::Fieldset {
1463 title: #title,
1464 fields: &[ #( #field_lits ),* ],
1465 })
1466 });
1467
1468 let list_per_page = admin.list_per_page.unwrap_or(0);
1469
1470 let ordering_pairs = admin
1471 .ordering
1472 .as_ref()
1473 .map(|(v, _)| v.as_slice())
1474 .unwrap_or(&[]);
1475 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1476 let name = name.as_str();
1477 let desc = *desc;
1478 quote!((#name, #desc))
1479 });
1480
1481 quote! {
1482 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1483 list_display: &[ #( #list_display_lits ),* ],
1484 search_fields: &[ #( #search_fields_lits ),* ],
1485 list_per_page: #list_per_page,
1486 ordering: &[ #( #ordering_tokens ),* ],
1487 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1488 list_filter: &[ #( #list_filter_lits ),* ],
1489 actions: &[ #( #actions_lits ),* ],
1490 fieldsets: &[ #( #fieldset_tokens ),* ],
1491 })
1492 }
1493}
1494
1495fn inherent_impl_tokens(
1496 struct_name: &syn::Ident,
1497 fields: &CollectedFields,
1498 primary_key: Option<&(syn::Ident, String)>,
1499 column_consts: &TokenStream2,
1500 audited_fields: Option<&[&ColumnEntry]>,
1501 indexes: &[IndexAttr],
1502) -> TokenStream2 {
1503 let executor_passes_to_data_write = if audited_fields.is_some() {
1509 quote!(&mut *_executor)
1510 } else {
1511 quote!(_executor)
1512 };
1513 let executor_param = if audited_fields.is_some() {
1514 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1515 } else {
1516 quote!(_executor: _E)
1517 };
1518 let executor_generics = if audited_fields.is_some() {
1519 quote!()
1520 } else {
1521 quote!(<'_c, _E>)
1522 };
1523 let executor_where = if audited_fields.is_some() {
1524 quote!()
1525 } else {
1526 quote! {
1527 where
1528 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1529 }
1530 };
1531 let pool_to_save_on = if audited_fields.is_some() {
1536 quote! {
1537 let mut _conn = pool.acquire().await?;
1538 self.save_on(&mut *_conn).await
1539 }
1540 } else {
1541 quote!(self.save_on(pool).await)
1542 };
1543 let pool_to_insert_on = if audited_fields.is_some() {
1544 quote! {
1545 let mut _conn = pool.acquire().await?;
1546 self.insert_on(&mut *_conn).await
1547 }
1548 } else {
1549 quote!(self.insert_on(pool).await)
1550 };
1551 let pool_to_delete_on = if audited_fields.is_some() {
1552 quote! {
1553 let mut _conn = pool.acquire().await?;
1554 self.delete_on(&mut *_conn).await
1555 }
1556 } else {
1557 quote!(self.delete_on(pool).await)
1558 };
1559 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1560 quote! {
1561 let mut _conn = pool.acquire().await?;
1562 Self::bulk_insert_on(rows, &mut *_conn).await
1563 }
1564 } else {
1565 quote!(Self::bulk_insert_on(rows, pool).await)
1566 };
1567 let pool_to_upsert_on = if audited_fields.is_some() {
1574 quote! {
1575 let mut _conn = pool.acquire().await?;
1576 self.upsert_on(&mut *_conn).await
1577 }
1578 } else {
1579 quote!(self.upsert_on(pool).await)
1580 };
1581
1582 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1600 quote!()
1609 } else if audited_fields.is_some() && fields.has_auto {
1610 quote!()
1613 } else if fields.has_auto {
1614 let pushes = &fields.insert_pushes;
1615 let returning_cols = &fields.returning_cols;
1616 quote! {
1617 pub async fn insert_pool(
1623 &mut self,
1624 pool: &::rustango::sql::Pool,
1625 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1626 let mut _columns: ::std::vec::Vec<&'static str> =
1627 ::std::vec::Vec::new();
1628 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1629 ::std::vec::Vec::new();
1630 #( #pushes )*
1631 let _query = ::rustango::core::InsertQuery {
1632 model: <Self as ::rustango::core::Model>::SCHEMA,
1633 columns: _columns,
1634 values: _values,
1635 returning: ::std::vec![ #( #returning_cols ),* ],
1636 on_conflict: ::core::option::Option::None,
1637 };
1638 let _result = ::rustango::sql::insert_returning_pool(
1639 pool, &_query,
1640 ).await?;
1641 ::rustango::sql::apply_auto_pk_pool(_result, self)
1642 }
1643 }
1644 } else {
1645 let insert_columns = &fields.insert_columns;
1646 let insert_values = &fields.insert_values;
1647 quote! {
1648 pub async fn insert_pool(
1655 &self,
1656 pool: &::rustango::sql::Pool,
1657 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1658 let _query = ::rustango::core::InsertQuery {
1659 model: <Self as ::rustango::core::Model>::SCHEMA,
1660 columns: ::std::vec![ #( #insert_columns ),* ],
1661 values: ::std::vec![ #( #insert_values ),* ],
1662 returning: ::std::vec::Vec::new(),
1663 on_conflict: ::core::option::Option::None,
1664 };
1665 ::rustango::sql::insert_pool(pool, &_query).await
1666 }
1667 }
1668 };
1669
1670 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1683 .map(|tracked| {
1684 tracked
1685 .iter()
1686 .map(|c| {
1687 let column_lit = c.column.as_str();
1688 let ident = &c.ident;
1689 quote! {
1690 (
1691 #column_lit,
1692 ::serde_json::to_value(&self.#ident)
1693 .unwrap_or(::serde_json::Value::Null),
1694 )
1695 }
1696 })
1697 .collect()
1698 })
1699 .unwrap_or_default();
1700 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1701 if fields.pk_is_auto {
1702 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1703 } else {
1704 quote!(::std::format!("{}", &self.#pk_ident))
1705 }
1706 } else {
1707 quote!(::std::string::String::new())
1708 };
1709 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1710 if audited_fields.is_some() {
1711 let pairs = audit_pair_tokens.iter();
1712 let pk_str = audit_pk_to_string.clone();
1713 quote! {
1714 let _audit_entry = ::rustango::audit::PendingEntry {
1715 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1716 entity_pk: #pk_str,
1717 operation: #op_path,
1718 source: ::rustango::audit::current_source(),
1719 changes: ::rustango::audit::snapshot_changes(&[
1720 #( #pairs ),*
1721 ]),
1722 };
1723 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1724 }
1725 } else {
1726 quote!()
1727 }
1728 };
1729 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1730 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1731 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1732 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1733
1734 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1750 let pk_column_lit = pk_col.as_str();
1751 let assignments = &fields.update_assignments;
1752 if audited_fields.is_some() {
1753 if fields.pk_is_auto {
1754 quote!()
1758 } else {
1759 let pairs = audit_pair_tokens.iter();
1760 let pk_str = audit_pk_to_string.clone();
1761 quote! {
1762 pub async fn save_pool(
1776 &mut self,
1777 pool: &::rustango::sql::Pool,
1778 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1779 let _query = ::rustango::core::UpdateQuery {
1780 model: <Self as ::rustango::core::Model>::SCHEMA,
1781 set: ::std::vec![ #( #assignments ),* ],
1782 where_clause: ::rustango::core::WhereExpr::Predicate(
1783 ::rustango::core::Filter {
1784 column: #pk_column_lit,
1785 op: ::rustango::core::Op::Eq,
1786 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1787 ::core::clone::Clone::clone(&self.#pk_ident)
1788 ),
1789 }
1790 ),
1791 };
1792 let _audit_entry = ::rustango::audit::PendingEntry {
1793 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1794 entity_pk: #pk_str,
1795 operation: ::rustango::audit::AuditOp::Update,
1796 source: ::rustango::audit::current_source(),
1797 changes: ::rustango::audit::snapshot_changes(&[
1798 #( #pairs ),*
1799 ]),
1800 };
1801 let _ = ::rustango::audit::save_one_with_audit_pool(
1802 pool, &_query, &_audit_entry,
1803 ).await?;
1804 ::core::result::Result::Ok(())
1805 }
1806 }
1807 }
1808 } else {
1809 let dispatch_unset = if fields.pk_is_auto {
1810 quote! {
1811 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1812 return self.insert_pool(pool).await;
1813 }
1814 }
1815 } else {
1816 quote!()
1817 };
1818 quote! {
1819 pub async fn save_pool(
1826 &mut self,
1827 pool: &::rustango::sql::Pool,
1828 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1829 #dispatch_unset
1830 let _query = ::rustango::core::UpdateQuery {
1831 model: <Self as ::rustango::core::Model>::SCHEMA,
1832 set: ::std::vec![ #( #assignments ),* ],
1833 where_clause: ::rustango::core::WhereExpr::Predicate(
1834 ::rustango::core::Filter {
1835 column: #pk_column_lit,
1836 op: ::rustango::core::Op::Eq,
1837 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1838 ::core::clone::Clone::clone(&self.#pk_ident)
1839 ),
1840 }
1841 ),
1842 };
1843 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1844 ::core::result::Result::Ok(())
1845 }
1846 }
1847 }
1848 } else {
1849 quote!()
1850 };
1851
1852 let pool_insert_method = if audited_fields.is_some() {
1859 if let Some(_) = primary_key {
1860 let pushes = if fields.has_auto {
1861 fields.insert_pushes.clone()
1862 } else {
1863 fields
1868 .insert_columns
1869 .iter()
1870 .zip(&fields.insert_values)
1871 .map(|(col, val)| {
1872 quote! {
1873 _columns.push(#col);
1874 _values.push(#val);
1875 }
1876 })
1877 .collect()
1878 };
1879 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1880 fields.returning_cols.clone()
1881 } else {
1882 primary_key
1889 .map(|(_, col)| {
1890 let lit = col.as_str();
1891 vec![quote!(#lit)]
1892 })
1893 .unwrap_or_default()
1894 };
1895 let pairs = audit_pair_tokens.iter();
1896 let pk_str = audit_pk_to_string.clone();
1897 quote! {
1898 pub async fn insert_pool(
1907 &mut self,
1908 pool: &::rustango::sql::Pool,
1909 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1910 let mut _columns: ::std::vec::Vec<&'static str> =
1911 ::std::vec::Vec::new();
1912 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1913 ::std::vec::Vec::new();
1914 #( #pushes )*
1915 let _query = ::rustango::core::InsertQuery {
1916 model: <Self as ::rustango::core::Model>::SCHEMA,
1917 columns: _columns,
1918 values: _values,
1919 returning: ::std::vec![ #( #returning_cols ),* ],
1920 on_conflict: ::core::option::Option::None,
1921 };
1922 let _audit_entry = ::rustango::audit::PendingEntry {
1923 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1924 entity_pk: #pk_str,
1925 operation: ::rustango::audit::AuditOp::Create,
1926 source: ::rustango::audit::current_source(),
1927 changes: ::rustango::audit::snapshot_changes(&[
1928 #( #pairs ),*
1929 ]),
1930 };
1931 let _result = ::rustango::audit::insert_one_with_audit_pool(
1932 pool, &_query, &_audit_entry,
1933 ).await?;
1934 ::rustango::sql::apply_auto_pk_pool(_result, self)
1935 }
1936 }
1937 } else {
1938 quote!()
1939 }
1940 } else {
1941 pool_insert_method
1943 };
1944
1945 let pool_save_method = if let Some(tracked) = audited_fields {
1966 if let Some((pk_ident, pk_col)) = primary_key {
1967 let pk_column_lit = pk_col.as_str();
1968 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1972 let pk_str = audit_pk_to_string.clone();
1973 let mk_before_pairs =
1978 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1979 tracked
1980 .iter()
1981 .map(|c| {
1982 let column_lit = c.column.as_str();
1983 let value_ty = &c.value_ty;
1984 quote! {
1985 (
1986 #column_lit,
1987 match #getter::<#value_ty>(
1988 _audit_before_row, #column_lit,
1989 ) {
1990 ::core::result::Result::Ok(v) => {
1991 ::serde_json::to_value(&v)
1992 .unwrap_or(::serde_json::Value::Null)
1993 }
1994 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1995 },
1996 )
1997 }
1998 })
1999 .collect()
2000 };
2001 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
2002 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
2003 let before_pairs_my: Vec<proc_macro2::TokenStream> =
2004 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
2005 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
2006 mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
2007 let pg_select_cols: String = tracked
2008 .iter()
2009 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2010 .collect::<Vec<_>>()
2011 .join(", ");
2012 let my_select_cols: String = tracked
2013 .iter()
2014 .map(|c| format!("`{}`", c.column.replace('`', "``")))
2015 .collect::<Vec<_>>()
2016 .join(", ");
2017 let sqlite_select_cols: String = pg_select_cols.clone();
2021 let pk_value_for_bind = if fields.pk_is_auto {
2022 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2023 } else {
2024 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2025 };
2026 let assignments = &fields.update_assignments;
2027 let unset_dispatch = if fields.has_auto {
2028 quote! {
2029 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2030 return self.insert_pool(pool).await;
2031 }
2032 }
2033 } else {
2034 quote!()
2035 };
2036 quote! {
2037 pub async fn save_pool(
2051 &mut self,
2052 pool: &::rustango::sql::Pool,
2053 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2054 #unset_dispatch
2055 let _query = ::rustango::core::UpdateQuery {
2056 model: <Self as ::rustango::core::Model>::SCHEMA,
2057 set: ::std::vec![ #( #assignments ),* ],
2058 where_clause: ::rustango::core::WhereExpr::Predicate(
2059 ::rustango::core::Filter {
2060 column: #pk_column_lit,
2061 op: ::rustango::core::Op::Eq,
2062 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2063 ::core::clone::Clone::clone(&self.#pk_ident)
2064 ),
2065 }
2066 ),
2067 };
2068 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2069 ::std::vec![ #( #after_pairs_pg ),* ];
2070 ::rustango::audit::save_one_with_diff_pool(
2071 pool,
2072 &_query,
2073 #pk_column_lit,
2074 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2075 #pk_value_for_bind,
2076 ),
2077 <Self as ::rustango::core::Model>::SCHEMA.table,
2078 #pk_str,
2079 _after_pairs,
2080 #pg_select_cols,
2081 #my_select_cols,
2082 #sqlite_select_cols,
2083 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2084 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2085 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2086 ).await
2087 }
2088 }
2089 } else {
2090 quote!()
2091 }
2092 } else {
2093 pool_save_method
2094 };
2095
2096 let pool_delete_method = {
2103 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2104 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2105 if let Some(pk_ident) = pk_ident_for_pool {
2106 if audited_fields.is_some() {
2107 let pairs = audit_pair_tokens.iter();
2108 let pk_str = audit_pk_to_string.clone();
2109 quote! {
2110 pub async fn delete_pool(
2117 &self,
2118 pool: &::rustango::sql::Pool,
2119 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2120 let _query = ::rustango::core::DeleteQuery {
2121 model: <Self as ::rustango::core::Model>::SCHEMA,
2122 where_clause: ::rustango::core::WhereExpr::Predicate(
2123 ::rustango::core::Filter {
2124 column: #pk_column_lit,
2125 op: ::rustango::core::Op::Eq,
2126 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2127 ::core::clone::Clone::clone(&self.#pk_ident)
2128 ),
2129 }
2130 ),
2131 };
2132 let _audit_entry = ::rustango::audit::PendingEntry {
2133 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2134 entity_pk: #pk_str,
2135 operation: ::rustango::audit::AuditOp::Delete,
2136 source: ::rustango::audit::current_source(),
2137 changes: ::rustango::audit::snapshot_changes(&[
2138 #( #pairs ),*
2139 ]),
2140 };
2141 ::rustango::audit::delete_one_with_audit_pool(
2142 pool, &_query, &_audit_entry,
2143 ).await
2144 }
2145 }
2146 } else {
2147 quote! {
2148 pub async fn delete_pool(
2155 &self,
2156 pool: &::rustango::sql::Pool,
2157 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2158 let _query = ::rustango::core::DeleteQuery {
2159 model: <Self as ::rustango::core::Model>::SCHEMA,
2160 where_clause: ::rustango::core::WhereExpr::Predicate(
2161 ::rustango::core::Filter {
2162 column: #pk_column_lit,
2163 op: ::rustango::core::Op::Eq,
2164 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2165 ::core::clone::Clone::clone(&self.#pk_ident)
2166 ),
2167 }
2168 ),
2169 };
2170 ::rustango::sql::delete_pool(pool, &_query).await
2171 }
2172 }
2173 }
2174 } else {
2175 quote!()
2176 }
2177 };
2178
2179 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
2189 audited_fields
2190 {
2191 if tracked.is_empty() {
2192 (quote!(), quote!())
2193 } else {
2194 let select_cols: String = tracked
2195 .iter()
2196 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2197 .collect::<Vec<_>>()
2198 .join(", ");
2199 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
2200 let select_cols_lit = select_cols;
2201 let pk_column_lit_for_select = pk_column_for_select;
2202 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2203 if fields.pk_is_auto {
2204 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2205 } else {
2206 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2207 }
2208 } else {
2209 quote!(0_i64)
2210 };
2211 let before_pairs = tracked.iter().map(|c| {
2212 let column_lit = c.column.as_str();
2213 let value_ty = &c.value_ty;
2214 quote! {
2215 (
2216 #column_lit,
2217 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2218 &_audit_before_row, #column_lit,
2219 ) {
2220 ::core::result::Result::Ok(v) => {
2221 ::serde_json::to_value(&v)
2222 .unwrap_or(::serde_json::Value::Null)
2223 }
2224 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2225 },
2226 )
2227 }
2228 });
2229 let after_pairs = tracked.iter().map(|c| {
2230 let column_lit = c.column.as_str();
2231 let ident = &c.ident;
2232 quote! {
2233 (
2234 #column_lit,
2235 ::serde_json::to_value(&self.#ident)
2236 .unwrap_or(::serde_json::Value::Null),
2237 )
2238 }
2239 });
2240 let pk_str = audit_pk_to_string.clone();
2241 let pre = quote! {
2242 let _audit_select_sql = ::std::format!(
2243 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2244 #select_cols_lit,
2245 <Self as ::rustango::core::Model>::SCHEMA.table,
2246 #pk_column_lit_for_select,
2247 );
2248 let _audit_before_pairs:
2249 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2250 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2251 .bind(#pk_value_for_bind)
2252 .fetch_optional(&mut *_executor)
2253 .await
2254 {
2255 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2256 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2257 }
2258 _ => ::core::option::Option::None,
2259 };
2260 };
2261 let post = quote! {
2262 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2263 let _audit_after:
2264 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2265 ::std::vec![ #( #after_pairs ),* ];
2266 let _audit_entry = ::rustango::audit::PendingEntry {
2267 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2268 entity_pk: #pk_str,
2269 operation: ::rustango::audit::AuditOp::Update,
2270 source: ::rustango::audit::current_source(),
2271 changes: ::rustango::audit::diff_changes(
2272 &_audit_before,
2273 &_audit_after,
2274 ),
2275 };
2276 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2277 }
2278 };
2279 (pre, post)
2280 }
2281 } else {
2282 (quote!(), quote!())
2283 };
2284
2285 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2289 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2290 if fields.pk_is_auto {
2291 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2292 } else {
2293 quote!(::std::format!("{}", &_row.#pk_ident))
2294 }
2295 } else {
2296 quote!(::std::string::String::new())
2297 };
2298 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
2299 let column_lit = c.column.as_str();
2300 let ident = &c.ident;
2301 quote! {
2302 (
2303 #column_lit,
2304 ::serde_json::to_value(&_row.#ident)
2305 .unwrap_or(::serde_json::Value::Null),
2306 )
2307 }
2308 });
2309 quote! {
2310 let _audit_source = ::rustango::audit::current_source();
2311 let mut _audit_entries:
2312 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2313 ::std::vec::Vec::with_capacity(rows.len());
2314 for _row in rows.iter() {
2315 _audit_entries.push(::rustango::audit::PendingEntry {
2316 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2317 entity_pk: #row_pk_str,
2318 operation: ::rustango::audit::AuditOp::Create,
2319 source: _audit_source.clone(),
2320 changes: ::rustango::audit::snapshot_changes(&[
2321 #( #row_pairs ),*
2322 ]),
2323 });
2324 }
2325 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2326 }
2327 } else {
2328 quote!()
2329 };
2330
2331 let save_method = if fields.pk_is_auto {
2332 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
2333 let pk_column_lit = pk_column.as_str();
2334 let assignments = &fields.update_assignments;
2335 let upsert_cols = &fields.upsert_update_columns;
2336 let upsert_pushes = &fields.insert_pushes;
2337 let upsert_returning = &fields.returning_cols;
2338 let upsert_auto_assigns = &fields.auto_assigns;
2339 let upsert_target_columns: Vec<String> = indexes
2348 .iter()
2349 .find(|i| i.unique && !i.columns.is_empty())
2350 .map(|i| i.columns.clone())
2351 .unwrap_or_else(|| vec![pk_column.clone()]);
2352 let upsert_target_lits = upsert_target_columns
2353 .iter()
2354 .map(String::as_str)
2355 .collect::<Vec<_>>();
2356 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2357 quote!(::rustango::core::ConflictClause::DoNothing)
2358 } else {
2359 quote!(::rustango::core::ConflictClause::DoUpdate {
2360 target: ::std::vec![ #( #upsert_target_lits ),* ],
2361 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2362 })
2363 };
2364 Some(quote! {
2365 pub async fn save(
2383 &mut self,
2384 pool: &::rustango::sql::sqlx::PgPool,
2385 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2386 #pool_to_save_on
2387 }
2388
2389 pub async fn save_on #executor_generics (
2400 &mut self,
2401 #executor_param,
2402 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2403 #executor_where
2404 {
2405 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2406 return self.insert_on(#executor_passes_to_data_write).await;
2407 }
2408 #audit_update_pre
2409 let _query = ::rustango::core::UpdateQuery {
2410 model: <Self as ::rustango::core::Model>::SCHEMA,
2411 set: ::std::vec![ #( #assignments ),* ],
2412 where_clause: ::rustango::core::WhereExpr::Predicate(
2413 ::rustango::core::Filter {
2414 column: #pk_column_lit,
2415 op: ::rustango::core::Op::Eq,
2416 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2417 ::core::clone::Clone::clone(&self.#pk_ident)
2418 ),
2419 }
2420 ),
2421 };
2422 let _ = ::rustango::sql::update_on(
2423 #executor_passes_to_data_write,
2424 &_query,
2425 ).await?;
2426 #audit_update_post
2427 ::core::result::Result::Ok(())
2428 }
2429
2430 pub async fn save_on_with #executor_generics (
2441 &mut self,
2442 #executor_param,
2443 source: ::rustango::audit::AuditSource,
2444 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2445 #executor_where
2446 {
2447 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2448 }
2449
2450 pub async fn upsert(
2460 &mut self,
2461 pool: &::rustango::sql::sqlx::PgPool,
2462 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2463 #pool_to_upsert_on
2464 }
2465
2466 pub async fn upsert_on #executor_generics (
2472 &mut self,
2473 #executor_param,
2474 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2475 #executor_where
2476 {
2477 let mut _columns: ::std::vec::Vec<&'static str> =
2478 ::std::vec::Vec::new();
2479 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2480 ::std::vec::Vec::new();
2481 #( #upsert_pushes )*
2482 let query = ::rustango::core::InsertQuery {
2483 model: <Self as ::rustango::core::Model>::SCHEMA,
2484 columns: _columns,
2485 values: _values,
2486 returning: ::std::vec![ #( #upsert_returning ),* ],
2487 on_conflict: ::core::option::Option::Some(#conflict_clause),
2488 };
2489 let _returning_row_v = ::rustango::sql::insert_returning_on(
2490 #executor_passes_to_data_write,
2491 &query,
2492 ).await?;
2493 let _returning_row = &_returning_row_v;
2494 #( #upsert_auto_assigns )*
2495 ::core::result::Result::Ok(())
2496 }
2497 })
2498 } else {
2499 None
2500 };
2501
2502 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2503 let pk_column_lit = pk_column.as_str();
2504 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2511 let col_lit = col;
2512 quote! {
2513 pub async fn soft_delete_on #executor_generics (
2523 &self,
2524 #executor_param,
2525 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2526 #executor_where
2527 {
2528 let _query = ::rustango::core::UpdateQuery {
2529 model: <Self as ::rustango::core::Model>::SCHEMA,
2530 set: ::std::vec![
2531 ::rustango::core::Assignment {
2532 column: #col_lit,
2533 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2534 ::chrono::Utc::now()
2535 ),
2536 },
2537 ],
2538 where_clause: ::rustango::core::WhereExpr::Predicate(
2539 ::rustango::core::Filter {
2540 column: #pk_column_lit,
2541 op: ::rustango::core::Op::Eq,
2542 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2543 ::core::clone::Clone::clone(&self.#pk_ident)
2544 ),
2545 }
2546 ),
2547 };
2548 let _affected = ::rustango::sql::update_on(
2549 #executor_passes_to_data_write,
2550 &_query,
2551 ).await?;
2552 #audit_softdelete_emit
2553 ::core::result::Result::Ok(_affected)
2554 }
2555
2556 pub async fn restore_on #executor_generics (
2563 &self,
2564 #executor_param,
2565 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2566 #executor_where
2567 {
2568 let _query = ::rustango::core::UpdateQuery {
2569 model: <Self as ::rustango::core::Model>::SCHEMA,
2570 set: ::std::vec![
2571 ::rustango::core::Assignment {
2572 column: #col_lit,
2573 value: ::rustango::core::SqlValue::Null,
2574 },
2575 ],
2576 where_clause: ::rustango::core::WhereExpr::Predicate(
2577 ::rustango::core::Filter {
2578 column: #pk_column_lit,
2579 op: ::rustango::core::Op::Eq,
2580 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2581 ::core::clone::Clone::clone(&self.#pk_ident)
2582 ),
2583 }
2584 ),
2585 };
2586 let _affected = ::rustango::sql::update_on(
2587 #executor_passes_to_data_write,
2588 &_query,
2589 ).await?;
2590 #audit_restore_emit
2591 ::core::result::Result::Ok(_affected)
2592 }
2593 }
2594 } else {
2595 quote!()
2596 };
2597 quote! {
2598 pub async fn delete(
2606 &self,
2607 pool: &::rustango::sql::sqlx::PgPool,
2608 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2609 #pool_to_delete_on
2610 }
2611
2612 pub async fn delete_on #executor_generics (
2619 &self,
2620 #executor_param,
2621 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2622 #executor_where
2623 {
2624 let query = ::rustango::core::DeleteQuery {
2625 model: <Self as ::rustango::core::Model>::SCHEMA,
2626 where_clause: ::rustango::core::WhereExpr::Predicate(
2627 ::rustango::core::Filter {
2628 column: #pk_column_lit,
2629 op: ::rustango::core::Op::Eq,
2630 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2631 ::core::clone::Clone::clone(&self.#pk_ident)
2632 ),
2633 }
2634 ),
2635 };
2636 let _affected = ::rustango::sql::delete_on(
2637 #executor_passes_to_data_write,
2638 &query,
2639 ).await?;
2640 #audit_delete_emit
2641 ::core::result::Result::Ok(_affected)
2642 }
2643
2644 pub async fn delete_on_with #executor_generics (
2650 &self,
2651 #executor_param,
2652 source: ::rustango::audit::AuditSource,
2653 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2654 #executor_where
2655 {
2656 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2657 }
2658 #pool_delete_method
2659 #pool_insert_method
2660 #pool_save_method
2661 #soft_delete_methods
2662 }
2663 });
2664
2665 let insert_method = if fields.has_auto {
2666 let pushes = &fields.insert_pushes;
2667 let returning_cols = &fields.returning_cols;
2668 let auto_assigns = &fields.auto_assigns;
2669 quote! {
2670 pub async fn insert(
2679 &mut self,
2680 pool: &::rustango::sql::sqlx::PgPool,
2681 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2682 #pool_to_insert_on
2683 }
2684
2685 pub async fn insert_on #executor_generics (
2691 &mut self,
2692 #executor_param,
2693 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2694 #executor_where
2695 {
2696 let mut _columns: ::std::vec::Vec<&'static str> =
2697 ::std::vec::Vec::new();
2698 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2699 ::std::vec::Vec::new();
2700 #( #pushes )*
2701 let query = ::rustango::core::InsertQuery {
2702 model: <Self as ::rustango::core::Model>::SCHEMA,
2703 columns: _columns,
2704 values: _values,
2705 returning: ::std::vec![ #( #returning_cols ),* ],
2706 on_conflict: ::core::option::Option::None,
2707 };
2708 let _returning_row_v = ::rustango::sql::insert_returning_on(
2709 #executor_passes_to_data_write,
2710 &query,
2711 ).await?;
2712 let _returning_row = &_returning_row_v;
2713 #( #auto_assigns )*
2714 #audit_insert_emit
2715 ::core::result::Result::Ok(())
2716 }
2717
2718 pub async fn insert_on_with #executor_generics (
2724 &mut self,
2725 #executor_param,
2726 source: ::rustango::audit::AuditSource,
2727 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2728 #executor_where
2729 {
2730 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2731 }
2732 }
2733 } else {
2734 let insert_columns = &fields.insert_columns;
2735 let insert_values = &fields.insert_values;
2736 quote! {
2737 pub async fn insert(
2743 &self,
2744 pool: &::rustango::sql::sqlx::PgPool,
2745 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2746 self.insert_on(pool).await
2747 }
2748
2749 pub async fn insert_on<'_c, _E>(
2755 &self,
2756 _executor: _E,
2757 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2758 where
2759 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2760 {
2761 let query = ::rustango::core::InsertQuery {
2762 model: <Self as ::rustango::core::Model>::SCHEMA,
2763 columns: ::std::vec![ #( #insert_columns ),* ],
2764 values: ::std::vec![ #( #insert_values ),* ],
2765 returning: ::std::vec::Vec::new(),
2766 on_conflict: ::core::option::Option::None,
2767 };
2768 ::rustango::sql::insert_on(_executor, &query).await
2769 }
2770 }
2771 };
2772
2773 let bulk_insert_method = if fields.has_auto {
2774 let cols_no_auto = &fields.bulk_columns_no_auto;
2775 let cols_all = &fields.bulk_columns_all;
2776 let pushes_no_auto = &fields.bulk_pushes_no_auto;
2777 let pushes_all = &fields.bulk_pushes_all;
2778 let returning_cols = &fields.returning_cols;
2779 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2780 let uniformity = &fields.bulk_auto_uniformity;
2781 let first_auto_ident = fields
2782 .first_auto_ident
2783 .as_ref()
2784 .expect("has_auto implies first_auto_ident is Some");
2785 quote! {
2786 pub async fn bulk_insert(
2800 rows: &mut [Self],
2801 pool: &::rustango::sql::sqlx::PgPool,
2802 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2803 #pool_to_bulk_insert_on
2804 }
2805
2806 pub async fn bulk_insert_on #executor_generics (
2812 rows: &mut [Self],
2813 #executor_param,
2814 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2815 #executor_where
2816 {
2817 if rows.is_empty() {
2818 return ::core::result::Result::Ok(());
2819 }
2820 let _first_unset = matches!(
2821 rows[0].#first_auto_ident,
2822 ::rustango::sql::Auto::Unset
2823 );
2824 #( #uniformity )*
2825
2826 let mut _all_rows: ::std::vec::Vec<
2827 ::std::vec::Vec<::rustango::core::SqlValue>,
2828 > = ::std::vec::Vec::with_capacity(rows.len());
2829 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2830 for _row in rows.iter() {
2831 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2832 ::std::vec::Vec::new();
2833 #( #pushes_no_auto )*
2834 _all_rows.push(_row_vals);
2835 }
2836 ::std::vec![ #( #cols_no_auto ),* ]
2837 } else {
2838 for _row in rows.iter() {
2839 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2840 ::std::vec::Vec::new();
2841 #( #pushes_all )*
2842 _all_rows.push(_row_vals);
2843 }
2844 ::std::vec![ #( #cols_all ),* ]
2845 };
2846
2847 let _query = ::rustango::core::BulkInsertQuery {
2848 model: <Self as ::rustango::core::Model>::SCHEMA,
2849 columns: _columns,
2850 rows: _all_rows,
2851 returning: ::std::vec![ #( #returning_cols ),* ],
2852 on_conflict: ::core::option::Option::None,
2853 };
2854 let _returned = ::rustango::sql::bulk_insert_on(
2855 #executor_passes_to_data_write,
2856 &_query,
2857 ).await?;
2858 if _returned.len() != rows.len() {
2859 return ::core::result::Result::Err(
2860 ::rustango::sql::ExecError::Sql(
2861 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2862 expected: rows.len(),
2863 actual: _returned.len(),
2864 }
2865 )
2866 );
2867 }
2868 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2869 #auto_assigns_for_row
2870 }
2871 #audit_bulk_insert_emit
2872 ::core::result::Result::Ok(())
2873 }
2874 }
2875 } else {
2876 let cols_all = &fields.bulk_columns_all;
2877 let pushes_all = &fields.bulk_pushes_all;
2878 quote! {
2879 pub async fn bulk_insert(
2889 rows: &[Self],
2890 pool: &::rustango::sql::sqlx::PgPool,
2891 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2892 Self::bulk_insert_on(rows, pool).await
2893 }
2894
2895 pub async fn bulk_insert_on<'_c, _E>(
2901 rows: &[Self],
2902 _executor: _E,
2903 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2904 where
2905 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2906 {
2907 if rows.is_empty() {
2908 return ::core::result::Result::Ok(());
2909 }
2910 let mut _all_rows: ::std::vec::Vec<
2911 ::std::vec::Vec<::rustango::core::SqlValue>,
2912 > = ::std::vec::Vec::with_capacity(rows.len());
2913 for _row in rows.iter() {
2914 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2915 ::std::vec::Vec::new();
2916 #( #pushes_all )*
2917 _all_rows.push(_row_vals);
2918 }
2919 let _query = ::rustango::core::BulkInsertQuery {
2920 model: <Self as ::rustango::core::Model>::SCHEMA,
2921 columns: ::std::vec![ #( #cols_all ),* ],
2922 rows: _all_rows,
2923 returning: ::std::vec::Vec::new(),
2924 on_conflict: ::core::option::Option::None,
2925 };
2926 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2927 ::core::result::Result::Ok(())
2928 }
2929 }
2930 };
2931
2932 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2933 quote! {
2934 #[doc(hidden)]
2939 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2940 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2941 ::core::clone::Clone::clone(&self.#pk_ident)
2942 )
2943 }
2944 }
2945 });
2946
2947 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2948 quote! {
2949 impl ::rustango::sql::HasPkValue for #struct_name {
2950 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2951 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2952 ::core::clone::Clone::clone(&self.#pk_ident)
2953 )
2954 }
2955 }
2956 }
2957 });
2958
2959 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2960
2961 let assign_auto_pk_pool_impl = {
2967 let auto_assigns = &fields.auto_assigns;
2968 let auto_assigns_sqlite: Vec<TokenStream2> = fields
2974 .auto_field_idents
2975 .iter()
2976 .map(|(ident, column)| {
2977 quote! {
2978 self.#ident = ::rustango::sql::try_get_returning_sqlite(
2979 _returning_row, #column
2980 )?;
2981 }
2982 })
2983 .collect();
2984 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
2985 let value_ty = fields
3003 .first_auto_value_ty
3004 .as_ref()
3005 .expect("first_auto_value_ty set whenever first_auto_ident is");
3006 quote! {
3007 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
3008 ::rustango_from_mysql_auto_id(_id)?;
3009 self.#first = ::rustango::sql::Auto::Set(_converted);
3010 ::core::result::Result::Ok(())
3011 }
3012 } else {
3013 quote! {
3014 let _ = _id;
3015 ::core::result::Result::Ok(())
3016 }
3017 };
3018 quote! {
3019 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
3020 fn __rustango_assign_from_pg_row(
3021 &mut self,
3022 _returning_row: &::rustango::sql::PgReturningRow,
3023 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3024 #( #auto_assigns )*
3025 ::core::result::Result::Ok(())
3026 }
3027 fn __rustango_assign_from_mysql_id(
3028 &mut self,
3029 _id: i64,
3030 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3031 #mysql_body
3032 }
3033 fn __rustango_assign_from_sqlite_row(
3034 &mut self,
3035 _returning_row: &::rustango::sql::SqliteReturningRow,
3036 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3037 #( #auto_assigns_sqlite )*
3038 ::core::result::Result::Ok(())
3039 }
3040 }
3041 }
3042 };
3043
3044 let from_aliased_row_inits = &fields.from_aliased_row_inits;
3045 let aliased_row_helper = quote! {
3046 #[doc(hidden)]
3052 pub fn __rustango_from_aliased_row(
3053 row: &::rustango::sql::sqlx::postgres::PgRow,
3054 prefix: &str,
3055 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3056 ::core::result::Result::Ok(Self {
3057 #( #from_aliased_row_inits ),*
3058 })
3059 }
3060 };
3061 let aliased_row_helper_my = quote! {
3064 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
3065 #( #from_aliased_row_inits ),*
3066 });
3067 };
3068
3069 let aliased_row_helper_sqlite = quote! {
3072 ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
3073 #( #from_aliased_row_inits ),*
3074 });
3075 };
3076
3077 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
3078 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
3079 let load_related_impl_sqlite =
3080 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
3081
3082 quote! {
3083 impl #struct_name {
3084 #[must_use]
3086 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
3087 ::rustango::query::QuerySet::new()
3088 }
3089
3090 #insert_method
3091
3092 #bulk_insert_method
3093
3094 #save_method
3095
3096 #pk_methods
3097
3098 #pk_value_helper
3099
3100 #aliased_row_helper
3101
3102 #column_consts
3103 }
3104
3105 #aliased_row_helper_my
3106
3107 #aliased_row_helper_sqlite
3108
3109 #load_related_impl
3110
3111 #load_related_impl_my
3112
3113 #load_related_impl_sqlite
3114
3115 #has_pk_value_impl
3116
3117 #fk_pk_access_impl
3118
3119 #assign_auto_pk_pool_impl
3120 }
3121}
3122
3123fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
3127 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
3128 let col_lit = column.as_str();
3129 quote! {
3130 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
3131 _returning_row,
3132 #col_lit,
3133 )?;
3134 }
3135 });
3136 quote! { #( #lines )* }
3137}
3138
3139fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
3141 let lines = entries.iter().map(|e| {
3142 let ident = &e.ident;
3143 let col_ty = column_type_ident(ident);
3144 quote! {
3145 #[allow(non_upper_case_globals)]
3146 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
3147 }
3148 });
3149 quote! { #(#lines)* }
3150}
3151
3152fn column_module_tokens(
3155 module_ident: &syn::Ident,
3156 struct_name: &syn::Ident,
3157 entries: &[ColumnEntry],
3158) -> TokenStream2 {
3159 let items = entries.iter().map(|e| {
3160 let col_ty = column_type_ident(&e.ident);
3161 let value_ty = &e.value_ty;
3162 let name = &e.name;
3163 let column = &e.column;
3164 let field_type_tokens = &e.field_type_tokens;
3165 quote! {
3166 #[derive(::core::clone::Clone, ::core::marker::Copy)]
3167 pub struct #col_ty;
3168
3169 impl ::rustango::core::Column for #col_ty {
3170 type Model = super::#struct_name;
3171 type Value = #value_ty;
3172 const NAME: &'static str = #name;
3173 const COLUMN: &'static str = #column;
3174 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
3175 }
3176 }
3177 });
3178 quote! {
3179 #[doc(hidden)]
3180 #[allow(non_camel_case_types, non_snake_case)]
3181 pub mod #module_ident {
3182 #[allow(unused_imports)]
3187 use super::*;
3188 #(#items)*
3189 }
3190 }
3191}
3192
3193fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3194 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3195}
3196
3197fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3198 syn::Ident::new(
3199 &format!("__rustango_cols_{struct_name}"),
3200 struct_name.span(),
3201 )
3202}
3203
3204fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3205 quote! {
3216 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3217 for #struct_name
3218 {
3219 fn from_row(
3220 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3221 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3222 ::core::result::Result::Ok(Self {
3223 #( #from_row_inits ),*
3224 })
3225 }
3226 }
3227
3228 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3229 #( #from_row_inits ),*
3230 });
3231
3232 ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
3233 #( #from_row_inits ),*
3234 });
3235 }
3236}
3237
3238struct ContainerAttrs {
3239 table: Option<String>,
3240 display: Option<(String, proc_macro2::Span)>,
3241 app: Option<String>,
3248 admin: Option<AdminAttrs>,
3253 audit: Option<AuditAttrs>,
3259 permissions: bool,
3263 m2m: Vec<M2MAttr>,
3267 indexes: Vec<IndexAttr>,
3273 checks: Vec<CheckAttr>,
3276 composite_fks: Vec<CompositeFkAttr>,
3280 generic_fks: Vec<GenericFkAttr>,
3284 scope: Option<String>,
3290}
3291
3292struct IndexAttr {
3294 name: Option<String>,
3296 columns: Vec<String>,
3298 unique: bool,
3300}
3301
3302struct CheckAttr {
3304 name: String,
3305 expr: String,
3306}
3307
3308struct CompositeFkAttr {
3314 name: String,
3316 to: String,
3318 from: Vec<String>,
3320 on: Vec<String>,
3322}
3323
3324struct GenericFkAttr {
3329 name: String,
3331 ct_column: String,
3333 pk_column: String,
3335}
3336
3337struct M2MAttr {
3339 name: String,
3341 to: String,
3343 through: String,
3345 src: String,
3347 dst: String,
3349}
3350
3351#[derive(Default)]
3357struct AuditAttrs {
3358 track: Option<(Vec<String>, proc_macro2::Span)>,
3362}
3363
3364#[derive(Default)]
3369struct AdminAttrs {
3370 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3371 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3372 list_per_page: Option<usize>,
3373 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3374 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3375 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3376 actions: Option<(Vec<String>, proc_macro2::Span)>,
3379 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3383}
3384
3385fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3386 let mut out = ContainerAttrs {
3387 table: None,
3388 display: None,
3389 app: None,
3390 admin: None,
3391 audit: None,
3392 permissions: true,
3401 m2m: Vec::new(),
3402 indexes: Vec::new(),
3403 checks: Vec::new(),
3404 composite_fks: Vec::new(),
3405 generic_fks: Vec::new(),
3406 scope: None,
3407 };
3408 for attr in &input.attrs {
3409 if !attr.path().is_ident("rustango") {
3410 continue;
3411 }
3412 attr.parse_nested_meta(|meta| {
3413 if meta.path.is_ident("table") {
3414 let s: LitStr = meta.value()?.parse()?;
3415 let name = s.value();
3416 validate_table_name(&name, s.span())?;
3426 out.table = Some(name);
3427 return Ok(());
3428 }
3429 if meta.path.is_ident("display") {
3430 let s: LitStr = meta.value()?.parse()?;
3431 out.display = Some((s.value(), s.span()));
3432 return Ok(());
3433 }
3434 if meta.path.is_ident("app") {
3435 let s: LitStr = meta.value()?.parse()?;
3436 out.app = Some(s.value());
3437 return Ok(());
3438 }
3439 if meta.path.is_ident("scope") {
3440 let s: LitStr = meta.value()?.parse()?;
3441 let val = s.value();
3442 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
3443 return Err(meta.error(format!(
3444 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
3445 )));
3446 }
3447 out.scope = Some(val);
3448 return Ok(());
3449 }
3450 if meta.path.is_ident("admin") {
3451 let mut admin = AdminAttrs::default();
3452 meta.parse_nested_meta(|inner| {
3453 if inner.path.is_ident("list_display") {
3454 let s: LitStr = inner.value()?.parse()?;
3455 admin.list_display =
3456 Some((split_field_list(&s.value()), s.span()));
3457 return Ok(());
3458 }
3459 if inner.path.is_ident("search_fields") {
3460 let s: LitStr = inner.value()?.parse()?;
3461 admin.search_fields =
3462 Some((split_field_list(&s.value()), s.span()));
3463 return Ok(());
3464 }
3465 if inner.path.is_ident("readonly_fields") {
3466 let s: LitStr = inner.value()?.parse()?;
3467 admin.readonly_fields =
3468 Some((split_field_list(&s.value()), s.span()));
3469 return Ok(());
3470 }
3471 if inner.path.is_ident("list_per_page") {
3472 let lit: syn::LitInt = inner.value()?.parse()?;
3473 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3474 return Ok(());
3475 }
3476 if inner.path.is_ident("ordering") {
3477 let s: LitStr = inner.value()?.parse()?;
3478 admin.ordering = Some((
3479 parse_ordering_list(&s.value()),
3480 s.span(),
3481 ));
3482 return Ok(());
3483 }
3484 if inner.path.is_ident("list_filter") {
3485 let s: LitStr = inner.value()?.parse()?;
3486 admin.list_filter =
3487 Some((split_field_list(&s.value()), s.span()));
3488 return Ok(());
3489 }
3490 if inner.path.is_ident("actions") {
3491 let s: LitStr = inner.value()?.parse()?;
3492 admin.actions =
3493 Some((split_field_list(&s.value()), s.span()));
3494 return Ok(());
3495 }
3496 if inner.path.is_ident("fieldsets") {
3497 let s: LitStr = inner.value()?.parse()?;
3498 admin.fieldsets =
3499 Some((parse_fieldset_list(&s.value()), s.span()));
3500 return Ok(());
3501 }
3502 Err(inner.error(
3503 "unknown admin attribute (supported: \
3504 `list_display`, `search_fields`, `readonly_fields`, \
3505 `list_filter`, `list_per_page`, `ordering`, `actions`, \
3506 `fieldsets`)",
3507 ))
3508 })?;
3509 out.admin = Some(admin);
3510 return Ok(());
3511 }
3512 if meta.path.is_ident("audit") {
3513 let mut audit = AuditAttrs::default();
3514 meta.parse_nested_meta(|inner| {
3515 if inner.path.is_ident("track") {
3516 let s: LitStr = inner.value()?.parse()?;
3517 audit.track =
3518 Some((split_field_list(&s.value()), s.span()));
3519 return Ok(());
3520 }
3521 Err(inner.error(
3522 "unknown audit attribute (supported: `track`)",
3523 ))
3524 })?;
3525 out.audit = Some(audit);
3526 return Ok(());
3527 }
3528 if meta.path.is_ident("permissions") {
3529 if let Ok(v) = meta.value() {
3534 let lit: syn::LitBool = v.parse()?;
3535 out.permissions = lit.value;
3536 } else {
3537 out.permissions = true;
3538 }
3539 return Ok(());
3540 }
3541 if meta.path.is_ident("unique_together") {
3542 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3551 out.indexes.push(IndexAttr { name, columns, unique: true });
3552 return Ok(());
3553 }
3554 if meta.path.is_ident("index_together") {
3555 let (columns, name) = parse_together_attr(&meta, "index_together")?;
3561 out.indexes.push(IndexAttr { name, columns, unique: false });
3562 return Ok(());
3563 }
3564 if meta.path.is_ident("index") {
3565 let cols_lit: LitStr = meta.value()?.parse()?;
3573 let columns = split_field_list(&cols_lit.value());
3574 out.indexes.push(IndexAttr { name: None, columns, unique: false });
3575 return Ok(());
3576 }
3577 if meta.path.is_ident("check") {
3578 let mut name: Option<String> = None;
3580 let mut expr: Option<String> = None;
3581 meta.parse_nested_meta(|inner| {
3582 if inner.path.is_ident("name") {
3583 let s: LitStr = inner.value()?.parse()?;
3584 name = Some(s.value());
3585 return Ok(());
3586 }
3587 if inner.path.is_ident("expr") {
3588 let s: LitStr = inner.value()?.parse()?;
3589 expr = Some(s.value());
3590 return Ok(());
3591 }
3592 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3593 })?;
3594 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3595 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3596 out.checks.push(CheckAttr { name, expr });
3597 return Ok(());
3598 }
3599 if meta.path.is_ident("generic_fk") {
3600 let mut gfk = GenericFkAttr {
3601 name: String::new(),
3602 ct_column: String::new(),
3603 pk_column: String::new(),
3604 };
3605 meta.parse_nested_meta(|inner| {
3606 if inner.path.is_ident("name") {
3607 let s: LitStr = inner.value()?.parse()?;
3608 gfk.name = s.value();
3609 return Ok(());
3610 }
3611 if inner.path.is_ident("ct_column") {
3612 let s: LitStr = inner.value()?.parse()?;
3613 gfk.ct_column = s.value();
3614 return Ok(());
3615 }
3616 if inner.path.is_ident("pk_column") {
3617 let s: LitStr = inner.value()?.parse()?;
3618 gfk.pk_column = s.value();
3619 return Ok(());
3620 }
3621 Err(inner.error(
3622 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3623 ))
3624 })?;
3625 if gfk.name.is_empty() {
3626 return Err(meta.error("generic_fk requires `name = \"...\"`"));
3627 }
3628 if gfk.ct_column.is_empty() {
3629 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3630 }
3631 if gfk.pk_column.is_empty() {
3632 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3633 }
3634 out.generic_fks.push(gfk);
3635 return Ok(());
3636 }
3637 if meta.path.is_ident("fk_composite") {
3638 let mut fk = CompositeFkAttr {
3639 name: String::new(),
3640 to: String::new(),
3641 from: Vec::new(),
3642 on: Vec::new(),
3643 };
3644 meta.parse_nested_meta(|inner| {
3645 if inner.path.is_ident("name") {
3646 let s: LitStr = inner.value()?.parse()?;
3647 fk.name = s.value();
3648 return Ok(());
3649 }
3650 if inner.path.is_ident("to") {
3651 let s: LitStr = inner.value()?.parse()?;
3652 fk.to = s.value();
3653 return Ok(());
3654 }
3655 if inner.path.is_ident("on") || inner.path.is_ident("from") {
3658 let value = inner.value()?;
3659 let content;
3660 syn::parenthesized!(content in value);
3661 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3662 content.parse_terminated(
3663 |p| p.parse::<syn::LitStr>(),
3664 syn::Token![,],
3665 )?;
3666 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3667 if inner.path.is_ident("on") {
3668 fk.on = cols;
3669 } else {
3670 fk.from = cols;
3671 }
3672 return Ok(());
3673 }
3674 Err(inner.error(
3675 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3676 ))
3677 })?;
3678 if fk.name.is_empty() {
3679 return Err(meta.error("fk_composite requires `name = \"...\"`"));
3680 }
3681 if fk.to.is_empty() {
3682 return Err(meta.error("fk_composite requires `to = \"...\"`"));
3683 }
3684 if fk.from.is_empty() || fk.on.is_empty() {
3685 return Err(meta.error(
3686 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3687 ));
3688 }
3689 if fk.from.len() != fk.on.len() {
3690 return Err(meta.error(format!(
3691 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3692 fk.from.len(),
3693 fk.on.len(),
3694 )));
3695 }
3696 out.composite_fks.push(fk);
3697 return Ok(());
3698 }
3699 if meta.path.is_ident("m2m") {
3700 let mut m2m = M2MAttr {
3701 name: String::new(),
3702 to: String::new(),
3703 through: String::new(),
3704 src: String::new(),
3705 dst: String::new(),
3706 };
3707 meta.parse_nested_meta(|inner| {
3708 if inner.path.is_ident("name") {
3709 let s: LitStr = inner.value()?.parse()?;
3710 m2m.name = s.value();
3711 return Ok(());
3712 }
3713 if inner.path.is_ident("to") {
3714 let s: LitStr = inner.value()?.parse()?;
3715 m2m.to = s.value();
3716 return Ok(());
3717 }
3718 if inner.path.is_ident("through") {
3719 let s: LitStr = inner.value()?.parse()?;
3720 m2m.through = s.value();
3721 return Ok(());
3722 }
3723 if inner.path.is_ident("src") {
3724 let s: LitStr = inner.value()?.parse()?;
3725 m2m.src = s.value();
3726 return Ok(());
3727 }
3728 if inner.path.is_ident("dst") {
3729 let s: LitStr = inner.value()?.parse()?;
3730 m2m.dst = s.value();
3731 return Ok(());
3732 }
3733 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3734 })?;
3735 if m2m.name.is_empty() {
3736 return Err(meta.error("m2m requires `name = \"...\"`"));
3737 }
3738 if m2m.to.is_empty() {
3739 return Err(meta.error("m2m requires `to = \"...\"`"));
3740 }
3741 if m2m.through.is_empty() {
3742 return Err(meta.error("m2m requires `through = \"...\"`"));
3743 }
3744 if m2m.src.is_empty() {
3745 return Err(meta.error("m2m requires `src = \"...\"`"));
3746 }
3747 if m2m.dst.is_empty() {
3748 return Err(meta.error("m2m requires `dst = \"...\"`"));
3749 }
3750 out.m2m.push(m2m);
3751 return Ok(());
3752 }
3753 Err(meta.error("unknown rustango container attribute"))
3754 })?;
3755 }
3756 Ok(out)
3757}
3758
3759fn split_field_list(raw: &str) -> Vec<String> {
3763 raw.split(',')
3764 .map(str::trim)
3765 .filter(|s| !s.is_empty())
3766 .map(str::to_owned)
3767 .collect()
3768}
3769
3770fn parse_together_attr(
3778 meta: &syn::meta::ParseNestedMeta<'_>,
3779 attr: &str,
3780) -> syn::Result<(Vec<String>, Option<String>)> {
3781 if meta.input.peek(syn::Token![=]) {
3784 let cols_lit: LitStr = meta.value()?.parse()?;
3785 let columns = split_field_list(&cols_lit.value());
3786 check_together_columns(meta, attr, &columns)?;
3787 return Ok((columns, None));
3788 }
3789 let mut columns: Option<Vec<String>> = None;
3790 let mut name: Option<String> = None;
3791 meta.parse_nested_meta(|inner| {
3792 if inner.path.is_ident("columns") {
3793 let s: LitStr = inner.value()?.parse()?;
3794 columns = Some(split_field_list(&s.value()));
3795 return Ok(());
3796 }
3797 if inner.path.is_ident("name") {
3798 let s: LitStr = inner.value()?.parse()?;
3799 name = Some(s.value());
3800 return Ok(());
3801 }
3802 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3803 })?;
3804 let columns = columns.ok_or_else(|| {
3805 meta.error(format!(
3806 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3807 ))
3808 })?;
3809 check_together_columns(meta, attr, &columns)?;
3810 Ok((columns, name))
3811}
3812
3813fn check_together_columns(
3814 meta: &syn::meta::ParseNestedMeta<'_>,
3815 attr: &str,
3816 columns: &[String],
3817) -> syn::Result<()> {
3818 if columns.len() < 2 {
3819 let single = if attr == "unique_together" {
3820 "#[rustango(unique)] on the field"
3821 } else {
3822 "#[rustango(index)] on the field"
3823 };
3824 return Err(meta.error(format!(
3825 "{attr} expects two or more columns; for a single-column equivalent use {single}",
3826 )));
3827 }
3828 Ok(())
3829}
3830
3831fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3840 raw.split('|')
3841 .map(str::trim)
3842 .filter(|s| !s.is_empty())
3843 .map(|section| {
3844 let (title, rest) = match section.split_once(':') {
3846 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
3847 _ => (String::new(), section),
3848 };
3849 let fields = split_field_list(rest);
3850 (title, fields)
3851 })
3852 .collect()
3853}
3854
3855fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3858 raw.split(',')
3859 .map(str::trim)
3860 .filter(|s| !s.is_empty())
3861 .map(|spec| {
3862 spec.strip_prefix('-')
3863 .map_or((spec.to_owned(), false), |rest| {
3864 (rest.trim().to_owned(), true)
3865 })
3866 })
3867 .collect()
3868}
3869
3870struct FieldAttrs {
3871 column: Option<String>,
3872 primary_key: bool,
3873 fk: Option<String>,
3874 o2o: Option<String>,
3875 on: Option<String>,
3876 max_length: Option<u32>,
3877 min: Option<i64>,
3878 max: Option<i64>,
3879 default: Option<String>,
3880 auto_uuid: bool,
3886 auto_now_add: bool,
3891 auto_now: bool,
3897 soft_delete: bool,
3902 unique: bool,
3905 index: bool,
3909 index_unique: bool,
3910 index_name: Option<String>,
3911 generated_as: Option<String>,
3917}
3918
3919fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3920 let mut out = FieldAttrs {
3921 column: None,
3922 primary_key: false,
3923 fk: None,
3924 o2o: None,
3925 on: None,
3926 max_length: None,
3927 min: None,
3928 max: None,
3929 default: None,
3930 auto_uuid: false,
3931 auto_now_add: false,
3932 auto_now: false,
3933 soft_delete: false,
3934 unique: false,
3935 index: false,
3936 index_unique: false,
3937 index_name: None,
3938 generated_as: None,
3939 };
3940 for attr in &field.attrs {
3941 if !attr.path().is_ident("rustango") {
3942 continue;
3943 }
3944 attr.parse_nested_meta(|meta| {
3945 if meta.path.is_ident("column") {
3946 let s: LitStr = meta.value()?.parse()?;
3947 let name = s.value();
3948 validate_sql_identifier(&name, "column", s.span())?;
3949 out.column = Some(name);
3950 return Ok(());
3951 }
3952 if meta.path.is_ident("primary_key") {
3953 out.primary_key = true;
3954 return Ok(());
3955 }
3956 if meta.path.is_ident("fk") {
3957 let s: LitStr = meta.value()?.parse()?;
3958 out.fk = Some(s.value());
3959 return Ok(());
3960 }
3961 if meta.path.is_ident("o2o") {
3962 let s: LitStr = meta.value()?.parse()?;
3963 out.o2o = Some(s.value());
3964 return Ok(());
3965 }
3966 if meta.path.is_ident("on") {
3967 let s: LitStr = meta.value()?.parse()?;
3968 out.on = Some(s.value());
3969 return Ok(());
3970 }
3971 if meta.path.is_ident("max_length") {
3972 let lit: syn::LitInt = meta.value()?.parse()?;
3973 out.max_length = Some(lit.base10_parse::<u32>()?);
3974 return Ok(());
3975 }
3976 if meta.path.is_ident("min") {
3977 out.min = Some(parse_signed_i64(&meta)?);
3978 return Ok(());
3979 }
3980 if meta.path.is_ident("max") {
3981 out.max = Some(parse_signed_i64(&meta)?);
3982 return Ok(());
3983 }
3984 if meta.path.is_ident("default") {
3985 let s: LitStr = meta.value()?.parse()?;
3986 out.default = Some(s.value());
3987 return Ok(());
3988 }
3989 if meta.path.is_ident("generated_as") {
3990 let s: LitStr = meta.value()?.parse()?;
3991 out.generated_as = Some(s.value());
3992 return Ok(());
3993 }
3994 if meta.path.is_ident("auto_uuid") {
3995 out.auto_uuid = true;
3996 out.primary_key = true;
4000 if out.default.is_none() {
4001 out.default = Some("gen_random_uuid()".into());
4002 }
4003 return Ok(());
4004 }
4005 if meta.path.is_ident("auto_now_add") {
4006 out.auto_now_add = true;
4007 if out.default.is_none() {
4008 out.default = Some("now()".into());
4009 }
4010 return Ok(());
4011 }
4012 if meta.path.is_ident("auto_now") {
4013 out.auto_now = true;
4014 if out.default.is_none() {
4015 out.default = Some("now()".into());
4016 }
4017 return Ok(());
4018 }
4019 if meta.path.is_ident("soft_delete") {
4020 out.soft_delete = true;
4021 return Ok(());
4022 }
4023 if meta.path.is_ident("unique") {
4024 out.unique = true;
4025 return Ok(());
4026 }
4027 if meta.path.is_ident("index") {
4028 out.index = true;
4029 if meta.input.peek(syn::token::Paren) {
4031 meta.parse_nested_meta(|inner| {
4032 if inner.path.is_ident("unique") {
4033 out.index_unique = true;
4034 return Ok(());
4035 }
4036 if inner.path.is_ident("name") {
4037 let s: LitStr = inner.value()?.parse()?;
4038 out.index_name = Some(s.value());
4039 return Ok(());
4040 }
4041 Err(inner
4042 .error("unknown index sub-attribute (supported: `unique`, `name`)"))
4043 })?;
4044 }
4045 return Ok(());
4046 }
4047 Err(meta.error("unknown rustango field attribute"))
4048 })?;
4049 }
4050 Ok(out)
4051}
4052
4053fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
4055 let expr: syn::Expr = meta.value()?.parse()?;
4056 match expr {
4057 syn::Expr::Lit(syn::ExprLit {
4058 lit: syn::Lit::Int(lit),
4059 ..
4060 }) => lit.base10_parse::<i64>(),
4061 syn::Expr::Unary(syn::ExprUnary {
4062 op: syn::UnOp::Neg(_),
4063 expr,
4064 ..
4065 }) => {
4066 if let syn::Expr::Lit(syn::ExprLit {
4067 lit: syn::Lit::Int(lit),
4068 ..
4069 }) = *expr
4070 {
4071 let v: i64 = lit.base10_parse()?;
4072 Ok(-v)
4073 } else {
4074 Err(syn::Error::new_spanned(expr, "expected integer literal"))
4075 }
4076 }
4077 other => Err(syn::Error::new_spanned(
4078 other,
4079 "expected integer literal (signed)",
4080 )),
4081 }
4082}
4083
4084struct FieldInfo<'a> {
4085 ident: &'a syn::Ident,
4086 column: String,
4087 primary_key: bool,
4088 auto: bool,
4092 value_ty: &'a Type,
4095 field_type_tokens: TokenStream2,
4097 schema: TokenStream2,
4098 from_row_init: TokenStream2,
4099 from_aliased_row_init: TokenStream2,
4105 fk_inner: Option<Type>,
4109 fk_pk_kind: DetectedKind,
4115 nullable: bool,
4123 auto_now: bool,
4129 auto_now_add: bool,
4135 soft_delete: bool,
4140 generated_as: Option<String>,
4145}
4146
4147fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
4161 validate_sql_identifier(name, "table", span)
4162}
4163
4164fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
4169 if name.is_empty() {
4170 return Err(syn::Error::new(
4171 span,
4172 format!("`{kind} = \"\"` is not a valid SQL identifier"),
4173 ));
4174 }
4175 let mut chars = name.chars();
4176 let first = chars.next().unwrap();
4177 if !(first.is_ascii_alphabetic() || first == '_') {
4178 return Err(syn::Error::new(
4179 span,
4180 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
4181 ));
4182 }
4183 for c in chars {
4184 if !(c.is_ascii_alphanumeric() || c == '_') {
4185 return Err(syn::Error::new(
4186 span,
4187 format!(
4188 "{kind} name `{name}` contains invalid character {c:?} — \
4189 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
4190 Hyphens in particular break FK / index name derivation \
4191 downstream; use underscores instead (e.g. `{}`)",
4192 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
4193 ),
4194 ));
4195 }
4196 }
4197 Ok(())
4198}
4199
4200fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
4201 let attrs = parse_field_attrs(field)?;
4202 let ident = field
4203 .ident
4204 .as_ref()
4205 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4206 let name = ident.to_string();
4207 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
4208 let primary_key = attrs.primary_key;
4209 let DetectedType {
4210 kind,
4211 nullable,
4212 auto: detected_auto,
4213 fk_inner,
4214 } = detect_type(&field.ty)?;
4215 check_bound_compatibility(field, &attrs, kind)?;
4216 let auto = detected_auto;
4217 if attrs.auto_uuid {
4223 if kind != DetectedKind::Uuid {
4224 return Err(syn::Error::new_spanned(
4225 field,
4226 "`#[rustango(auto_uuid)]` requires the field type to be \
4227 `Auto<uuid::Uuid>`",
4228 ));
4229 }
4230 if !detected_auto {
4231 return Err(syn::Error::new_spanned(
4232 field,
4233 "`#[rustango(auto_uuid)]` requires the field type to be \
4234 wrapped in `Auto<...>` so the macro skips the column on \
4235 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
4236 ));
4237 }
4238 }
4239 if attrs.auto_now_add || attrs.auto_now {
4240 if kind != DetectedKind::DateTime {
4241 return Err(syn::Error::new_spanned(
4242 field,
4243 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4244 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
4245 ));
4246 }
4247 if !detected_auto {
4248 return Err(syn::Error::new_spanned(
4249 field,
4250 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4251 the field type to be wrapped in `Auto<...>` so the macro skips \
4252 the column on INSERT and the DB DEFAULT (`now()`) fires",
4253 ));
4254 }
4255 }
4256 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
4257 return Err(syn::Error::new_spanned(
4258 field,
4259 "`#[rustango(soft_delete)]` requires the field type to be \
4260 `Option<chrono::DateTime<chrono::Utc>>`",
4261 ));
4262 }
4263 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
4264 if detected_auto && !primary_key && !is_mixin_auto {
4265 return Err(syn::Error::new_spanned(
4266 field,
4267 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
4268 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
4269 `auto_now`",
4270 ));
4271 }
4272 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
4273 return Err(syn::Error::new_spanned(
4274 field,
4275 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
4276 SERIAL / BIGSERIAL already supplies a default sequence.",
4277 ));
4278 }
4279 if fk_inner.is_some() && primary_key {
4280 return Err(syn::Error::new_spanned(
4281 field,
4282 "`ForeignKey<T>` is not allowed on a primary-key field — \
4283 a row's PK is its own identity, not a reference to a parent.",
4284 ));
4285 }
4286 if attrs.generated_as.is_some() {
4287 if primary_key {
4288 return Err(syn::Error::new_spanned(
4289 field,
4290 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4291 primary-key field — a PK must be writable so the row \
4292 has an identity at INSERT time.",
4293 ));
4294 }
4295 if attrs.default.is_some() {
4296 return Err(syn::Error::new_spanned(
4297 field,
4298 "`#[rustango(generated_as = \"…\")]` cannot combine with \
4299 `default = \"…\"` — Postgres rejects DEFAULT on \
4300 generated columns. The expression IS the default.",
4301 ));
4302 }
4303 if detected_auto {
4304 return Err(syn::Error::new_spanned(
4305 field,
4306 "`#[rustango(generated_as = \"…\")]` is not allowed on \
4307 an `Auto<T>` field — generated columns are computed \
4308 by the DB, not server-assigned via a sequence. Use a \
4309 plain Rust type (e.g. `f64`).",
4310 ));
4311 }
4312 if fk_inner.is_some() {
4313 return Err(syn::Error::new_spanned(
4314 field,
4315 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4316 ForeignKey field.",
4317 ));
4318 }
4319 }
4320 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
4321 let column_lit = column.as_str();
4322 let field_type_tokens = kind.variant_tokens();
4323 let max_length = optional_u32(attrs.max_length);
4324 let min = optional_i64(attrs.min);
4325 let max = optional_i64(attrs.max);
4326 let default = optional_str(attrs.default.as_deref());
4327
4328 let unique = attrs.unique;
4329 let generated_as = optional_str(attrs.generated_as.as_deref());
4330 let schema = quote! {
4331 ::rustango::core::FieldSchema {
4332 name: #name,
4333 column: #column_lit,
4334 ty: #field_type_tokens,
4335 nullable: #nullable,
4336 primary_key: #primary_key,
4337 relation: #relation,
4338 max_length: #max_length,
4339 min: #min,
4340 max: #max,
4341 default: #default,
4342 auto: #auto,
4343 unique: #unique,
4344 generated_as: #generated_as,
4345 }
4346 };
4347
4348 let from_row_init = quote! {
4349 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4350 };
4351 let from_aliased_row_init = quote! {
4352 #ident: ::rustango::sql::sqlx::Row::try_get(
4353 row,
4354 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4355 )?
4356 };
4357
4358 Ok(FieldInfo {
4359 ident,
4360 column,
4361 primary_key,
4362 auto,
4363 value_ty: &field.ty,
4364 field_type_tokens,
4365 schema,
4366 from_row_init,
4367 from_aliased_row_init,
4368 fk_inner: fk_inner.cloned(),
4369 fk_pk_kind: kind,
4370 nullable,
4371 auto_now: attrs.auto_now,
4372 auto_now_add: attrs.auto_now_add,
4373 soft_delete: attrs.soft_delete,
4374 generated_as: attrs.generated_as.clone(),
4375 })
4376}
4377
4378fn check_bound_compatibility(
4379 field: &syn::Field,
4380 attrs: &FieldAttrs,
4381 kind: DetectedKind,
4382) -> syn::Result<()> {
4383 if attrs.max_length.is_some() && kind != DetectedKind::String {
4384 return Err(syn::Error::new_spanned(
4385 field,
4386 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4387 ));
4388 }
4389 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4390 return Err(syn::Error::new_spanned(
4391 field,
4392 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4393 ));
4394 }
4395 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4396 if min > max {
4397 return Err(syn::Error::new_spanned(
4398 field,
4399 format!("`min` ({min}) is greater than `max` ({max})"),
4400 ));
4401 }
4402 }
4403 Ok(())
4404}
4405
4406fn optional_u32(value: Option<u32>) -> TokenStream2 {
4407 if let Some(v) = value {
4408 quote!(::core::option::Option::Some(#v))
4409 } else {
4410 quote!(::core::option::Option::None)
4411 }
4412}
4413
4414fn optional_i64(value: Option<i64>) -> TokenStream2 {
4415 if let Some(v) = value {
4416 quote!(::core::option::Option::Some(#v))
4417 } else {
4418 quote!(::core::option::Option::None)
4419 }
4420}
4421
4422fn optional_str(value: Option<&str>) -> TokenStream2 {
4423 if let Some(v) = value {
4424 quote!(::core::option::Option::Some(#v))
4425 } else {
4426 quote!(::core::option::Option::None)
4427 }
4428}
4429
4430fn relation_tokens(
4431 field: &syn::Field,
4432 attrs: &FieldAttrs,
4433 fk_inner: Option<&syn::Type>,
4434 table: &str,
4435) -> syn::Result<TokenStream2> {
4436 if let Some(inner) = fk_inner {
4437 if attrs.fk.is_some() || attrs.o2o.is_some() {
4438 return Err(syn::Error::new_spanned(
4439 field,
4440 "`ForeignKey<T>` already declares the FK target via the type parameter — \
4441 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4442 ));
4443 }
4444 let on = attrs.on.as_deref().unwrap_or("id");
4445 return Ok(quote! {
4446 ::core::option::Option::Some(::rustango::core::Relation::Fk {
4447 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4448 on: #on,
4449 })
4450 });
4451 }
4452 match (&attrs.fk, &attrs.o2o) {
4453 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4454 field,
4455 "`fk` and `o2o` are mutually exclusive",
4456 )),
4457 (Some(to), None) => {
4458 let on = attrs.on.as_deref().unwrap_or("id");
4459 let resolved = if to == "self" { table } else { to };
4465 Ok(quote! {
4466 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4467 })
4468 }
4469 (None, Some(to)) => {
4470 let on = attrs.on.as_deref().unwrap_or("id");
4471 let resolved = if to == "self" { table } else { to };
4472 Ok(quote! {
4473 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4474 })
4475 }
4476 (None, None) => {
4477 if attrs.on.is_some() {
4478 return Err(syn::Error::new_spanned(
4479 field,
4480 "`on` requires `fk` or `o2o`",
4481 ));
4482 }
4483 Ok(quote!(::core::option::Option::None))
4484 }
4485 }
4486}
4487
4488#[derive(Clone, Copy, PartialEq, Eq)]
4492enum DetectedKind {
4493 I16,
4494 I32,
4495 I64,
4496 F32,
4497 F64,
4498 Bool,
4499 String,
4500 DateTime,
4501 Date,
4502 Uuid,
4503 Json,
4504}
4505
4506impl DetectedKind {
4507 fn variant_tokens(self) -> TokenStream2 {
4508 match self {
4509 Self::I16 => quote!(::rustango::core::FieldType::I16),
4510 Self::I32 => quote!(::rustango::core::FieldType::I32),
4511 Self::I64 => quote!(::rustango::core::FieldType::I64),
4512 Self::F32 => quote!(::rustango::core::FieldType::F32),
4513 Self::F64 => quote!(::rustango::core::FieldType::F64),
4514 Self::Bool => quote!(::rustango::core::FieldType::Bool),
4515 Self::String => quote!(::rustango::core::FieldType::String),
4516 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4517 Self::Date => quote!(::rustango::core::FieldType::Date),
4518 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4519 Self::Json => quote!(::rustango::core::FieldType::Json),
4520 }
4521 }
4522
4523 fn is_integer(self) -> bool {
4524 matches!(self, Self::I16 | Self::I32 | Self::I64)
4525 }
4526
4527 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4535 match self {
4536 Self::I16 => (quote!(I16), quote!(0i16)),
4537 Self::I32 => (quote!(I32), quote!(0i32)),
4538 Self::I64 => (quote!(I64), quote!(0i64)),
4539 Self::F32 => (quote!(F32), quote!(0f32)),
4540 Self::F64 => (quote!(F64), quote!(0f64)),
4541 Self::Bool => (quote!(Bool), quote!(false)),
4542 Self::String => (quote!(String), quote!(::std::string::String::new())),
4543 Self::DateTime => (
4544 quote!(DateTime),
4545 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4546 ),
4547 Self::Date => (
4548 quote!(Date),
4549 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4550 ),
4551 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4552 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4553 }
4554 }
4555}
4556
4557#[derive(Clone, Copy)]
4563struct DetectedType<'a> {
4564 kind: DetectedKind,
4565 nullable: bool,
4566 auto: bool,
4567 fk_inner: Option<&'a syn::Type>,
4568}
4569
4570fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4575 let Type::Path(TypePath { path, qself: None }) = ty else {
4576 return None;
4577 };
4578 let last = path.segments.last()?;
4579 if last.ident != "Auto" {
4580 return None;
4581 }
4582 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4583 return None;
4584 };
4585 args.args.iter().find_map(|a| match a {
4586 syn::GenericArgument::Type(t) => Some(t),
4587 _ => None,
4588 })
4589}
4590
4591fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4592 let Type::Path(TypePath { path, qself: None }) = ty else {
4593 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4594 };
4595 let last = path
4596 .segments
4597 .last()
4598 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4599
4600 if last.ident == "Option" {
4601 let inner = generic_inner(ty, &last.arguments, "Option")?;
4602 let inner_det = detect_type(inner)?;
4603 if inner_det.nullable {
4604 return Err(syn::Error::new_spanned(
4605 ty,
4606 "nested Option is not supported",
4607 ));
4608 }
4609 if inner_det.auto {
4610 return Err(syn::Error::new_spanned(
4611 ty,
4612 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4613 ));
4614 }
4615 return Ok(DetectedType {
4616 nullable: true,
4617 ..inner_det
4618 });
4619 }
4620
4621 if last.ident == "Auto" {
4622 let inner = generic_inner(ty, &last.arguments, "Auto")?;
4623 let inner_det = detect_type(inner)?;
4624 if inner_det.auto {
4625 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
4626 }
4627 if inner_det.nullable {
4628 return Err(syn::Error::new_spanned(
4629 ty,
4630 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4631 ));
4632 }
4633 if inner_det.fk_inner.is_some() {
4634 return Err(syn::Error::new_spanned(
4635 ty,
4636 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4637 ));
4638 }
4639 if !matches!(
4640 inner_det.kind,
4641 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4642 ) {
4643 return Err(syn::Error::new_spanned(
4644 ty,
4645 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4646 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4647 (DEFAULT now())",
4648 ));
4649 }
4650 return Ok(DetectedType {
4651 auto: true,
4652 ..inner_det
4653 });
4654 }
4655
4656 if last.ident == "ForeignKey" {
4657 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4658 let kind = match key_ty {
4666 Some(k) => detect_type(k)?.kind,
4667 None => DetectedKind::I64,
4668 };
4669 return Ok(DetectedType {
4670 kind,
4671 nullable: false,
4672 auto: false,
4673 fk_inner: Some(inner),
4674 });
4675 }
4676
4677 let kind = match last.ident.to_string().as_str() {
4678 "i16" => DetectedKind::I16,
4679 "i32" => DetectedKind::I32,
4680 "i64" => DetectedKind::I64,
4681 "f32" => DetectedKind::F32,
4682 "f64" => DetectedKind::F64,
4683 "bool" => DetectedKind::Bool,
4684 "String" => DetectedKind::String,
4685 "DateTime" => DetectedKind::DateTime,
4686 "NaiveDate" => DetectedKind::Date,
4687 "Uuid" => DetectedKind::Uuid,
4688 "Value" => DetectedKind::Json,
4689 other => {
4690 return Err(syn::Error::new_spanned(
4691 ty,
4692 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)"),
4693 ));
4694 }
4695 };
4696 Ok(DetectedType {
4697 kind,
4698 nullable: false,
4699 auto: false,
4700 fk_inner: None,
4701 })
4702}
4703
4704fn generic_inner<'a>(
4705 ty: &'a Type,
4706 arguments: &'a PathArguments,
4707 wrapper: &str,
4708) -> syn::Result<&'a Type> {
4709 let PathArguments::AngleBracketed(args) = arguments else {
4710 return Err(syn::Error::new_spanned(
4711 ty,
4712 format!("{wrapper} requires a generic argument"),
4713 ));
4714 };
4715 args.args
4716 .iter()
4717 .find_map(|a| match a {
4718 GenericArgument::Type(t) => Some(t),
4719 _ => None,
4720 })
4721 .ok_or_else(|| {
4722 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4723 })
4724}
4725
4726fn generic_pair<'a>(
4730 ty: &'a Type,
4731 arguments: &'a PathArguments,
4732 wrapper: &str,
4733) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4734 let PathArguments::AngleBracketed(args) = arguments else {
4735 return Err(syn::Error::new_spanned(
4736 ty,
4737 format!("{wrapper} requires a generic argument"),
4738 ));
4739 };
4740 let mut types = args.args.iter().filter_map(|a| match a {
4741 GenericArgument::Type(t) => Some(t),
4742 _ => None,
4743 });
4744 let first = types.next().ok_or_else(|| {
4745 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4746 })?;
4747 let second = types.next();
4748 Ok((first, second))
4749}
4750
4751fn to_snake_case(s: &str) -> String {
4752 let mut out = String::with_capacity(s.len() + 4);
4753 for (i, ch) in s.chars().enumerate() {
4754 if ch.is_ascii_uppercase() {
4755 if i > 0 {
4756 out.push('_');
4757 }
4758 out.push(ch.to_ascii_lowercase());
4759 } else {
4760 out.push(ch);
4761 }
4762 }
4763 out
4764}
4765
4766#[derive(Default)]
4772struct FormFieldAttrs {
4773 min: Option<i64>,
4774 max: Option<i64>,
4775 min_length: Option<u32>,
4776 max_length: Option<u32>,
4777}
4778
4779#[derive(Clone, Copy)]
4781enum FormFieldKind {
4782 String,
4783 I16,
4784 I32,
4785 I64,
4786 F32,
4787 F64,
4788 Bool,
4789}
4790
4791impl FormFieldKind {
4792 fn parse_method(self) -> &'static str {
4793 match self {
4794 Self::I16 => "i16",
4795 Self::I32 => "i32",
4796 Self::I64 => "i64",
4797 Self::F32 => "f32",
4798 Self::F64 => "f64",
4799 Self::String | Self::Bool => "",
4802 }
4803 }
4804}
4805
4806fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4807 let struct_name = &input.ident;
4808
4809 let Data::Struct(data) = &input.data else {
4810 return Err(syn::Error::new_spanned(
4811 struct_name,
4812 "Form can only be derived on structs",
4813 ));
4814 };
4815 let Fields::Named(named) = &data.fields else {
4816 return Err(syn::Error::new_spanned(
4817 struct_name,
4818 "Form requires a struct with named fields",
4819 ));
4820 };
4821
4822 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4823 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4824
4825 for field in &named.named {
4826 let ident = field
4827 .ident
4828 .as_ref()
4829 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4830 let attrs = parse_form_field_attrs(field)?;
4831 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4832
4833 let name_lit = ident.to_string();
4834 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4835 field_blocks.push(parse_block);
4836 field_idents.push(ident);
4837 }
4838
4839 Ok(quote! {
4840 impl ::rustango::forms::Form for #struct_name {
4841 fn parse(
4842 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4843 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4844 let mut __errors = ::rustango::forms::FormErrors::default();
4845 #( #field_blocks )*
4846 if !__errors.is_empty() {
4847 return ::core::result::Result::Err(__errors);
4848 }
4849 ::core::result::Result::Ok(Self {
4850 #( #field_idents ),*
4851 })
4852 }
4853 }
4854 })
4855}
4856
4857fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4858 let mut out = FormFieldAttrs::default();
4859 for attr in &field.attrs {
4860 if !attr.path().is_ident("form") {
4861 continue;
4862 }
4863 attr.parse_nested_meta(|meta| {
4864 if meta.path.is_ident("min") {
4865 let lit: syn::LitInt = meta.value()?.parse()?;
4866 out.min = Some(lit.base10_parse::<i64>()?);
4867 return Ok(());
4868 }
4869 if meta.path.is_ident("max") {
4870 let lit: syn::LitInt = meta.value()?.parse()?;
4871 out.max = Some(lit.base10_parse::<i64>()?);
4872 return Ok(());
4873 }
4874 if meta.path.is_ident("min_length") {
4875 let lit: syn::LitInt = meta.value()?.parse()?;
4876 out.min_length = Some(lit.base10_parse::<u32>()?);
4877 return Ok(());
4878 }
4879 if meta.path.is_ident("max_length") {
4880 let lit: syn::LitInt = meta.value()?.parse()?;
4881 out.max_length = Some(lit.base10_parse::<u32>()?);
4882 return Ok(());
4883 }
4884 Err(meta.error(
4885 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4886 ))
4887 })?;
4888 }
4889 Ok(out)
4890}
4891
4892fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4893 let Type::Path(TypePath { path, qself: None }) = ty else {
4894 return Err(syn::Error::new(
4895 span,
4896 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4897 ));
4898 };
4899 let last = path
4900 .segments
4901 .last()
4902 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4903
4904 if last.ident == "Option" {
4905 let inner = generic_inner(ty, &last.arguments, "Option")?;
4906 let (kind, nested) = detect_form_field(inner, span)?;
4907 if nested {
4908 return Err(syn::Error::new(
4909 span,
4910 "nested Option in Form fields is not supported",
4911 ));
4912 }
4913 return Ok((kind, true));
4914 }
4915
4916 let kind = match last.ident.to_string().as_str() {
4917 "String" => FormFieldKind::String,
4918 "i16" => FormFieldKind::I16,
4919 "i32" => FormFieldKind::I32,
4920 "i64" => FormFieldKind::I64,
4921 "f32" => FormFieldKind::F32,
4922 "f64" => FormFieldKind::F64,
4923 "bool" => FormFieldKind::Bool,
4924 other => {
4925 return Err(syn::Error::new(
4926 span,
4927 format!(
4928 "Form field type `{other}` is not supported in v0.8 — use String / \
4929 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4930 ),
4931 ));
4932 }
4933 };
4934 Ok((kind, false))
4935}
4936
4937#[allow(clippy::too_many_lines)]
4938fn render_form_field_parse(
4939 ident: &syn::Ident,
4940 name_lit: &str,
4941 kind: FormFieldKind,
4942 nullable: bool,
4943 attrs: &FormFieldAttrs,
4944) -> TokenStream2 {
4945 let lookup = quote! {
4948 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4949 };
4950
4951 let parsed_value = match kind {
4952 FormFieldKind::Bool => quote! {
4953 let __v: bool = match __raw {
4954 ::core::option::Option::None => false,
4955 ::core::option::Option::Some(__s) => !matches!(
4956 __s.to_ascii_lowercase().as_str(),
4957 "" | "false" | "0" | "off" | "no"
4958 ),
4959 };
4960 },
4961 FormFieldKind::String => {
4962 if nullable {
4963 quote! {
4964 let __v: ::core::option::Option<::std::string::String> = match __raw {
4965 ::core::option::Option::None => ::core::option::Option::None,
4966 ::core::option::Option::Some(__s) if __s.is_empty() => {
4967 ::core::option::Option::None
4968 }
4969 ::core::option::Option::Some(__s) => {
4970 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4971 }
4972 };
4973 }
4974 } else {
4975 quote! {
4976 let __v: ::std::string::String = match __raw {
4977 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4978 ::core::clone::Clone::clone(__s)
4979 }
4980 _ => {
4981 __errors.add(#name_lit, "This field is required.");
4982 ::std::string::String::new()
4983 }
4984 };
4985 }
4986 }
4987 }
4988 FormFieldKind::I16
4989 | FormFieldKind::I32
4990 | FormFieldKind::I64
4991 | FormFieldKind::F32
4992 | FormFieldKind::F64 => {
4993 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
4994 let ty_lit = kind.parse_method();
4995 let default_val = match kind {
4996 FormFieldKind::I16 => quote! { 0i16 },
4997 FormFieldKind::I32 => quote! { 0i32 },
4998 FormFieldKind::I64 => quote! { 0i64 },
4999 FormFieldKind::F32 => quote! { 0f32 },
5000 FormFieldKind::F64 => quote! { 0f64 },
5001 _ => quote! { Default::default() },
5002 };
5003 if nullable {
5004 quote! {
5005 let __v: ::core::option::Option<#parse_ty> = match __raw {
5006 ::core::option::Option::None => ::core::option::Option::None,
5007 ::core::option::Option::Some(__s) if __s.is_empty() => {
5008 ::core::option::Option::None
5009 }
5010 ::core::option::Option::Some(__s) => {
5011 match __s.parse::<#parse_ty>() {
5012 ::core::result::Result::Ok(__n) => {
5013 ::core::option::Option::Some(__n)
5014 }
5015 ::core::result::Result::Err(__e) => {
5016 __errors.add(
5017 #name_lit,
5018 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5019 );
5020 ::core::option::Option::None
5021 }
5022 }
5023 }
5024 };
5025 }
5026 } else {
5027 quote! {
5028 let __v: #parse_ty = match __raw {
5029 ::core::option::Option::Some(__s) if !__s.is_empty() => {
5030 match __s.parse::<#parse_ty>() {
5031 ::core::result::Result::Ok(__n) => __n,
5032 ::core::result::Result::Err(__e) => {
5033 __errors.add(
5034 #name_lit,
5035 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5036 );
5037 #default_val
5038 }
5039 }
5040 }
5041 _ => {
5042 __errors.add(#name_lit, "This field is required.");
5043 #default_val
5044 }
5045 };
5046 }
5047 }
5048 }
5049 };
5050
5051 let validators = render_form_validators(name_lit, kind, nullable, attrs);
5052
5053 quote! {
5054 let #ident = {
5055 #lookup
5056 #parsed_value
5057 #validators
5058 __v
5059 };
5060 }
5061}
5062
5063fn render_form_validators(
5064 name_lit: &str,
5065 kind: FormFieldKind,
5066 nullable: bool,
5067 attrs: &FormFieldAttrs,
5068) -> TokenStream2 {
5069 let mut checks: Vec<TokenStream2> = Vec::new();
5070
5071 let val_ref = if nullable {
5072 quote! { __v.as_ref() }
5073 } else {
5074 quote! { ::core::option::Option::Some(&__v) }
5075 };
5076
5077 let is_string = matches!(kind, FormFieldKind::String);
5078 let is_numeric = matches!(
5079 kind,
5080 FormFieldKind::I16
5081 | FormFieldKind::I32
5082 | FormFieldKind::I64
5083 | FormFieldKind::F32
5084 | FormFieldKind::F64
5085 );
5086
5087 if is_string {
5088 if let Some(min_len) = attrs.min_length {
5089 let min_len_usize = min_len as usize;
5090 checks.push(quote! {
5091 if let ::core::option::Option::Some(__s) = #val_ref {
5092 if __s.len() < #min_len_usize {
5093 __errors.add(
5094 #name_lit,
5095 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
5096 );
5097 }
5098 }
5099 });
5100 }
5101 if let Some(max_len) = attrs.max_length {
5102 let max_len_usize = max_len as usize;
5103 checks.push(quote! {
5104 if let ::core::option::Option::Some(__s) = #val_ref {
5105 if __s.len() > #max_len_usize {
5106 __errors.add(
5107 #name_lit,
5108 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
5109 );
5110 }
5111 }
5112 });
5113 }
5114 }
5115
5116 if is_numeric {
5117 if let Some(min) = attrs.min {
5118 checks.push(quote! {
5119 if let ::core::option::Option::Some(__n) = #val_ref {
5120 if (*__n as f64) < (#min as f64) {
5121 __errors.add(
5122 #name_lit,
5123 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
5124 );
5125 }
5126 }
5127 });
5128 }
5129 if let Some(max) = attrs.max {
5130 checks.push(quote! {
5131 if let ::core::option::Option::Some(__n) = #val_ref {
5132 if (*__n as f64) > (#max as f64) {
5133 __errors.add(
5134 #name_lit,
5135 ::std::format!("Ensure this value is less than or equal to {}.", #max),
5136 );
5137 }
5138 }
5139 });
5140 }
5141 }
5142
5143 quote! { #( #checks )* }
5144}
5145
5146struct ViewSetAttrs {
5151 model: syn::Path,
5152 fields: Option<Vec<String>>,
5153 filter_fields: Vec<String>,
5154 search_fields: Vec<String>,
5155 ordering: Vec<(String, bool)>,
5157 page_size: Option<usize>,
5158 read_only: bool,
5159 perms: ViewSetPermsAttrs,
5160}
5161
5162#[derive(Default)]
5163struct ViewSetPermsAttrs {
5164 list: Vec<String>,
5165 retrieve: Vec<String>,
5166 create: Vec<String>,
5167 update: Vec<String>,
5168 destroy: Vec<String>,
5169}
5170
5171fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
5172 let struct_name = &input.ident;
5173
5174 match &input.data {
5176 Data::Struct(s) => match &s.fields {
5177 Fields::Unit | Fields::Named(_) => {}
5178 Fields::Unnamed(_) => {
5179 return Err(syn::Error::new_spanned(
5180 struct_name,
5181 "ViewSet can only be derived on a unit struct or an empty named struct",
5182 ));
5183 }
5184 },
5185 _ => {
5186 return Err(syn::Error::new_spanned(
5187 struct_name,
5188 "ViewSet can only be derived on a struct",
5189 ));
5190 }
5191 }
5192
5193 let attrs = parse_viewset_attrs(input)?;
5194 let model_path = &attrs.model;
5195
5196 let fields_call = if let Some(ref fields) = attrs.fields {
5198 let lits = fields.iter().map(|f| f.as_str());
5199 quote!(.fields(&[ #(#lits),* ]))
5200 } else {
5201 quote!()
5202 };
5203
5204 let filter_fields_call = if attrs.filter_fields.is_empty() {
5205 quote!()
5206 } else {
5207 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
5208 quote!(.filter_fields(&[ #(#lits),* ]))
5209 };
5210
5211 let search_fields_call = if attrs.search_fields.is_empty() {
5212 quote!()
5213 } else {
5214 let lits = attrs.search_fields.iter().map(|f| f.as_str());
5215 quote!(.search_fields(&[ #(#lits),* ]))
5216 };
5217
5218 let ordering_call = if attrs.ordering.is_empty() {
5219 quote!()
5220 } else {
5221 let pairs = attrs.ordering.iter().map(|(f, desc)| {
5222 let f = f.as_str();
5223 quote!((#f, #desc))
5224 });
5225 quote!(.ordering(&[ #(#pairs),* ]))
5226 };
5227
5228 let page_size_call = if let Some(n) = attrs.page_size {
5229 quote!(.page_size(#n))
5230 } else {
5231 quote!()
5232 };
5233
5234 let read_only_call = if attrs.read_only {
5235 quote!(.read_only())
5236 } else {
5237 quote!()
5238 };
5239
5240 let perms = &attrs.perms;
5241 let perms_call = if perms.list.is_empty()
5242 && perms.retrieve.is_empty()
5243 && perms.create.is_empty()
5244 && perms.update.is_empty()
5245 && perms.destroy.is_empty()
5246 {
5247 quote!()
5248 } else {
5249 let list_lits = perms.list.iter().map(|s| s.as_str());
5250 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
5251 let create_lits = perms.create.iter().map(|s| s.as_str());
5252 let update_lits = perms.update.iter().map(|s| s.as_str());
5253 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
5254 quote! {
5255 .permissions(::rustango::viewset::ViewSetPerms {
5256 list: ::std::vec![ #(#list_lits.to_owned()),* ],
5257 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
5258 create: ::std::vec![ #(#create_lits.to_owned()),* ],
5259 update: ::std::vec![ #(#update_lits.to_owned()),* ],
5260 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
5261 })
5262 }
5263 };
5264
5265 Ok(quote! {
5266 impl #struct_name {
5267 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
5270 ::rustango::viewset::ViewSet::for_model(
5271 <#model_path as ::rustango::core::Model>::SCHEMA
5272 )
5273 #fields_call
5274 #filter_fields_call
5275 #search_fields_call
5276 #ordering_call
5277 #page_size_call
5278 #perms_call
5279 #read_only_call
5280 .router(prefix, pool)
5281 }
5282 }
5283 })
5284}
5285
5286fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
5287 let mut model: Option<syn::Path> = None;
5288 let mut fields: Option<Vec<String>> = None;
5289 let mut filter_fields: Vec<String> = Vec::new();
5290 let mut search_fields: Vec<String> = Vec::new();
5291 let mut ordering: Vec<(String, bool)> = Vec::new();
5292 let mut page_size: Option<usize> = None;
5293 let mut read_only = false;
5294 let mut perms = ViewSetPermsAttrs::default();
5295
5296 for attr in &input.attrs {
5297 if !attr.path().is_ident("viewset") {
5298 continue;
5299 }
5300 attr.parse_nested_meta(|meta| {
5301 if meta.path.is_ident("model") {
5302 let path: syn::Path = meta.value()?.parse()?;
5303 model = Some(path);
5304 return Ok(());
5305 }
5306 if meta.path.is_ident("fields") {
5307 let s: LitStr = meta.value()?.parse()?;
5308 fields = Some(split_field_list(&s.value()));
5309 return Ok(());
5310 }
5311 if meta.path.is_ident("filter_fields") {
5312 let s: LitStr = meta.value()?.parse()?;
5313 filter_fields = split_field_list(&s.value());
5314 return Ok(());
5315 }
5316 if meta.path.is_ident("search_fields") {
5317 let s: LitStr = meta.value()?.parse()?;
5318 search_fields = split_field_list(&s.value());
5319 return Ok(());
5320 }
5321 if meta.path.is_ident("ordering") {
5322 let s: LitStr = meta.value()?.parse()?;
5323 ordering = parse_ordering_list(&s.value());
5324 return Ok(());
5325 }
5326 if meta.path.is_ident("page_size") {
5327 let lit: syn::LitInt = meta.value()?.parse()?;
5328 page_size = Some(lit.base10_parse::<usize>()?);
5329 return Ok(());
5330 }
5331 if meta.path.is_ident("read_only") {
5332 read_only = true;
5333 return Ok(());
5334 }
5335 if meta.path.is_ident("permissions") {
5336 meta.parse_nested_meta(|inner| {
5337 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5338 let s: LitStr = inner.value()?.parse()?;
5339 Ok(split_field_list(&s.value()))
5340 };
5341 if inner.path.is_ident("list") {
5342 perms.list = parse_codenames(&inner)?;
5343 } else if inner.path.is_ident("retrieve") {
5344 perms.retrieve = parse_codenames(&inner)?;
5345 } else if inner.path.is_ident("create") {
5346 perms.create = parse_codenames(&inner)?;
5347 } else if inner.path.is_ident("update") {
5348 perms.update = parse_codenames(&inner)?;
5349 } else if inner.path.is_ident("destroy") {
5350 perms.destroy = parse_codenames(&inner)?;
5351 } else {
5352 return Err(inner.error(
5353 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5354 ));
5355 }
5356 Ok(())
5357 })?;
5358 return Ok(());
5359 }
5360 Err(meta.error(
5361 "unknown viewset attribute (supported: model, fields, filter_fields, \
5362 search_fields, ordering, page_size, read_only, permissions(...))",
5363 ))
5364 })?;
5365 }
5366
5367 let model = model.ok_or_else(|| {
5368 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
5369 })?;
5370
5371 Ok(ViewSetAttrs {
5372 model,
5373 fields,
5374 filter_fields,
5375 search_fields,
5376 ordering,
5377 page_size,
5378 read_only,
5379 perms,
5380 })
5381}
5382
5383struct SerializerContainerAttrs {
5386 model: syn::Path,
5387}
5388
5389#[derive(Default)]
5390struct SerializerFieldAttrs {
5391 read_only: bool,
5392 write_only: bool,
5393 source: Option<String>,
5394 skip: bool,
5395 method: Option<String>,
5399 validate: Option<String>,
5404 nested: bool,
5414 nested_strict: bool,
5419 many: Option<syn::Type>,
5428}
5429
5430fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5431 let mut model: Option<syn::Path> = None;
5432 for attr in &input.attrs {
5433 if !attr.path().is_ident("serializer") {
5434 continue;
5435 }
5436 attr.parse_nested_meta(|meta| {
5437 if meta.path.is_ident("model") {
5438 let _eq: syn::Token![=] = meta.input.parse()?;
5439 model = Some(meta.input.parse()?);
5440 return Ok(());
5441 }
5442 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5443 })?;
5444 }
5445 let model = model.ok_or_else(|| {
5446 syn::Error::new_spanned(
5447 &input.ident,
5448 "`#[serializer(model = SomeModel)]` is required",
5449 )
5450 })?;
5451 Ok(SerializerContainerAttrs { model })
5452}
5453
5454fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5455 let mut out = SerializerFieldAttrs::default();
5456 for attr in &field.attrs {
5457 if !attr.path().is_ident("serializer") {
5458 continue;
5459 }
5460 attr.parse_nested_meta(|meta| {
5461 if meta.path.is_ident("read_only") {
5462 out.read_only = true;
5463 return Ok(());
5464 }
5465 if meta.path.is_ident("write_only") {
5466 out.write_only = true;
5467 return Ok(());
5468 }
5469 if meta.path.is_ident("skip") {
5470 out.skip = true;
5471 return Ok(());
5472 }
5473 if meta.path.is_ident("source") {
5474 let s: LitStr = meta.value()?.parse()?;
5475 out.source = Some(s.value());
5476 return Ok(());
5477 }
5478 if meta.path.is_ident("method") {
5479 let s: LitStr = meta.value()?.parse()?;
5480 out.method = Some(s.value());
5481 return Ok(());
5482 }
5483 if meta.path.is_ident("validate") {
5484 let s: LitStr = meta.value()?.parse()?;
5485 out.validate = Some(s.value());
5486 return Ok(());
5487 }
5488 if meta.path.is_ident("many") {
5489 let _eq: syn::Token![=] = meta.input.parse()?;
5490 out.many = Some(meta.input.parse()?);
5491 return Ok(());
5492 }
5493 if meta.path.is_ident("nested") {
5494 out.nested = true;
5495 if meta.input.peek(syn::token::Paren) {
5498 meta.parse_nested_meta(|inner| {
5499 if inner.path.is_ident("strict") {
5500 out.nested_strict = true;
5501 return Ok(());
5502 }
5503 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5504 })?;
5505 }
5506 return Ok(());
5507 }
5508 Err(meta.error(
5509 "unknown serializer field attribute (supported: \
5510 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5511 ))
5512 })?;
5513 }
5514 if out.read_only && out.write_only {
5516 return Err(syn::Error::new_spanned(
5517 field,
5518 "a field cannot be both `read_only` and `write_only`",
5519 ));
5520 }
5521 if out.method.is_some() && out.source.is_some() {
5522 return Err(syn::Error::new_spanned(
5523 field,
5524 "`method` and `source` are mutually exclusive — `method` computes \
5525 the value from a method, `source` reads it from a different model field",
5526 ));
5527 }
5528 Ok(out)
5529}
5530
5531fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5532 let struct_name = &input.ident;
5533 let struct_name_lit = struct_name.to_string();
5534
5535 let Data::Struct(data) = &input.data else {
5536 return Err(syn::Error::new_spanned(
5537 struct_name,
5538 "Serializer can only be derived on structs",
5539 ));
5540 };
5541 let Fields::Named(named) = &data.fields else {
5542 return Err(syn::Error::new_spanned(
5543 struct_name,
5544 "Serializer requires a struct with named fields",
5545 ));
5546 };
5547
5548 let container = parse_serializer_container_attrs(input)?;
5549 let model_path = &container.model;
5550
5551 #[allow(dead_code)]
5555 struct FieldInfo {
5556 ident: syn::Ident,
5557 ty: syn::Type,
5558 attrs: SerializerFieldAttrs,
5559 }
5560 let mut fields_info: Vec<FieldInfo> = Vec::new();
5561 for field in &named.named {
5562 let ident = field.ident.clone().expect("named field has ident");
5563 let attrs = parse_serializer_field_attrs(field)?;
5564 fields_info.push(FieldInfo {
5565 ident,
5566 ty: field.ty.clone(),
5567 attrs,
5568 });
5569 }
5570
5571 let from_model_fields = fields_info.iter().map(|fi| {
5573 let ident = &fi.ident;
5574 let ty = &fi.ty;
5575 if let Some(_inner) = &fi.attrs.many {
5576 quote! { #ident: ::std::vec::Vec::new() }
5580 } else if let Some(method) = &fi.attrs.method {
5581 let method_ident = syn::Ident::new(method, ident.span());
5585 quote! { #ident: Self::#method_ident(model) }
5586 } else if fi.attrs.nested {
5587 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5603 let src_ident = syn::Ident::new(&src_name, ident.span());
5604 if fi.attrs.nested_strict {
5605 let panic_msg = format!(
5606 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5607 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5608 );
5609 quote! {
5610 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5611 model.#src_ident.value().expect(#panic_msg),
5612 )
5613 }
5614 } else {
5615 quote! {
5616 #ident: match model.#src_ident.value() {
5617 ::core::option::Option::Some(__loaded) =>
5618 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5619 ::core::option::Option::None =>
5620 ::core::default::Default::default(),
5621 }
5622 }
5623 }
5624 } else if fi.attrs.write_only || fi.attrs.skip {
5625 quote! { #ident: ::core::default::Default::default() }
5627 } else if let Some(src) = &fi.attrs.source {
5628 let src_ident = syn::Ident::new(src, ident.span());
5629 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5630 } else {
5631 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5632 }
5633 });
5634
5635 let validator_calls: Vec<_> = fields_info
5639 .iter()
5640 .filter_map(|fi| {
5641 let ident = &fi.ident;
5642 let name_lit = ident.to_string();
5643 let method = fi.attrs.validate.as_ref()?;
5644 let method_ident = syn::Ident::new(method, ident.span());
5645 Some(quote! {
5646 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5647 __errors.add(#name_lit.to_owned(), __e);
5648 }
5649 })
5650 })
5651 .collect();
5652 let validate_method = if validator_calls.is_empty() {
5653 quote! {}
5654 } else {
5655 quote! {
5656 impl #struct_name {
5657 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5661 let mut __errors = ::rustango::forms::FormErrors::default();
5662 #( #validator_calls )*
5663 if __errors.is_empty() {
5664 ::core::result::Result::Ok(())
5665 } else {
5666 ::core::result::Result::Err(__errors)
5667 }
5668 }
5669 }
5670 }
5671 };
5672
5673 let many_setters: Vec<_> = fields_info
5677 .iter()
5678 .filter_map(|fi| {
5679 let many_ty = fi.attrs.many.as_ref()?;
5680 let ident = &fi.ident;
5681 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5682 Some(quote! {
5683 pub fn #setter(
5688 &mut self,
5689 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5690 ) -> &mut Self {
5691 self.#ident = models.iter()
5692 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5693 .collect();
5694 self
5695 }
5696 })
5697 })
5698 .collect();
5699 let many_setters_impl = if many_setters.is_empty() {
5700 quote! {}
5701 } else {
5702 quote! {
5703 impl #struct_name {
5704 #( #many_setters )*
5705 }
5706 }
5707 };
5708
5709 let output_fields: Vec<_> = fields_info
5711 .iter()
5712 .filter(|fi| !fi.attrs.write_only)
5713 .collect();
5714 let output_field_count = output_fields.len();
5715 let serialize_fields = output_fields.iter().map(|fi| {
5716 let ident = &fi.ident;
5717 let name_lit = ident.to_string();
5718 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5719 });
5720
5721 let writable_lits: Vec<_> = fields_info
5723 .iter()
5724 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5725 .map(|fi| fi.ident.to_string())
5726 .collect();
5727
5728 let openapi_impl = {
5732 #[cfg(feature = "openapi")]
5733 {
5734 let property_calls = output_fields.iter().map(|fi| {
5735 let ident = &fi.ident;
5736 let name_lit = ident.to_string();
5737 let ty = &fi.ty;
5738 let nullable_call = if is_option(ty) {
5739 quote! { .nullable() }
5740 } else {
5741 quote! {}
5742 };
5743 quote! {
5744 .property(
5745 #name_lit,
5746 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5747 #nullable_call,
5748 )
5749 }
5750 });
5751 let required_lits: Vec<_> = output_fields
5752 .iter()
5753 .filter(|fi| !is_option(&fi.ty))
5754 .map(|fi| fi.ident.to_string())
5755 .collect();
5756 quote! {
5757 impl ::rustango::openapi::OpenApiSchema for #struct_name {
5758 fn openapi_schema() -> ::rustango::openapi::Schema {
5759 ::rustango::openapi::Schema::object()
5760 #( #property_calls )*
5761 .required([ #( #required_lits ),* ])
5762 }
5763 }
5764 }
5765 }
5766 #[cfg(not(feature = "openapi"))]
5767 {
5768 quote! {}
5769 }
5770 };
5771
5772 Ok(quote! {
5773 impl ::rustango::serializer::ModelSerializer for #struct_name {
5774 type Model = #model_path;
5775
5776 fn from_model(model: &Self::Model) -> Self {
5777 Self {
5778 #( #from_model_fields ),*
5779 }
5780 }
5781
5782 fn writable_fields() -> &'static [&'static str] {
5783 &[ #( #writable_lits ),* ]
5784 }
5785 }
5786
5787 impl ::serde::Serialize for #struct_name {
5788 fn serialize<S>(&self, serializer: S)
5789 -> ::core::result::Result<S::Ok, S::Error>
5790 where
5791 S: ::serde::Serializer,
5792 {
5793 use ::serde::ser::SerializeStruct;
5794 let mut __state = serializer.serialize_struct(
5795 #struct_name_lit,
5796 #output_field_count,
5797 )?;
5798 #( #serialize_fields )*
5799 __state.end()
5800 }
5801 }
5802
5803 #openapi_impl
5804
5805 #validate_method
5806
5807 #many_setters_impl
5808 })
5809}
5810
5811#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5815fn is_option(ty: &syn::Type) -> bool {
5816 if let syn::Type::Path(p) = ty {
5817 if let Some(last) = p.path.segments.last() {
5818 return last.ident == "Option";
5819 }
5820 }
5821 false
5822}