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 tokio_attr = if args.is_empty() {
210 quote! { #[::rustango::__private_runtime::tokio::main] }
211 } else {
212 quote! { #[::rustango::__private_runtime::tokio::main(#args)] }
213 };
214
215 let body = input.block.clone();
217 input.block = syn::parse2(quote! {{
218 {
219 use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
220 let _ = tracing_subscriber::fmt()
223 .with_env_filter(
224 EnvFilter::try_from_default_env()
225 .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
226 )
227 .try_init();
228 }
229 #body
230 }})?;
231
232 Ok(quote! {
233 #tokio_attr
234 #input
235 })
236}
237
238fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
239 let path_str = if input.is_empty() {
241 "./migrations".to_string()
242 } else {
243 let lit: LitStr = syn::parse2(input)?;
244 lit.value()
245 };
246
247 let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
248 syn::Error::new(
249 proc_macro2::Span::call_site(),
250 "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
251 )
252 })?;
253 let abs = std::path::Path::new(&manifest).join(&path_str);
254
255 let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
256 if abs.is_dir() {
257 let read = std::fs::read_dir(&abs).map_err(|e| {
258 syn::Error::new(
259 proc_macro2::Span::call_site(),
260 format!("embed_migrations!: cannot read {}: {e}", abs.display()),
261 )
262 })?;
263 for entry in read.flatten() {
264 let path = entry.path();
265 if !path.is_file() {
266 continue;
267 }
268 if path.extension().and_then(|s| s.to_str()) != Some("json") {
269 continue;
270 }
271 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
272 continue;
273 };
274 entries.push((stem.to_owned(), path));
275 }
276 }
277 entries.sort_by(|a, b| a.0.cmp(&b.0));
278
279 let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
292 let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
293 for (stem, path) in &entries {
294 let raw = std::fs::read_to_string(path).map_err(|e| {
295 syn::Error::new(
296 proc_macro2::Span::call_site(),
297 format!(
298 "embed_migrations!: cannot read {} for chain validation: {e}",
299 path.display()
300 ),
301 )
302 })?;
303 let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
304 syn::Error::new(
305 proc_macro2::Span::call_site(),
306 format!(
307 "embed_migrations!: {} is not valid JSON: {e}",
308 path.display()
309 ),
310 )
311 })?;
312 let name = json
313 .get("name")
314 .and_then(|v| v.as_str())
315 .ok_or_else(|| {
316 syn::Error::new(
317 proc_macro2::Span::call_site(),
318 format!(
319 "embed_migrations!: {} is missing the `name` field",
320 path.display()
321 ),
322 )
323 })?
324 .to_owned();
325 if name != *stem {
326 return Err(syn::Error::new(
327 proc_macro2::Span::call_site(),
328 format!(
329 "embed_migrations!: file stem `{stem}` does not match the migration's \
330 `name` field `{name}` — rename the file or fix the JSON",
331 ),
332 ));
333 }
334 let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
335 chain_names.push(name.clone());
336 prev_refs.push((name, prev));
337 }
338
339 let name_set: std::collections::HashSet<&str> =
340 chain_names.iter().map(String::as_str).collect();
341 for (name, prev) in &prev_refs {
342 if let Some(p) = prev {
343 if !name_set.contains(p.as_str()) {
344 return Err(syn::Error::new(
345 proc_macro2::Span::call_site(),
346 format!(
347 "embed_migrations!: broken migration chain — `{name}` declares \
348 prev=`{p}` but no migration with that name exists in {}",
349 abs.display()
350 ),
351 ));
352 }
353 }
354 }
355
356 let pairs: Vec<TokenStream2> = entries
357 .iter()
358 .map(|(name, path)| {
359 let path_lit = path.display().to_string();
360 quote! { (#name, ::core::include_str!(#path_lit)) }
361 })
362 .collect();
363
364 Ok(quote! {
365 {
366 const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
367 __RUSTANGO_EMBEDDED
368 }
369 })
370}
371
372fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
373 let struct_name = &input.ident;
374
375 let Data::Struct(data) = &input.data else {
376 return Err(syn::Error::new_spanned(
377 struct_name,
378 "Model can only be derived on structs",
379 ));
380 };
381 let Fields::Named(named) = &data.fields else {
382 return Err(syn::Error::new_spanned(
383 struct_name,
384 "Model requires a struct with named fields",
385 ));
386 };
387
388 let container = parse_container_attrs(input)?;
389 let table = container
390 .table
391 .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
392 let model_name = struct_name.to_string();
393
394 let collected = collect_fields(named, &table)?;
395
396 if let Some((ref display, span)) = container.display {
398 if !collected.field_names.iter().any(|n| n == display) {
399 return Err(syn::Error::new(
400 span,
401 format!("`display = \"{display}\"` does not match any field on this struct"),
402 ));
403 }
404 }
405 let display = container.display.map(|(name, _)| name);
406 let app_label = container.app.clone();
407
408 if let Some(admin) = &container.admin {
410 for (label, list) in [
411 ("list_display", &admin.list_display),
412 ("search_fields", &admin.search_fields),
413 ("readonly_fields", &admin.readonly_fields),
414 ("list_filter", &admin.list_filter),
415 ] {
416 if let Some((names, span)) = list {
417 for name in names {
418 if !collected.field_names.iter().any(|n| n == name) {
419 return Err(syn::Error::new(
420 *span,
421 format!(
422 "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
423 ),
424 ));
425 }
426 }
427 }
428 }
429 if let Some((pairs, span)) = &admin.ordering {
430 for (name, _) in pairs {
431 if !collected.field_names.iter().any(|n| n == name) {
432 return Err(syn::Error::new(
433 *span,
434 format!(
435 "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
436 ),
437 ));
438 }
439 }
440 }
441 if let Some((groups, span)) = &admin.fieldsets {
442 for (_, fields) in groups {
443 for name in fields {
444 if !collected.field_names.iter().any(|n| n == name) {
445 return Err(syn::Error::new(
446 *span,
447 format!(
448 "`fieldsets`: \"{name}\" is not a declared field on this struct"
449 ),
450 ));
451 }
452 }
453 }
454 }
455 }
456 if let Some(audit) = &container.audit {
457 if let Some((names, span)) = &audit.track {
458 for name in names {
459 if !collected.field_names.iter().any(|n| n == name) {
460 return Err(syn::Error::new(
461 *span,
462 format!(
463 "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
464 ),
465 ));
466 }
467 }
468 }
469 }
470
471 let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
474 audit
475 .track
476 .as_ref()
477 .map(|(names, _)| names.clone())
478 .unwrap_or_default()
479 });
480
481 let mut all_indexes: Vec<IndexAttr> = container.indexes;
483 for field in &named.named {
484 let ident = field.ident.as_ref().expect("named");
485 let col = to_snake_case(&ident.to_string()); if let Ok(fa) = parse_field_attrs(field) {
488 if fa.index {
489 let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
490 let auto_name = if fa.index_unique {
491 format!("{table}_{col_name}_uq_idx")
492 } else {
493 format!("{table}_{col_name}_idx")
494 };
495 all_indexes.push(IndexAttr {
496 name: fa.index_name.or(Some(auto_name)),
497 columns: vec![col_name],
498 unique: fa.index_unique,
499 });
500 }
501 }
502 }
503
504 let model_impl = model_impl_tokens(
505 struct_name,
506 &model_name,
507 &table,
508 display.as_deref(),
509 app_label.as_deref(),
510 container.admin.as_ref(),
511 &collected.field_schemas,
512 collected.soft_delete_column.as_deref(),
513 container.permissions,
514 audit_track_names.as_deref(),
515 &container.m2m,
516 &all_indexes,
517 &container.checks,
518 &container.composite_fks,
519 &container.generic_fks,
520 container.scope.as_deref(),
521 );
522 let module_ident = column_module_ident(struct_name);
523 let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
524 let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
525 let track_set: Option<std::collections::HashSet<&str>> = audit
526 .track
527 .as_ref()
528 .map(|(names, _)| names.iter().map(String::as_str).collect());
529 collected
530 .column_entries
531 .iter()
532 .filter(|c| {
533 track_set
534 .as_ref()
535 .map_or(true, |s| s.contains(c.name.as_str()))
536 })
537 .collect()
538 });
539 let inherent_impl = inherent_impl_tokens(
540 struct_name,
541 &collected,
542 collected.primary_key.as_ref(),
543 &column_consts,
544 audited_fields.as_deref(),
545 &all_indexes,
546 );
547 let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
548 let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
549 let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
550 let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
551
552 Ok(quote! {
553 #model_impl
554 #inherent_impl
555 #from_row_impl
556 #column_module
557 #reverse_helpers
558 #m2m_accessors
559
560 ::rustango::core::inventory::submit! {
561 ::rustango::core::ModelEntry {
562 schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
563 module_path: ::core::module_path!(),
568 }
569 }
570 })
571}
572
573fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
584 let arms = fk_relations.iter().map(|rel| {
585 let parent_ty = &rel.parent_type;
586 let fk_col = rel.fk_column.as_str();
587 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
590 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
591 let assign = if rel.nullable {
592 quote! {
593 self.#field_ident = ::core::option::Option::Some(
594 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
595 );
596 }
597 } else {
598 quote! {
599 self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
600 }
601 };
602 quote! {
603 #fk_col => {
604 let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
605 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
612 ::rustango::core::SqlValue::#variant_ident(v) => v,
613 _other => {
614 ::core::debug_assert!(
615 false,
616 "rustango macro bug: load_related on FK `{}` expected \
617 SqlValue::{} from parent's __rustango_pk_value but got \
618 {:?} — file a bug at https://github.com/ujeenet/rustango",
619 #fk_col,
620 ::core::stringify!(#variant_ident),
621 _other,
622 );
623 #default_expr
624 }
625 };
626 #assign
627 ::core::result::Result::Ok(true)
628 }
629 }
630 });
631 quote! {
632 impl ::rustango::sql::LoadRelated for #struct_name {
633 #[allow(unused_variables)]
634 fn __rustango_load_related(
635 &mut self,
636 row: &::rustango::sql::sqlx::postgres::PgRow,
637 field_name: &str,
638 alias: &str,
639 ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
640 match field_name {
641 #( #arms )*
642 _ => ::core::result::Result::Ok(false),
643 }
644 }
645 }
646 }
647}
648
649fn load_related_impl_my_tokens(
657 struct_name: &syn::Ident,
658 fk_relations: &[FkRelation],
659) -> TokenStream2 {
660 let arms = fk_relations.iter().map(|rel| {
661 let parent_ty = &rel.parent_type;
662 let fk_col = rel.fk_column.as_str();
663 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
664 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
665 let assign = if rel.nullable {
666 quote! {
667 __self.#field_ident = ::core::option::Option::Some(
668 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
669 );
670 }
671 } else {
672 quote! {
673 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
674 }
675 };
676 quote! {
681 #fk_col => {
682 let _parent: #parent_ty =
683 <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
684 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
687 ::rustango::core::SqlValue::#variant_ident(v) => v,
688 _other => {
689 ::core::debug_assert!(
690 false,
691 "rustango macro bug: load_related on FK `{}` expected \
692 SqlValue::{} from parent's __rustango_pk_value but got \
693 {:?} — file a bug at https://github.com/ujeenet/rustango",
694 #fk_col,
695 ::core::stringify!(#variant_ident),
696 _other,
697 );
698 #default_expr
699 }
700 };
701 #assign
702 ::core::result::Result::Ok(true)
703 }
704 }
705 });
706 quote! {
707 ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
708 #( #arms )*
709 });
710 }
711}
712
713fn load_related_impl_sqlite_tokens(
717 struct_name: &syn::Ident,
718 fk_relations: &[FkRelation],
719) -> TokenStream2 {
720 let arms = fk_relations.iter().map(|rel| {
721 let parent_ty = &rel.parent_type;
722 let fk_col = rel.fk_column.as_str();
723 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
724 let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
725 let assign = if rel.nullable {
726 quote! {
727 __self.#field_ident = ::core::option::Option::Some(
728 ::rustango::sql::ForeignKey::loaded(_pk, _parent),
729 );
730 }
731 } else {
732 quote! {
733 __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
734 }
735 };
736 quote! {
737 #fk_col => {
738 let _parent: #parent_ty =
739 <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
740 let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
741 ::rustango::core::SqlValue::#variant_ident(v) => v,
742 _other => {
743 ::core::debug_assert!(
744 false,
745 "rustango macro bug: load_related on FK `{}` expected \
746 SqlValue::{} from parent's __rustango_pk_value but got \
747 {:?} — file a bug at https://github.com/ujeenet/rustango",
748 #fk_col,
749 ::core::stringify!(#variant_ident),
750 _other,
751 );
752 #default_expr
753 }
754 };
755 #assign
756 ::core::result::Result::Ok(true)
757 }
758 }
759 });
760 quote! {
761 ::rustango::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias| {
762 #( #arms )*
763 });
764 }
765}
766
767fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
775 let arms = fk_relations.iter().map(|rel| {
776 let fk_col = rel.fk_column.as_str();
777 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
778 if rel.pk_kind == DetectedKind::I64 {
779 if rel.nullable {
785 quote! {
786 #fk_col => self.#field_ident
787 .as_ref()
788 .map(|fk| ::rustango::sql::ForeignKey::pk(fk)),
789 }
790 } else {
791 quote! {
792 #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
793 }
794 }
795 } else {
796 quote! {
804 #fk_col => ::core::option::Option::None,
805 }
806 }
807 });
808 let value_arms = fk_relations.iter().map(|rel| {
814 let fk_col = rel.fk_column.as_str();
815 let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
816 if rel.nullable {
817 quote! {
818 #fk_col => self.#field_ident
819 .as_ref()
820 .map(|fk| ::core::convert::Into::<::rustango::core::SqlValue>::into(
821 ::rustango::sql::ForeignKey::pk(fk)
822 )),
823 }
824 } else {
825 quote! {
826 #fk_col => ::core::option::Option::Some(
827 ::core::convert::Into::<::rustango::core::SqlValue>::into(
828 self.#field_ident.pk()
829 )
830 ),
831 }
832 }
833 });
834 quote! {
835 impl ::rustango::sql::FkPkAccess for #struct_name {
836 #[allow(unused_variables)]
837 fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
838 match field_name {
839 #( #arms )*
840 _ => ::core::option::Option::None,
841 }
842 }
843 #[allow(unused_variables)]
844 fn __rustango_fk_pk_value(
845 &self,
846 field_name: &str,
847 ) -> ::core::option::Option<::rustango::core::SqlValue> {
848 match field_name {
849 #( #value_arms )*
850 _ => ::core::option::Option::None,
851 }
852 }
853 }
854 }
855}
856
857fn reverse_helper_tokens(child_ident: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
863 if fk_relations.is_empty() {
864 return TokenStream2::new();
865 }
866 let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
870 let method_ident = syn::Ident::new(&suffix, child_ident.span());
871 let impls = fk_relations.iter().map(|rel| {
872 let parent_ty = &rel.parent_type;
873 let fk_col = rel.fk_column.as_str();
874 let doc = format!(
875 "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
876 Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
877 generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
878 further `{child_ident}::objects()` filters via direct queryset use."
879 );
880 quote! {
881 impl #parent_ty {
882 #[doc = #doc]
883 pub async fn #method_ident<'_c, _E>(
888 &self,
889 _executor: _E,
890 ) -> ::core::result::Result<
891 ::std::vec::Vec<#child_ident>,
892 ::rustango::sql::ExecError,
893 >
894 where
895 _E: ::rustango::sql::sqlx::Executor<
896 '_c,
897 Database = ::rustango::sql::sqlx::Postgres,
898 >,
899 {
900 let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
901 ::rustango::query::QuerySet::<#child_ident>::new()
902 .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
903 .fetch_on(_executor)
904 .await
905 }
906 }
907 }
908 });
909 quote! { #( #impls )* }
910}
911
912fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
915 if m2m_relations.is_empty() {
916 return TokenStream2::new();
917 }
918 let methods = m2m_relations.iter().map(|rel| {
919 let method_name = format!("{}_m2m", rel.name);
920 let method_ident = syn::Ident::new(&method_name, struct_name.span());
921 let through = rel.through.as_str();
922 let src_col = rel.src.as_str();
923 let dst_col = rel.dst.as_str();
924 quote! {
925 pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
926 ::rustango::sql::M2MManager {
927 src_pk: self.__rustango_pk_value(),
928 through: #through,
929 src_col: #src_col,
930 dst_col: #dst_col,
931 }
932 }
933 }
934 });
935 quote! {
936 impl #struct_name {
937 #( #methods )*
938 }
939 }
940}
941
942struct ColumnEntry {
943 ident: syn::Ident,
946 value_ty: Type,
948 name: String,
950 column: String,
952 field_type_tokens: TokenStream2,
954}
955
956struct CollectedFields {
957 field_schemas: Vec<TokenStream2>,
958 from_row_inits: Vec<TokenStream2>,
959 from_aliased_row_inits: Vec<TokenStream2>,
963 insert_columns: Vec<TokenStream2>,
966 insert_values: Vec<TokenStream2>,
969 insert_pushes: Vec<TokenStream2>,
974 returning_cols: Vec<TokenStream2>,
977 auto_assigns: Vec<TokenStream2>,
980 auto_field_idents: Vec<(syn::Ident, String)>,
984 first_auto_value_ty: Option<Type>,
987 bulk_pushes_no_auto: Vec<TokenStream2>,
991 bulk_pushes_all: Vec<TokenStream2>,
995 bulk_columns_no_auto: Vec<TokenStream2>,
998 bulk_columns_all: Vec<TokenStream2>,
1001 bulk_auto_uniformity: Vec<TokenStream2>,
1005 first_auto_ident: Option<syn::Ident>,
1008 has_auto: bool,
1010 pk_is_auto: bool,
1014 update_assignments: Vec<TokenStream2>,
1017 upsert_update_columns: Vec<TokenStream2>,
1020 primary_key: Option<(syn::Ident, String)>,
1021 column_entries: Vec<ColumnEntry>,
1022 field_names: Vec<String>,
1025 fk_relations: Vec<FkRelation>,
1030 soft_delete_column: Option<String>,
1035}
1036
1037#[derive(Clone)]
1038struct FkRelation {
1039 parent_type: Type,
1042 fk_column: String,
1045 pk_kind: DetectedKind,
1050 nullable: bool,
1055}
1056
1057fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
1058 let cap = named.named.len();
1059 let mut out = CollectedFields {
1060 field_schemas: Vec::with_capacity(cap),
1061 from_row_inits: Vec::with_capacity(cap),
1062 from_aliased_row_inits: Vec::with_capacity(cap),
1063 insert_columns: Vec::with_capacity(cap),
1064 insert_values: Vec::with_capacity(cap),
1065 insert_pushes: Vec::with_capacity(cap),
1066 returning_cols: Vec::new(),
1067 auto_assigns: Vec::new(),
1068 auto_field_idents: Vec::new(),
1069 first_auto_value_ty: None,
1070 bulk_pushes_no_auto: Vec::with_capacity(cap),
1071 bulk_pushes_all: Vec::with_capacity(cap),
1072 bulk_columns_no_auto: Vec::with_capacity(cap),
1073 bulk_columns_all: Vec::with_capacity(cap),
1074 bulk_auto_uniformity: Vec::new(),
1075 first_auto_ident: None,
1076 has_auto: false,
1077 pk_is_auto: false,
1078 update_assignments: Vec::with_capacity(cap),
1079 upsert_update_columns: Vec::with_capacity(cap),
1080 primary_key: None,
1081 column_entries: Vec::with_capacity(cap),
1082 field_names: Vec::with_capacity(cap),
1083 fk_relations: Vec::new(),
1084 soft_delete_column: None,
1085 };
1086
1087 for field in &named.named {
1088 let info = process_field(field, table)?;
1089 out.field_names.push(info.ident.to_string());
1090 out.field_schemas.push(info.schema);
1091 out.from_row_inits.push(info.from_row_init);
1092 out.from_aliased_row_inits.push(info.from_aliased_row_init);
1093 if let Some(parent_ty) = info.fk_inner.clone() {
1094 out.fk_relations.push(FkRelation {
1095 parent_type: parent_ty,
1096 fk_column: info.column.clone(),
1097 pk_kind: info.fk_pk_kind,
1098 nullable: info.nullable,
1099 });
1100 }
1101 if info.soft_delete {
1102 if out.soft_delete_column.is_some() {
1103 return Err(syn::Error::new_spanned(
1104 field,
1105 "only one field may be marked `#[rustango(soft_delete)]`",
1106 ));
1107 }
1108 out.soft_delete_column = Some(info.column.clone());
1109 }
1110 let column = info.column.as_str();
1111 let ident = info.ident;
1112 if info.generated_as.is_some() {
1121 out.column_entries.push(ColumnEntry {
1122 ident: ident.clone(),
1123 value_ty: info.value_ty.clone(),
1124 name: ident.to_string(),
1125 column: info.column.clone(),
1126 field_type_tokens: info.field_type_tokens,
1127 });
1128 continue;
1129 }
1130 out.insert_columns.push(quote!(#column));
1131 out.insert_values.push(quote! {
1132 ::core::convert::Into::<::rustango::core::SqlValue>::into(
1133 ::core::clone::Clone::clone(&self.#ident)
1134 )
1135 });
1136 if info.auto {
1137 out.has_auto = true;
1138 if out.first_auto_ident.is_none() {
1139 out.first_auto_ident = Some(ident.clone());
1140 out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
1141 }
1142 out.returning_cols.push(quote!(#column));
1143 out.auto_field_idents
1144 .push((ident.clone(), info.column.clone()));
1145 out.auto_assigns.push(quote! {
1146 self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
1147 });
1148 out.insert_pushes.push(quote! {
1149 if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
1150 _columns.push(#column);
1151 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1152 ::core::clone::Clone::clone(_v)
1153 ));
1154 }
1155 });
1156 out.bulk_columns_all.push(quote!(#column));
1159 out.bulk_pushes_all.push(quote! {
1160 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1161 ::core::clone::Clone::clone(&_row.#ident)
1162 ));
1163 });
1164 let ident_clone = ident.clone();
1168 out.bulk_auto_uniformity.push(quote! {
1169 for _r in rows.iter().skip(1) {
1170 if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
1171 return ::core::result::Result::Err(
1172 ::rustango::sql::ExecError::Sql(
1173 ::rustango::sql::SqlError::BulkAutoMixed
1174 )
1175 );
1176 }
1177 }
1178 });
1179 } else {
1180 out.insert_pushes.push(quote! {
1181 _columns.push(#column);
1182 _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1183 ::core::clone::Clone::clone(&self.#ident)
1184 ));
1185 });
1186 out.bulk_columns_no_auto.push(quote!(#column));
1188 out.bulk_columns_all.push(quote!(#column));
1189 let push_expr = quote! {
1190 _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
1191 ::core::clone::Clone::clone(&_row.#ident)
1192 ));
1193 };
1194 out.bulk_pushes_no_auto.push(push_expr.clone());
1195 out.bulk_pushes_all.push(push_expr);
1196 }
1197 if info.primary_key {
1198 if out.primary_key.is_some() {
1199 return Err(syn::Error::new_spanned(
1200 field,
1201 "only one field may be marked `#[rustango(primary_key)]`",
1202 ));
1203 }
1204 out.primary_key = Some((ident.clone(), info.column.clone()));
1205 if info.auto {
1206 out.pk_is_auto = true;
1207 }
1208 } else if info.auto_now_add {
1209 } else if info.auto_now {
1211 out.update_assignments.push(quote! {
1216 ::rustango::core::Assignment {
1217 column: #column,
1218 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1219 ::chrono::Utc::now()
1220 ),
1221 }
1222 });
1223 out.upsert_update_columns.push(quote!(#column));
1224 } else {
1225 out.update_assignments.push(quote! {
1226 ::rustango::core::Assignment {
1227 column: #column,
1228 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1229 ::core::clone::Clone::clone(&self.#ident)
1230 ),
1231 }
1232 });
1233 out.upsert_update_columns.push(quote!(#column));
1234 }
1235 out.column_entries.push(ColumnEntry {
1236 ident: ident.clone(),
1237 value_ty: info.value_ty.clone(),
1238 name: ident.to_string(),
1239 column: info.column.clone(),
1240 field_type_tokens: info.field_type_tokens,
1241 });
1242 }
1243 Ok(out)
1244}
1245
1246fn model_impl_tokens(
1247 struct_name: &syn::Ident,
1248 model_name: &str,
1249 table: &str,
1250 display: Option<&str>,
1251 app_label: Option<&str>,
1252 admin: Option<&AdminAttrs>,
1253 field_schemas: &[TokenStream2],
1254 soft_delete_column: Option<&str>,
1255 permissions: bool,
1256 audit_track: Option<&[String]>,
1257 m2m_relations: &[M2MAttr],
1258 indexes: &[IndexAttr],
1259 checks: &[CheckAttr],
1260 composite_fks: &[CompositeFkAttr],
1261 generic_fks: &[GenericFkAttr],
1262 scope: Option<&str>,
1263) -> TokenStream2 {
1264 let display_tokens = if let Some(name) = display {
1265 quote!(::core::option::Option::Some(#name))
1266 } else {
1267 quote!(::core::option::Option::None)
1268 };
1269 let app_label_tokens = if let Some(name) = app_label {
1270 quote!(::core::option::Option::Some(#name))
1271 } else {
1272 quote!(::core::option::Option::None)
1273 };
1274 let soft_delete_tokens = if let Some(col) = soft_delete_column {
1275 quote!(::core::option::Option::Some(#col))
1276 } else {
1277 quote!(::core::option::Option::None)
1278 };
1279 let audit_track_tokens = match audit_track {
1280 None => quote!(::core::option::Option::None),
1281 Some(names) => {
1282 let lits = names.iter().map(|n| n.as_str());
1283 quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1284 }
1285 };
1286 let admin_tokens = admin_config_tokens(admin);
1287 let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
1291 Some("registry") => quote!(::rustango::core::ModelScope::Registry),
1292 _ => quote!(::rustango::core::ModelScope::Tenant),
1293 };
1294 let indexes_tokens = indexes.iter().map(|idx| {
1295 let name = idx.name.as_deref().unwrap_or("unnamed_index");
1296 let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1297 let unique = idx.unique;
1298 quote! {
1299 ::rustango::core::IndexSchema {
1300 name: #name,
1301 columns: &[ #(#cols),* ],
1302 unique: #unique,
1303 }
1304 }
1305 });
1306 let checks_tokens = checks.iter().map(|c| {
1307 let name = c.name.as_str();
1308 let expr = c.expr.as_str();
1309 quote! {
1310 ::rustango::core::CheckConstraint {
1311 name: #name,
1312 expr: #expr,
1313 }
1314 }
1315 });
1316 let composite_fk_tokens = composite_fks.iter().map(|rel| {
1317 let name = rel.name.as_str();
1318 let to = rel.to.as_str();
1319 let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1320 let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1321 quote! {
1322 ::rustango::core::CompositeFkRelation {
1323 name: #name,
1324 to: #to,
1325 from: &[ #(#from_cols),* ],
1326 on: &[ #(#on_cols),* ],
1327 }
1328 }
1329 });
1330 let generic_fk_tokens = generic_fks.iter().map(|rel| {
1331 let name = rel.name.as_str();
1332 let ct_col = rel.ct_column.as_str();
1333 let pk_col = rel.pk_column.as_str();
1334 quote! {
1335 ::rustango::core::GenericRelation {
1336 name: #name,
1337 ct_column: #ct_col,
1338 pk_column: #pk_col,
1339 }
1340 }
1341 });
1342 let m2m_tokens = m2m_relations.iter().map(|rel| {
1343 let name = rel.name.as_str();
1344 let to = rel.to.as_str();
1345 let through = rel.through.as_str();
1346 let src = rel.src.as_str();
1347 let dst = rel.dst.as_str();
1348 quote! {
1349 ::rustango::core::M2MRelation {
1350 name: #name,
1351 to: #to,
1352 through: #through,
1353 src_col: #src,
1354 dst_col: #dst,
1355 }
1356 }
1357 });
1358 quote! {
1359 impl ::rustango::core::Model for #struct_name {
1360 const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1361 name: #model_name,
1362 table: #table,
1363 fields: &[ #(#field_schemas),* ],
1364 display: #display_tokens,
1365 app_label: #app_label_tokens,
1366 admin: #admin_tokens,
1367 soft_delete_column: #soft_delete_tokens,
1368 permissions: #permissions,
1369 audit_track: #audit_track_tokens,
1370 m2m: &[ #(#m2m_tokens),* ],
1371 indexes: &[ #(#indexes_tokens),* ],
1372 check_constraints: &[ #(#checks_tokens),* ],
1373 composite_relations: &[ #(#composite_fk_tokens),* ],
1374 generic_relations: &[ #(#generic_fk_tokens),* ],
1375 scope: #scope_tokens,
1376 };
1377 }
1378 }
1379}
1380
1381fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1385 let Some(admin) = admin else {
1386 return quote!(::core::option::Option::None);
1387 };
1388
1389 let list_display = admin
1390 .list_display
1391 .as_ref()
1392 .map(|(v, _)| v.as_slice())
1393 .unwrap_or(&[]);
1394 let list_display_lits = list_display.iter().map(|s| s.as_str());
1395
1396 let search_fields = admin
1397 .search_fields
1398 .as_ref()
1399 .map(|(v, _)| v.as_slice())
1400 .unwrap_or(&[]);
1401 let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1402
1403 let readonly_fields = admin
1404 .readonly_fields
1405 .as_ref()
1406 .map(|(v, _)| v.as_slice())
1407 .unwrap_or(&[]);
1408 let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1409
1410 let list_filter = admin
1411 .list_filter
1412 .as_ref()
1413 .map(|(v, _)| v.as_slice())
1414 .unwrap_or(&[]);
1415 let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1416
1417 let actions = admin
1418 .actions
1419 .as_ref()
1420 .map(|(v, _)| v.as_slice())
1421 .unwrap_or(&[]);
1422 let actions_lits = actions.iter().map(|s| s.as_str());
1423
1424 let fieldsets = admin
1425 .fieldsets
1426 .as_ref()
1427 .map(|(v, _)| v.as_slice())
1428 .unwrap_or(&[]);
1429 let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1430 let title = title.as_str();
1431 let field_lits = fields.iter().map(|s| s.as_str());
1432 quote!(::rustango::core::Fieldset {
1433 title: #title,
1434 fields: &[ #( #field_lits ),* ],
1435 })
1436 });
1437
1438 let list_per_page = admin.list_per_page.unwrap_or(0);
1439
1440 let ordering_pairs = admin
1441 .ordering
1442 .as_ref()
1443 .map(|(v, _)| v.as_slice())
1444 .unwrap_or(&[]);
1445 let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1446 let name = name.as_str();
1447 let desc = *desc;
1448 quote!((#name, #desc))
1449 });
1450
1451 quote! {
1452 ::core::option::Option::Some(&::rustango::core::AdminConfig {
1453 list_display: &[ #( #list_display_lits ),* ],
1454 search_fields: &[ #( #search_fields_lits ),* ],
1455 list_per_page: #list_per_page,
1456 ordering: &[ #( #ordering_tokens ),* ],
1457 readonly_fields: &[ #( #readonly_fields_lits ),* ],
1458 list_filter: &[ #( #list_filter_lits ),* ],
1459 actions: &[ #( #actions_lits ),* ],
1460 fieldsets: &[ #( #fieldset_tokens ),* ],
1461 })
1462 }
1463}
1464
1465fn inherent_impl_tokens(
1466 struct_name: &syn::Ident,
1467 fields: &CollectedFields,
1468 primary_key: Option<&(syn::Ident, String)>,
1469 column_consts: &TokenStream2,
1470 audited_fields: Option<&[&ColumnEntry]>,
1471 indexes: &[IndexAttr],
1472) -> TokenStream2 {
1473 let executor_passes_to_data_write = if audited_fields.is_some() {
1479 quote!(&mut *_executor)
1480 } else {
1481 quote!(_executor)
1482 };
1483 let executor_param = if audited_fields.is_some() {
1484 quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1485 } else {
1486 quote!(_executor: _E)
1487 };
1488 let executor_generics = if audited_fields.is_some() {
1489 quote!()
1490 } else {
1491 quote!(<'_c, _E>)
1492 };
1493 let executor_where = if audited_fields.is_some() {
1494 quote!()
1495 } else {
1496 quote! {
1497 where
1498 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1499 }
1500 };
1501 let pool_to_save_on = if audited_fields.is_some() {
1506 quote! {
1507 let mut _conn = pool.acquire().await?;
1508 self.save_on(&mut *_conn).await
1509 }
1510 } else {
1511 quote!(self.save_on(pool).await)
1512 };
1513 let pool_to_insert_on = if audited_fields.is_some() {
1514 quote! {
1515 let mut _conn = pool.acquire().await?;
1516 self.insert_on(&mut *_conn).await
1517 }
1518 } else {
1519 quote!(self.insert_on(pool).await)
1520 };
1521 let pool_to_delete_on = if audited_fields.is_some() {
1522 quote! {
1523 let mut _conn = pool.acquire().await?;
1524 self.delete_on(&mut *_conn).await
1525 }
1526 } else {
1527 quote!(self.delete_on(pool).await)
1528 };
1529 let pool_to_bulk_insert_on = if audited_fields.is_some() {
1530 quote! {
1531 let mut _conn = pool.acquire().await?;
1532 Self::bulk_insert_on(rows, &mut *_conn).await
1533 }
1534 } else {
1535 quote!(Self::bulk_insert_on(rows, pool).await)
1536 };
1537 let pool_to_upsert_on = if audited_fields.is_some() {
1544 quote! {
1545 let mut _conn = pool.acquire().await?;
1546 self.upsert_on(&mut *_conn).await
1547 }
1548 } else {
1549 quote!(self.upsert_on(pool).await)
1550 };
1551
1552 let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1570 quote!()
1579 } else if audited_fields.is_some() && fields.has_auto {
1580 quote!()
1583 } else if fields.has_auto {
1584 let pushes = &fields.insert_pushes;
1585 let returning_cols = &fields.returning_cols;
1586 quote! {
1587 pub async fn insert_pool(
1593 &mut self,
1594 pool: &::rustango::sql::Pool,
1595 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1596 let mut _columns: ::std::vec::Vec<&'static str> =
1597 ::std::vec::Vec::new();
1598 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1599 ::std::vec::Vec::new();
1600 #( #pushes )*
1601 let _query = ::rustango::core::InsertQuery {
1602 model: <Self as ::rustango::core::Model>::SCHEMA,
1603 columns: _columns,
1604 values: _values,
1605 returning: ::std::vec![ #( #returning_cols ),* ],
1606 on_conflict: ::core::option::Option::None,
1607 };
1608 let _result = ::rustango::sql::insert_returning_pool(
1609 pool, &_query,
1610 ).await?;
1611 ::rustango::sql::apply_auto_pk_pool(_result, self)
1612 }
1613 }
1614 } else {
1615 let insert_columns = &fields.insert_columns;
1616 let insert_values = &fields.insert_values;
1617 quote! {
1618 pub async fn insert_pool(
1625 &self,
1626 pool: &::rustango::sql::Pool,
1627 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1628 let _query = ::rustango::core::InsertQuery {
1629 model: <Self as ::rustango::core::Model>::SCHEMA,
1630 columns: ::std::vec![ #( #insert_columns ),* ],
1631 values: ::std::vec![ #( #insert_values ),* ],
1632 returning: ::std::vec::Vec::new(),
1633 on_conflict: ::core::option::Option::None,
1634 };
1635 ::rustango::sql::insert_pool(pool, &_query).await
1636 }
1637 }
1638 };
1639
1640 let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1653 .map(|tracked| {
1654 tracked
1655 .iter()
1656 .map(|c| {
1657 let column_lit = c.column.as_str();
1658 let ident = &c.ident;
1659 quote! {
1660 (
1661 #column_lit,
1662 ::serde_json::to_value(&self.#ident)
1663 .unwrap_or(::serde_json::Value::Null),
1664 )
1665 }
1666 })
1667 .collect()
1668 })
1669 .unwrap_or_default();
1670 let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1671 if fields.pk_is_auto {
1672 quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1673 } else {
1674 quote!(::std::format!("{}", &self.#pk_ident))
1675 }
1676 } else {
1677 quote!(::std::string::String::new())
1678 };
1679 let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1680 if audited_fields.is_some() {
1681 let pairs = audit_pair_tokens.iter();
1682 let pk_str = audit_pk_to_string.clone();
1683 quote! {
1684 let _audit_entry = ::rustango::audit::PendingEntry {
1685 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1686 entity_pk: #pk_str,
1687 operation: #op_path,
1688 source: ::rustango::audit::current_source(),
1689 changes: ::rustango::audit::snapshot_changes(&[
1690 #( #pairs ),*
1691 ]),
1692 };
1693 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1694 }
1695 } else {
1696 quote!()
1697 }
1698 };
1699 let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1700 let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1701 let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1702 let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1703
1704 let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1720 let pk_column_lit = pk_col.as_str();
1721 let assignments = &fields.update_assignments;
1722 if audited_fields.is_some() {
1723 if fields.pk_is_auto {
1724 quote!()
1728 } else {
1729 let pairs = audit_pair_tokens.iter();
1730 let pk_str = audit_pk_to_string.clone();
1731 quote! {
1732 pub async fn save_pool(
1746 &mut self,
1747 pool: &::rustango::sql::Pool,
1748 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1749 let _query = ::rustango::core::UpdateQuery {
1750 model: <Self as ::rustango::core::Model>::SCHEMA,
1751 set: ::std::vec![ #( #assignments ),* ],
1752 where_clause: ::rustango::core::WhereExpr::Predicate(
1753 ::rustango::core::Filter {
1754 column: #pk_column_lit,
1755 op: ::rustango::core::Op::Eq,
1756 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1757 ::core::clone::Clone::clone(&self.#pk_ident)
1758 ),
1759 }
1760 ),
1761 };
1762 let _audit_entry = ::rustango::audit::PendingEntry {
1763 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1764 entity_pk: #pk_str,
1765 operation: ::rustango::audit::AuditOp::Update,
1766 source: ::rustango::audit::current_source(),
1767 changes: ::rustango::audit::snapshot_changes(&[
1768 #( #pairs ),*
1769 ]),
1770 };
1771 let _ = ::rustango::audit::save_one_with_audit_pool(
1772 pool, &_query, &_audit_entry,
1773 ).await?;
1774 ::core::result::Result::Ok(())
1775 }
1776 }
1777 }
1778 } else {
1779 let dispatch_unset = if fields.pk_is_auto {
1780 quote! {
1781 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1782 return self.insert_pool(pool).await;
1783 }
1784 }
1785 } else {
1786 quote!()
1787 };
1788 quote! {
1789 pub async fn save_pool(
1796 &mut self,
1797 pool: &::rustango::sql::Pool,
1798 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1799 #dispatch_unset
1800 let _query = ::rustango::core::UpdateQuery {
1801 model: <Self as ::rustango::core::Model>::SCHEMA,
1802 set: ::std::vec![ #( #assignments ),* ],
1803 where_clause: ::rustango::core::WhereExpr::Predicate(
1804 ::rustango::core::Filter {
1805 column: #pk_column_lit,
1806 op: ::rustango::core::Op::Eq,
1807 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1808 ::core::clone::Clone::clone(&self.#pk_ident)
1809 ),
1810 }
1811 ),
1812 };
1813 let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1814 ::core::result::Result::Ok(())
1815 }
1816 }
1817 }
1818 } else {
1819 quote!()
1820 };
1821
1822 let pool_insert_method = if audited_fields.is_some() {
1829 if let Some(_) = primary_key {
1830 let pushes = if fields.has_auto {
1831 fields.insert_pushes.clone()
1832 } else {
1833 fields
1838 .insert_columns
1839 .iter()
1840 .zip(&fields.insert_values)
1841 .map(|(col, val)| {
1842 quote! {
1843 _columns.push(#col);
1844 _values.push(#val);
1845 }
1846 })
1847 .collect()
1848 };
1849 let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1850 fields.returning_cols.clone()
1851 } else {
1852 primary_key
1859 .map(|(_, col)| {
1860 let lit = col.as_str();
1861 vec![quote!(#lit)]
1862 })
1863 .unwrap_or_default()
1864 };
1865 let pairs = audit_pair_tokens.iter();
1866 let pk_str = audit_pk_to_string.clone();
1867 quote! {
1868 pub async fn insert_pool(
1877 &mut self,
1878 pool: &::rustango::sql::Pool,
1879 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1880 let mut _columns: ::std::vec::Vec<&'static str> =
1881 ::std::vec::Vec::new();
1882 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1883 ::std::vec::Vec::new();
1884 #( #pushes )*
1885 let _query = ::rustango::core::InsertQuery {
1886 model: <Self as ::rustango::core::Model>::SCHEMA,
1887 columns: _columns,
1888 values: _values,
1889 returning: ::std::vec![ #( #returning_cols ),* ],
1890 on_conflict: ::core::option::Option::None,
1891 };
1892 let _audit_entry = ::rustango::audit::PendingEntry {
1893 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1894 entity_pk: #pk_str,
1895 operation: ::rustango::audit::AuditOp::Create,
1896 source: ::rustango::audit::current_source(),
1897 changes: ::rustango::audit::snapshot_changes(&[
1898 #( #pairs ),*
1899 ]),
1900 };
1901 let _result = ::rustango::audit::insert_one_with_audit_pool(
1902 pool, &_query, &_audit_entry,
1903 ).await?;
1904 ::rustango::sql::apply_auto_pk_pool(_result, self)
1905 }
1906 }
1907 } else {
1908 quote!()
1909 }
1910 } else {
1911 pool_insert_method
1913 };
1914
1915 let pool_save_method = if let Some(tracked) = audited_fields {
1936 if let Some((pk_ident, pk_col)) = primary_key {
1937 let pk_column_lit = pk_col.as_str();
1938 let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1942 let pk_str = audit_pk_to_string.clone();
1943 let mk_before_pairs =
1948 |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1949 tracked
1950 .iter()
1951 .map(|c| {
1952 let column_lit = c.column.as_str();
1953 let value_ty = &c.value_ty;
1954 quote! {
1955 (
1956 #column_lit,
1957 match #getter::<#value_ty>(
1958 _audit_before_row, #column_lit,
1959 ) {
1960 ::core::result::Result::Ok(v) => {
1961 ::serde_json::to_value(&v)
1962 .unwrap_or(::serde_json::Value::Null)
1963 }
1964 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1965 },
1966 )
1967 }
1968 })
1969 .collect()
1970 };
1971 let before_pairs_pg: Vec<proc_macro2::TokenStream> =
1972 mk_before_pairs(quote!(::rustango::sql::try_get_returning));
1973 let before_pairs_my: Vec<proc_macro2::TokenStream> =
1974 mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
1975 let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
1976 mk_before_pairs(quote!(::rustango::sql::try_get_returning_sqlite));
1977 let pg_select_cols: String = tracked
1978 .iter()
1979 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1980 .collect::<Vec<_>>()
1981 .join(", ");
1982 let my_select_cols: String = tracked
1983 .iter()
1984 .map(|c| format!("`{}`", c.column.replace('`', "``")))
1985 .collect::<Vec<_>>()
1986 .join(", ");
1987 let sqlite_select_cols: String = pg_select_cols.clone();
1991 let pk_value_for_bind = if fields.pk_is_auto {
1992 quote!(self.#pk_ident.get().copied().unwrap_or_default())
1993 } else {
1994 quote!(::core::clone::Clone::clone(&self.#pk_ident))
1995 };
1996 let assignments = &fields.update_assignments;
1997 let unset_dispatch = if fields.has_auto {
1998 quote! {
1999 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2000 return self.insert_pool(pool).await;
2001 }
2002 }
2003 } else {
2004 quote!()
2005 };
2006 quote! {
2007 pub async fn save_pool(
2021 &mut self,
2022 pool: &::rustango::sql::Pool,
2023 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2024 #unset_dispatch
2025 let _query = ::rustango::core::UpdateQuery {
2026 model: <Self as ::rustango::core::Model>::SCHEMA,
2027 set: ::std::vec![ #( #assignments ),* ],
2028 where_clause: ::rustango::core::WhereExpr::Predicate(
2029 ::rustango::core::Filter {
2030 column: #pk_column_lit,
2031 op: ::rustango::core::Op::Eq,
2032 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2033 ::core::clone::Clone::clone(&self.#pk_ident)
2034 ),
2035 }
2036 ),
2037 };
2038 let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2039 ::std::vec![ #( #after_pairs_pg ),* ];
2040 ::rustango::audit::save_one_with_diff_pool(
2041 pool,
2042 &_query,
2043 #pk_column_lit,
2044 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2045 #pk_value_for_bind,
2046 ),
2047 <Self as ::rustango::core::Model>::SCHEMA.table,
2048 #pk_str,
2049 _after_pairs,
2050 #pg_select_cols,
2051 #my_select_cols,
2052 #sqlite_select_cols,
2053 |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
2054 |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
2055 |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
2056 ).await
2057 }
2058 }
2059 } else {
2060 quote!()
2061 }
2062 } else {
2063 pool_save_method
2064 };
2065
2066 let pool_delete_method = {
2073 let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
2074 let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
2075 if let Some(pk_ident) = pk_ident_for_pool {
2076 if audited_fields.is_some() {
2077 let pairs = audit_pair_tokens.iter();
2078 let pk_str = audit_pk_to_string.clone();
2079 quote! {
2080 pub async fn delete_pool(
2087 &self,
2088 pool: &::rustango::sql::Pool,
2089 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2090 let _query = ::rustango::core::DeleteQuery {
2091 model: <Self as ::rustango::core::Model>::SCHEMA,
2092 where_clause: ::rustango::core::WhereExpr::Predicate(
2093 ::rustango::core::Filter {
2094 column: #pk_column_lit,
2095 op: ::rustango::core::Op::Eq,
2096 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2097 ::core::clone::Clone::clone(&self.#pk_ident)
2098 ),
2099 }
2100 ),
2101 };
2102 let _audit_entry = ::rustango::audit::PendingEntry {
2103 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2104 entity_pk: #pk_str,
2105 operation: ::rustango::audit::AuditOp::Delete,
2106 source: ::rustango::audit::current_source(),
2107 changes: ::rustango::audit::snapshot_changes(&[
2108 #( #pairs ),*
2109 ]),
2110 };
2111 ::rustango::audit::delete_one_with_audit_pool(
2112 pool, &_query, &_audit_entry,
2113 ).await
2114 }
2115 }
2116 } else {
2117 quote! {
2118 pub async fn delete_pool(
2125 &self,
2126 pool: &::rustango::sql::Pool,
2127 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2128 let _query = ::rustango::core::DeleteQuery {
2129 model: <Self as ::rustango::core::Model>::SCHEMA,
2130 where_clause: ::rustango::core::WhereExpr::Predicate(
2131 ::rustango::core::Filter {
2132 column: #pk_column_lit,
2133 op: ::rustango::core::Op::Eq,
2134 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2135 ::core::clone::Clone::clone(&self.#pk_ident)
2136 ),
2137 }
2138 ),
2139 };
2140 ::rustango::sql::delete_pool(pool, &_query).await
2141 }
2142 }
2143 }
2144 } else {
2145 quote!()
2146 }
2147 };
2148
2149 let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
2159 audited_fields
2160 {
2161 if tracked.is_empty() {
2162 (quote!(), quote!())
2163 } else {
2164 let select_cols: String = tracked
2165 .iter()
2166 .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
2167 .collect::<Vec<_>>()
2168 .join(", ");
2169 let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
2170 let select_cols_lit = select_cols;
2171 let pk_column_lit_for_select = pk_column_for_select;
2172 let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
2173 if fields.pk_is_auto {
2174 quote!(self.#pk_ident.get().copied().unwrap_or_default())
2175 } else {
2176 quote!(::core::clone::Clone::clone(&self.#pk_ident))
2177 }
2178 } else {
2179 quote!(0_i64)
2180 };
2181 let before_pairs = tracked.iter().map(|c| {
2182 let column_lit = c.column.as_str();
2183 let value_ty = &c.value_ty;
2184 quote! {
2185 (
2186 #column_lit,
2187 match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
2188 &_audit_before_row, #column_lit,
2189 ) {
2190 ::core::result::Result::Ok(v) => {
2191 ::serde_json::to_value(&v)
2192 .unwrap_or(::serde_json::Value::Null)
2193 }
2194 ::core::result::Result::Err(_) => ::serde_json::Value::Null,
2195 },
2196 )
2197 }
2198 });
2199 let after_pairs = tracked.iter().map(|c| {
2200 let column_lit = c.column.as_str();
2201 let ident = &c.ident;
2202 quote! {
2203 (
2204 #column_lit,
2205 ::serde_json::to_value(&self.#ident)
2206 .unwrap_or(::serde_json::Value::Null),
2207 )
2208 }
2209 });
2210 let pk_str = audit_pk_to_string.clone();
2211 let pre = quote! {
2212 let _audit_select_sql = ::std::format!(
2213 r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2214 #select_cols_lit,
2215 <Self as ::rustango::core::Model>::SCHEMA.table,
2216 #pk_column_lit_for_select,
2217 );
2218 let _audit_before_pairs:
2219 ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2220 match ::rustango::sql::sqlx::query(&_audit_select_sql)
2221 .bind(#pk_value_for_bind)
2222 .fetch_optional(&mut *_executor)
2223 .await
2224 {
2225 ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2226 ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2227 }
2228 _ => ::core::option::Option::None,
2229 };
2230 };
2231 let post = quote! {
2232 if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2233 let _audit_after:
2234 ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2235 ::std::vec![ #( #after_pairs ),* ];
2236 let _audit_entry = ::rustango::audit::PendingEntry {
2237 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2238 entity_pk: #pk_str,
2239 operation: ::rustango::audit::AuditOp::Update,
2240 source: ::rustango::audit::current_source(),
2241 changes: ::rustango::audit::diff_changes(
2242 &_audit_before,
2243 &_audit_after,
2244 ),
2245 };
2246 ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2247 }
2248 };
2249 (pre, post)
2250 }
2251 } else {
2252 (quote!(), quote!())
2253 };
2254
2255 let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2259 let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2260 if fields.pk_is_auto {
2261 quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2262 } else {
2263 quote!(::std::format!("{}", &_row.#pk_ident))
2264 }
2265 } else {
2266 quote!(::std::string::String::new())
2267 };
2268 let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
2269 let column_lit = c.column.as_str();
2270 let ident = &c.ident;
2271 quote! {
2272 (
2273 #column_lit,
2274 ::serde_json::to_value(&_row.#ident)
2275 .unwrap_or(::serde_json::Value::Null),
2276 )
2277 }
2278 });
2279 quote! {
2280 let _audit_source = ::rustango::audit::current_source();
2281 let mut _audit_entries:
2282 ::std::vec::Vec<::rustango::audit::PendingEntry> =
2283 ::std::vec::Vec::with_capacity(rows.len());
2284 for _row in rows.iter() {
2285 _audit_entries.push(::rustango::audit::PendingEntry {
2286 entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2287 entity_pk: #row_pk_str,
2288 operation: ::rustango::audit::AuditOp::Create,
2289 source: _audit_source.clone(),
2290 changes: ::rustango::audit::snapshot_changes(&[
2291 #( #row_pairs ),*
2292 ]),
2293 });
2294 }
2295 ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2296 }
2297 } else {
2298 quote!()
2299 };
2300
2301 let save_method = if fields.pk_is_auto {
2302 let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
2303 let pk_column_lit = pk_column.as_str();
2304 let assignments = &fields.update_assignments;
2305 let upsert_cols = &fields.upsert_update_columns;
2306 let upsert_pushes = &fields.insert_pushes;
2307 let upsert_returning = &fields.returning_cols;
2308 let upsert_auto_assigns = &fields.auto_assigns;
2309 let upsert_target_columns: Vec<String> = indexes
2318 .iter()
2319 .find(|i| i.unique && !i.columns.is_empty())
2320 .map(|i| i.columns.clone())
2321 .unwrap_or_else(|| vec![pk_column.clone()]);
2322 let upsert_target_lits = upsert_target_columns
2323 .iter()
2324 .map(String::as_str)
2325 .collect::<Vec<_>>();
2326 let conflict_clause = if fields.upsert_update_columns.is_empty() {
2327 quote!(::rustango::core::ConflictClause::DoNothing)
2328 } else {
2329 quote!(::rustango::core::ConflictClause::DoUpdate {
2330 target: ::std::vec![ #( #upsert_target_lits ),* ],
2331 update_columns: ::std::vec![ #( #upsert_cols ),* ],
2332 })
2333 };
2334 Some(quote! {
2335 pub async fn save(
2353 &mut self,
2354 pool: &::rustango::sql::sqlx::PgPool,
2355 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2356 #pool_to_save_on
2357 }
2358
2359 pub async fn save_on #executor_generics (
2370 &mut self,
2371 #executor_param,
2372 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2373 #executor_where
2374 {
2375 if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2376 return self.insert_on(#executor_passes_to_data_write).await;
2377 }
2378 #audit_update_pre
2379 let _query = ::rustango::core::UpdateQuery {
2380 model: <Self as ::rustango::core::Model>::SCHEMA,
2381 set: ::std::vec![ #( #assignments ),* ],
2382 where_clause: ::rustango::core::WhereExpr::Predicate(
2383 ::rustango::core::Filter {
2384 column: #pk_column_lit,
2385 op: ::rustango::core::Op::Eq,
2386 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2387 ::core::clone::Clone::clone(&self.#pk_ident)
2388 ),
2389 }
2390 ),
2391 };
2392 let _ = ::rustango::sql::update_on(
2393 #executor_passes_to_data_write,
2394 &_query,
2395 ).await?;
2396 #audit_update_post
2397 ::core::result::Result::Ok(())
2398 }
2399
2400 pub async fn save_on_with #executor_generics (
2411 &mut self,
2412 #executor_param,
2413 source: ::rustango::audit::AuditSource,
2414 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2415 #executor_where
2416 {
2417 ::rustango::audit::with_source(source, self.save_on(_executor)).await
2418 }
2419
2420 pub async fn upsert(
2430 &mut self,
2431 pool: &::rustango::sql::sqlx::PgPool,
2432 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2433 #pool_to_upsert_on
2434 }
2435
2436 pub async fn upsert_on #executor_generics (
2442 &mut self,
2443 #executor_param,
2444 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2445 #executor_where
2446 {
2447 let mut _columns: ::std::vec::Vec<&'static str> =
2448 ::std::vec::Vec::new();
2449 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2450 ::std::vec::Vec::new();
2451 #( #upsert_pushes )*
2452 let query = ::rustango::core::InsertQuery {
2453 model: <Self as ::rustango::core::Model>::SCHEMA,
2454 columns: _columns,
2455 values: _values,
2456 returning: ::std::vec![ #( #upsert_returning ),* ],
2457 on_conflict: ::core::option::Option::Some(#conflict_clause),
2458 };
2459 let _returning_row_v = ::rustango::sql::insert_returning_on(
2460 #executor_passes_to_data_write,
2461 &query,
2462 ).await?;
2463 let _returning_row = &_returning_row_v;
2464 #( #upsert_auto_assigns )*
2465 ::core::result::Result::Ok(())
2466 }
2467 })
2468 } else {
2469 None
2470 };
2471
2472 let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2473 let pk_column_lit = pk_column.as_str();
2474 let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2481 let col_lit = col;
2482 quote! {
2483 pub async fn soft_delete_on #executor_generics (
2493 &self,
2494 #executor_param,
2495 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2496 #executor_where
2497 {
2498 let _query = ::rustango::core::UpdateQuery {
2499 model: <Self as ::rustango::core::Model>::SCHEMA,
2500 set: ::std::vec![
2501 ::rustango::core::Assignment {
2502 column: #col_lit,
2503 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2504 ::chrono::Utc::now()
2505 ),
2506 },
2507 ],
2508 where_clause: ::rustango::core::WhereExpr::Predicate(
2509 ::rustango::core::Filter {
2510 column: #pk_column_lit,
2511 op: ::rustango::core::Op::Eq,
2512 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2513 ::core::clone::Clone::clone(&self.#pk_ident)
2514 ),
2515 }
2516 ),
2517 };
2518 let _affected = ::rustango::sql::update_on(
2519 #executor_passes_to_data_write,
2520 &_query,
2521 ).await?;
2522 #audit_softdelete_emit
2523 ::core::result::Result::Ok(_affected)
2524 }
2525
2526 pub async fn restore_on #executor_generics (
2533 &self,
2534 #executor_param,
2535 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2536 #executor_where
2537 {
2538 let _query = ::rustango::core::UpdateQuery {
2539 model: <Self as ::rustango::core::Model>::SCHEMA,
2540 set: ::std::vec![
2541 ::rustango::core::Assignment {
2542 column: #col_lit,
2543 value: ::rustango::core::SqlValue::Null,
2544 },
2545 ],
2546 where_clause: ::rustango::core::WhereExpr::Predicate(
2547 ::rustango::core::Filter {
2548 column: #pk_column_lit,
2549 op: ::rustango::core::Op::Eq,
2550 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2551 ::core::clone::Clone::clone(&self.#pk_ident)
2552 ),
2553 }
2554 ),
2555 };
2556 let _affected = ::rustango::sql::update_on(
2557 #executor_passes_to_data_write,
2558 &_query,
2559 ).await?;
2560 #audit_restore_emit
2561 ::core::result::Result::Ok(_affected)
2562 }
2563 }
2564 } else {
2565 quote!()
2566 };
2567 quote! {
2568 pub async fn delete(
2576 &self,
2577 pool: &::rustango::sql::sqlx::PgPool,
2578 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2579 #pool_to_delete_on
2580 }
2581
2582 pub async fn delete_on #executor_generics (
2589 &self,
2590 #executor_param,
2591 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2592 #executor_where
2593 {
2594 let query = ::rustango::core::DeleteQuery {
2595 model: <Self as ::rustango::core::Model>::SCHEMA,
2596 where_clause: ::rustango::core::WhereExpr::Predicate(
2597 ::rustango::core::Filter {
2598 column: #pk_column_lit,
2599 op: ::rustango::core::Op::Eq,
2600 value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2601 ::core::clone::Clone::clone(&self.#pk_ident)
2602 ),
2603 }
2604 ),
2605 };
2606 let _affected = ::rustango::sql::delete_on(
2607 #executor_passes_to_data_write,
2608 &query,
2609 ).await?;
2610 #audit_delete_emit
2611 ::core::result::Result::Ok(_affected)
2612 }
2613
2614 pub async fn delete_on_with #executor_generics (
2620 &self,
2621 #executor_param,
2622 source: ::rustango::audit::AuditSource,
2623 ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2624 #executor_where
2625 {
2626 ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2627 }
2628 #pool_delete_method
2629 #pool_insert_method
2630 #pool_save_method
2631 #soft_delete_methods
2632 }
2633 });
2634
2635 let insert_method = if fields.has_auto {
2636 let pushes = &fields.insert_pushes;
2637 let returning_cols = &fields.returning_cols;
2638 let auto_assigns = &fields.auto_assigns;
2639 quote! {
2640 pub async fn insert(
2649 &mut self,
2650 pool: &::rustango::sql::sqlx::PgPool,
2651 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2652 #pool_to_insert_on
2653 }
2654
2655 pub async fn insert_on #executor_generics (
2661 &mut self,
2662 #executor_param,
2663 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2664 #executor_where
2665 {
2666 let mut _columns: ::std::vec::Vec<&'static str> =
2667 ::std::vec::Vec::new();
2668 let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2669 ::std::vec::Vec::new();
2670 #( #pushes )*
2671 let query = ::rustango::core::InsertQuery {
2672 model: <Self as ::rustango::core::Model>::SCHEMA,
2673 columns: _columns,
2674 values: _values,
2675 returning: ::std::vec![ #( #returning_cols ),* ],
2676 on_conflict: ::core::option::Option::None,
2677 };
2678 let _returning_row_v = ::rustango::sql::insert_returning_on(
2679 #executor_passes_to_data_write,
2680 &query,
2681 ).await?;
2682 let _returning_row = &_returning_row_v;
2683 #( #auto_assigns )*
2684 #audit_insert_emit
2685 ::core::result::Result::Ok(())
2686 }
2687
2688 pub async fn insert_on_with #executor_generics (
2694 &mut self,
2695 #executor_param,
2696 source: ::rustango::audit::AuditSource,
2697 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2698 #executor_where
2699 {
2700 ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2701 }
2702 }
2703 } else {
2704 let insert_columns = &fields.insert_columns;
2705 let insert_values = &fields.insert_values;
2706 quote! {
2707 pub async fn insert(
2713 &self,
2714 pool: &::rustango::sql::sqlx::PgPool,
2715 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2716 self.insert_on(pool).await
2717 }
2718
2719 pub async fn insert_on<'_c, _E>(
2725 &self,
2726 _executor: _E,
2727 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2728 where
2729 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2730 {
2731 let query = ::rustango::core::InsertQuery {
2732 model: <Self as ::rustango::core::Model>::SCHEMA,
2733 columns: ::std::vec![ #( #insert_columns ),* ],
2734 values: ::std::vec![ #( #insert_values ),* ],
2735 returning: ::std::vec::Vec::new(),
2736 on_conflict: ::core::option::Option::None,
2737 };
2738 ::rustango::sql::insert_on(_executor, &query).await
2739 }
2740 }
2741 };
2742
2743 let bulk_insert_method = if fields.has_auto {
2744 let cols_no_auto = &fields.bulk_columns_no_auto;
2745 let cols_all = &fields.bulk_columns_all;
2746 let pushes_no_auto = &fields.bulk_pushes_no_auto;
2747 let pushes_all = &fields.bulk_pushes_all;
2748 let returning_cols = &fields.returning_cols;
2749 let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2750 let uniformity = &fields.bulk_auto_uniformity;
2751 let first_auto_ident = fields
2752 .first_auto_ident
2753 .as_ref()
2754 .expect("has_auto implies first_auto_ident is Some");
2755 quote! {
2756 pub async fn bulk_insert(
2770 rows: &mut [Self],
2771 pool: &::rustango::sql::sqlx::PgPool,
2772 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2773 #pool_to_bulk_insert_on
2774 }
2775
2776 pub async fn bulk_insert_on #executor_generics (
2782 rows: &mut [Self],
2783 #executor_param,
2784 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2785 #executor_where
2786 {
2787 if rows.is_empty() {
2788 return ::core::result::Result::Ok(());
2789 }
2790 let _first_unset = matches!(
2791 rows[0].#first_auto_ident,
2792 ::rustango::sql::Auto::Unset
2793 );
2794 #( #uniformity )*
2795
2796 let mut _all_rows: ::std::vec::Vec<
2797 ::std::vec::Vec<::rustango::core::SqlValue>,
2798 > = ::std::vec::Vec::with_capacity(rows.len());
2799 let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2800 for _row in rows.iter() {
2801 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2802 ::std::vec::Vec::new();
2803 #( #pushes_no_auto )*
2804 _all_rows.push(_row_vals);
2805 }
2806 ::std::vec![ #( #cols_no_auto ),* ]
2807 } else {
2808 for _row in rows.iter() {
2809 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2810 ::std::vec::Vec::new();
2811 #( #pushes_all )*
2812 _all_rows.push(_row_vals);
2813 }
2814 ::std::vec![ #( #cols_all ),* ]
2815 };
2816
2817 let _query = ::rustango::core::BulkInsertQuery {
2818 model: <Self as ::rustango::core::Model>::SCHEMA,
2819 columns: _columns,
2820 rows: _all_rows,
2821 returning: ::std::vec![ #( #returning_cols ),* ],
2822 on_conflict: ::core::option::Option::None,
2823 };
2824 let _returned = ::rustango::sql::bulk_insert_on(
2825 #executor_passes_to_data_write,
2826 &_query,
2827 ).await?;
2828 if _returned.len() != rows.len() {
2829 return ::core::result::Result::Err(
2830 ::rustango::sql::ExecError::Sql(
2831 ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2832 expected: rows.len(),
2833 actual: _returned.len(),
2834 }
2835 )
2836 );
2837 }
2838 for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2839 #auto_assigns_for_row
2840 }
2841 #audit_bulk_insert_emit
2842 ::core::result::Result::Ok(())
2843 }
2844 }
2845 } else {
2846 let cols_all = &fields.bulk_columns_all;
2847 let pushes_all = &fields.bulk_pushes_all;
2848 quote! {
2849 pub async fn bulk_insert(
2859 rows: &[Self],
2860 pool: &::rustango::sql::sqlx::PgPool,
2861 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2862 Self::bulk_insert_on(rows, pool).await
2863 }
2864
2865 pub async fn bulk_insert_on<'_c, _E>(
2871 rows: &[Self],
2872 _executor: _E,
2873 ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2874 where
2875 _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2876 {
2877 if rows.is_empty() {
2878 return ::core::result::Result::Ok(());
2879 }
2880 let mut _all_rows: ::std::vec::Vec<
2881 ::std::vec::Vec<::rustango::core::SqlValue>,
2882 > = ::std::vec::Vec::with_capacity(rows.len());
2883 for _row in rows.iter() {
2884 let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2885 ::std::vec::Vec::new();
2886 #( #pushes_all )*
2887 _all_rows.push(_row_vals);
2888 }
2889 let _query = ::rustango::core::BulkInsertQuery {
2890 model: <Self as ::rustango::core::Model>::SCHEMA,
2891 columns: ::std::vec![ #( #cols_all ),* ],
2892 rows: _all_rows,
2893 returning: ::std::vec::Vec::new(),
2894 on_conflict: ::core::option::Option::None,
2895 };
2896 let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2897 ::core::result::Result::Ok(())
2898 }
2899 }
2900 };
2901
2902 let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2903 quote! {
2904 #[doc(hidden)]
2909 pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2910 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2911 ::core::clone::Clone::clone(&self.#pk_ident)
2912 )
2913 }
2914 }
2915 });
2916
2917 let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2918 quote! {
2919 impl ::rustango::sql::HasPkValue for #struct_name {
2920 fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2921 ::core::convert::Into::<::rustango::core::SqlValue>::into(
2922 ::core::clone::Clone::clone(&self.#pk_ident)
2923 )
2924 }
2925 }
2926 }
2927 });
2928
2929 let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2930
2931 let assign_auto_pk_pool_impl = {
2937 let auto_assigns = &fields.auto_assigns;
2938 let auto_assigns_sqlite: Vec<TokenStream2> = fields
2944 .auto_field_idents
2945 .iter()
2946 .map(|(ident, column)| {
2947 quote! {
2948 self.#ident = ::rustango::sql::try_get_returning_sqlite(
2949 _returning_row, #column
2950 )?;
2951 }
2952 })
2953 .collect();
2954 let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
2955 let value_ty = fields
2973 .first_auto_value_ty
2974 .as_ref()
2975 .expect("first_auto_value_ty set whenever first_auto_ident is");
2976 quote! {
2977 let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
2978 ::rustango_from_mysql_auto_id(_id)?;
2979 self.#first = ::rustango::sql::Auto::Set(_converted);
2980 ::core::result::Result::Ok(())
2981 }
2982 } else {
2983 quote! {
2984 let _ = _id;
2985 ::core::result::Result::Ok(())
2986 }
2987 };
2988 quote! {
2989 impl ::rustango::sql::AssignAutoPkPool for #struct_name {
2990 fn __rustango_assign_from_pg_row(
2991 &mut self,
2992 _returning_row: &::rustango::sql::PgReturningRow,
2993 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2994 #( #auto_assigns )*
2995 ::core::result::Result::Ok(())
2996 }
2997 fn __rustango_assign_from_mysql_id(
2998 &mut self,
2999 _id: i64,
3000 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3001 #mysql_body
3002 }
3003 fn __rustango_assign_from_sqlite_row(
3004 &mut self,
3005 _returning_row: &::rustango::sql::SqliteReturningRow,
3006 ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
3007 #( #auto_assigns_sqlite )*
3008 ::core::result::Result::Ok(())
3009 }
3010 }
3011 }
3012 };
3013
3014 let from_aliased_row_inits = &fields.from_aliased_row_inits;
3015 let aliased_row_helper = quote! {
3016 #[doc(hidden)]
3022 pub fn __rustango_from_aliased_row(
3023 row: &::rustango::sql::sqlx::postgres::PgRow,
3024 prefix: &str,
3025 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3026 ::core::result::Result::Ok(Self {
3027 #( #from_aliased_row_inits ),*
3028 })
3029 }
3030 };
3031 let aliased_row_helper_my = quote! {
3034 ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
3035 #( #from_aliased_row_inits ),*
3036 });
3037 };
3038
3039 let aliased_row_helper_sqlite = quote! {
3042 ::rustango::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
3043 #( #from_aliased_row_inits ),*
3044 });
3045 };
3046
3047 let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
3048 let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
3049 let load_related_impl_sqlite =
3050 load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
3051
3052 quote! {
3053 impl #struct_name {
3054 #[must_use]
3056 pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
3057 ::rustango::query::QuerySet::new()
3058 }
3059
3060 #insert_method
3061
3062 #bulk_insert_method
3063
3064 #save_method
3065
3066 #pk_methods
3067
3068 #pk_value_helper
3069
3070 #aliased_row_helper
3071
3072 #column_consts
3073 }
3074
3075 #aliased_row_helper_my
3076
3077 #aliased_row_helper_sqlite
3078
3079 #load_related_impl
3080
3081 #load_related_impl_my
3082
3083 #load_related_impl_sqlite
3084
3085 #has_pk_value_impl
3086
3087 #fk_pk_access_impl
3088
3089 #assign_auto_pk_pool_impl
3090 }
3091}
3092
3093fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
3097 let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
3098 let col_lit = column.as_str();
3099 quote! {
3100 _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
3101 _returning_row,
3102 #col_lit,
3103 )?;
3104 }
3105 });
3106 quote! { #( #lines )* }
3107}
3108
3109fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
3111 let lines = entries.iter().map(|e| {
3112 let ident = &e.ident;
3113 let col_ty = column_type_ident(ident);
3114 quote! {
3115 #[allow(non_upper_case_globals)]
3116 pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
3117 }
3118 });
3119 quote! { #(#lines)* }
3120}
3121
3122fn column_module_tokens(
3125 module_ident: &syn::Ident,
3126 struct_name: &syn::Ident,
3127 entries: &[ColumnEntry],
3128) -> TokenStream2 {
3129 let items = entries.iter().map(|e| {
3130 let col_ty = column_type_ident(&e.ident);
3131 let value_ty = &e.value_ty;
3132 let name = &e.name;
3133 let column = &e.column;
3134 let field_type_tokens = &e.field_type_tokens;
3135 quote! {
3136 #[derive(::core::clone::Clone, ::core::marker::Copy)]
3137 pub struct #col_ty;
3138
3139 impl ::rustango::core::Column for #col_ty {
3140 type Model = super::#struct_name;
3141 type Value = #value_ty;
3142 const NAME: &'static str = #name;
3143 const COLUMN: &'static str = #column;
3144 const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
3145 }
3146 }
3147 });
3148 quote! {
3149 #[doc(hidden)]
3150 #[allow(non_camel_case_types, non_snake_case)]
3151 pub mod #module_ident {
3152 #[allow(unused_imports)]
3157 use super::*;
3158 #(#items)*
3159 }
3160 }
3161}
3162
3163fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
3164 syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
3165}
3166
3167fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
3168 syn::Ident::new(
3169 &format!("__rustango_cols_{struct_name}"),
3170 struct_name.span(),
3171 )
3172}
3173
3174fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
3175 quote! {
3186 impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
3187 for #struct_name
3188 {
3189 fn from_row(
3190 row: &'r ::rustango::sql::sqlx::postgres::PgRow,
3191 ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
3192 ::core::result::Result::Ok(Self {
3193 #( #from_row_inits ),*
3194 })
3195 }
3196 }
3197
3198 ::rustango::__impl_my_from_row!(#struct_name, |row| {
3199 #( #from_row_inits ),*
3200 });
3201
3202 ::rustango::__impl_sqlite_from_row!(#struct_name, |row| {
3203 #( #from_row_inits ),*
3204 });
3205 }
3206}
3207
3208struct ContainerAttrs {
3209 table: Option<String>,
3210 display: Option<(String, proc_macro2::Span)>,
3211 app: Option<String>,
3218 admin: Option<AdminAttrs>,
3223 audit: Option<AuditAttrs>,
3229 permissions: bool,
3233 m2m: Vec<M2MAttr>,
3237 indexes: Vec<IndexAttr>,
3243 checks: Vec<CheckAttr>,
3246 composite_fks: Vec<CompositeFkAttr>,
3250 generic_fks: Vec<GenericFkAttr>,
3254 scope: Option<String>,
3260}
3261
3262struct IndexAttr {
3264 name: Option<String>,
3266 columns: Vec<String>,
3268 unique: bool,
3270}
3271
3272struct CheckAttr {
3274 name: String,
3275 expr: String,
3276}
3277
3278struct CompositeFkAttr {
3284 name: String,
3286 to: String,
3288 from: Vec<String>,
3290 on: Vec<String>,
3292}
3293
3294struct GenericFkAttr {
3299 name: String,
3301 ct_column: String,
3303 pk_column: String,
3305}
3306
3307struct M2MAttr {
3309 name: String,
3311 to: String,
3313 through: String,
3315 src: String,
3317 dst: String,
3319}
3320
3321#[derive(Default)]
3327struct AuditAttrs {
3328 track: Option<(Vec<String>, proc_macro2::Span)>,
3332}
3333
3334#[derive(Default)]
3339struct AdminAttrs {
3340 list_display: Option<(Vec<String>, proc_macro2::Span)>,
3341 search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3342 list_per_page: Option<usize>,
3343 ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3344 readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3345 list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3346 actions: Option<(Vec<String>, proc_macro2::Span)>,
3349 fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3353}
3354
3355fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3356 let mut out = ContainerAttrs {
3357 table: None,
3358 display: None,
3359 app: None,
3360 admin: None,
3361 audit: None,
3362 permissions: true,
3371 m2m: Vec::new(),
3372 indexes: Vec::new(),
3373 checks: Vec::new(),
3374 composite_fks: Vec::new(),
3375 generic_fks: Vec::new(),
3376 scope: None,
3377 };
3378 for attr in &input.attrs {
3379 if !attr.path().is_ident("rustango") {
3380 continue;
3381 }
3382 attr.parse_nested_meta(|meta| {
3383 if meta.path.is_ident("table") {
3384 let s: LitStr = meta.value()?.parse()?;
3385 let name = s.value();
3386 validate_table_name(&name, s.span())?;
3396 out.table = Some(name);
3397 return Ok(());
3398 }
3399 if meta.path.is_ident("display") {
3400 let s: LitStr = meta.value()?.parse()?;
3401 out.display = Some((s.value(), s.span()));
3402 return Ok(());
3403 }
3404 if meta.path.is_ident("app") {
3405 let s: LitStr = meta.value()?.parse()?;
3406 out.app = Some(s.value());
3407 return Ok(());
3408 }
3409 if meta.path.is_ident("scope") {
3410 let s: LitStr = meta.value()?.parse()?;
3411 let val = s.value();
3412 if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
3413 return Err(meta.error(format!(
3414 "`scope` must be \"registry\" or \"tenant\", got {val:?}"
3415 )));
3416 }
3417 out.scope = Some(val);
3418 return Ok(());
3419 }
3420 if meta.path.is_ident("admin") {
3421 let mut admin = AdminAttrs::default();
3422 meta.parse_nested_meta(|inner| {
3423 if inner.path.is_ident("list_display") {
3424 let s: LitStr = inner.value()?.parse()?;
3425 admin.list_display =
3426 Some((split_field_list(&s.value()), s.span()));
3427 return Ok(());
3428 }
3429 if inner.path.is_ident("search_fields") {
3430 let s: LitStr = inner.value()?.parse()?;
3431 admin.search_fields =
3432 Some((split_field_list(&s.value()), s.span()));
3433 return Ok(());
3434 }
3435 if inner.path.is_ident("readonly_fields") {
3436 let s: LitStr = inner.value()?.parse()?;
3437 admin.readonly_fields =
3438 Some((split_field_list(&s.value()), s.span()));
3439 return Ok(());
3440 }
3441 if inner.path.is_ident("list_per_page") {
3442 let lit: syn::LitInt = inner.value()?.parse()?;
3443 admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3444 return Ok(());
3445 }
3446 if inner.path.is_ident("ordering") {
3447 let s: LitStr = inner.value()?.parse()?;
3448 admin.ordering = Some((
3449 parse_ordering_list(&s.value()),
3450 s.span(),
3451 ));
3452 return Ok(());
3453 }
3454 if inner.path.is_ident("list_filter") {
3455 let s: LitStr = inner.value()?.parse()?;
3456 admin.list_filter =
3457 Some((split_field_list(&s.value()), s.span()));
3458 return Ok(());
3459 }
3460 if inner.path.is_ident("actions") {
3461 let s: LitStr = inner.value()?.parse()?;
3462 admin.actions =
3463 Some((split_field_list(&s.value()), s.span()));
3464 return Ok(());
3465 }
3466 if inner.path.is_ident("fieldsets") {
3467 let s: LitStr = inner.value()?.parse()?;
3468 admin.fieldsets =
3469 Some((parse_fieldset_list(&s.value()), s.span()));
3470 return Ok(());
3471 }
3472 Err(inner.error(
3473 "unknown admin attribute (supported: \
3474 `list_display`, `search_fields`, `readonly_fields`, \
3475 `list_filter`, `list_per_page`, `ordering`, `actions`, \
3476 `fieldsets`)",
3477 ))
3478 })?;
3479 out.admin = Some(admin);
3480 return Ok(());
3481 }
3482 if meta.path.is_ident("audit") {
3483 let mut audit = AuditAttrs::default();
3484 meta.parse_nested_meta(|inner| {
3485 if inner.path.is_ident("track") {
3486 let s: LitStr = inner.value()?.parse()?;
3487 audit.track =
3488 Some((split_field_list(&s.value()), s.span()));
3489 return Ok(());
3490 }
3491 Err(inner.error(
3492 "unknown audit attribute (supported: `track`)",
3493 ))
3494 })?;
3495 out.audit = Some(audit);
3496 return Ok(());
3497 }
3498 if meta.path.is_ident("permissions") {
3499 if let Ok(v) = meta.value() {
3504 let lit: syn::LitBool = v.parse()?;
3505 out.permissions = lit.value;
3506 } else {
3507 out.permissions = true;
3508 }
3509 return Ok(());
3510 }
3511 if meta.path.is_ident("unique_together") {
3512 let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3521 out.indexes.push(IndexAttr { name, columns, unique: true });
3522 return Ok(());
3523 }
3524 if meta.path.is_ident("index_together") {
3525 let (columns, name) = parse_together_attr(&meta, "index_together")?;
3531 out.indexes.push(IndexAttr { name, columns, unique: false });
3532 return Ok(());
3533 }
3534 if meta.path.is_ident("index") {
3535 let cols_lit: LitStr = meta.value()?.parse()?;
3543 let columns = split_field_list(&cols_lit.value());
3544 out.indexes.push(IndexAttr { name: None, columns, unique: false });
3545 return Ok(());
3546 }
3547 if meta.path.is_ident("check") {
3548 let mut name: Option<String> = None;
3550 let mut expr: Option<String> = None;
3551 meta.parse_nested_meta(|inner| {
3552 if inner.path.is_ident("name") {
3553 let s: LitStr = inner.value()?.parse()?;
3554 name = Some(s.value());
3555 return Ok(());
3556 }
3557 if inner.path.is_ident("expr") {
3558 let s: LitStr = inner.value()?.parse()?;
3559 expr = Some(s.value());
3560 return Ok(());
3561 }
3562 Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3563 })?;
3564 let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3565 let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3566 out.checks.push(CheckAttr { name, expr });
3567 return Ok(());
3568 }
3569 if meta.path.is_ident("generic_fk") {
3570 let mut gfk = GenericFkAttr {
3571 name: String::new(),
3572 ct_column: String::new(),
3573 pk_column: String::new(),
3574 };
3575 meta.parse_nested_meta(|inner| {
3576 if inner.path.is_ident("name") {
3577 let s: LitStr = inner.value()?.parse()?;
3578 gfk.name = s.value();
3579 return Ok(());
3580 }
3581 if inner.path.is_ident("ct_column") {
3582 let s: LitStr = inner.value()?.parse()?;
3583 gfk.ct_column = s.value();
3584 return Ok(());
3585 }
3586 if inner.path.is_ident("pk_column") {
3587 let s: LitStr = inner.value()?.parse()?;
3588 gfk.pk_column = s.value();
3589 return Ok(());
3590 }
3591 Err(inner.error(
3592 "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3593 ))
3594 })?;
3595 if gfk.name.is_empty() {
3596 return Err(meta.error("generic_fk requires `name = \"...\"`"));
3597 }
3598 if gfk.ct_column.is_empty() {
3599 return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3600 }
3601 if gfk.pk_column.is_empty() {
3602 return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3603 }
3604 out.generic_fks.push(gfk);
3605 return Ok(());
3606 }
3607 if meta.path.is_ident("fk_composite") {
3608 let mut fk = CompositeFkAttr {
3609 name: String::new(),
3610 to: String::new(),
3611 from: Vec::new(),
3612 on: Vec::new(),
3613 };
3614 meta.parse_nested_meta(|inner| {
3615 if inner.path.is_ident("name") {
3616 let s: LitStr = inner.value()?.parse()?;
3617 fk.name = s.value();
3618 return Ok(());
3619 }
3620 if inner.path.is_ident("to") {
3621 let s: LitStr = inner.value()?.parse()?;
3622 fk.to = s.value();
3623 return Ok(());
3624 }
3625 if inner.path.is_ident("on") || inner.path.is_ident("from") {
3628 let value = inner.value()?;
3629 let content;
3630 syn::parenthesized!(content in value);
3631 let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3632 content.parse_terminated(
3633 |p| p.parse::<syn::LitStr>(),
3634 syn::Token![,],
3635 )?;
3636 let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3637 if inner.path.is_ident("on") {
3638 fk.on = cols;
3639 } else {
3640 fk.from = cols;
3641 }
3642 return Ok(());
3643 }
3644 Err(inner.error(
3645 "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3646 ))
3647 })?;
3648 if fk.name.is_empty() {
3649 return Err(meta.error("fk_composite requires `name = \"...\"`"));
3650 }
3651 if fk.to.is_empty() {
3652 return Err(meta.error("fk_composite requires `to = \"...\"`"));
3653 }
3654 if fk.from.is_empty() || fk.on.is_empty() {
3655 return Err(meta.error(
3656 "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3657 ));
3658 }
3659 if fk.from.len() != fk.on.len() {
3660 return Err(meta.error(format!(
3661 "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3662 fk.from.len(),
3663 fk.on.len(),
3664 )));
3665 }
3666 out.composite_fks.push(fk);
3667 return Ok(());
3668 }
3669 if meta.path.is_ident("m2m") {
3670 let mut m2m = M2MAttr {
3671 name: String::new(),
3672 to: String::new(),
3673 through: String::new(),
3674 src: String::new(),
3675 dst: String::new(),
3676 };
3677 meta.parse_nested_meta(|inner| {
3678 if inner.path.is_ident("name") {
3679 let s: LitStr = inner.value()?.parse()?;
3680 m2m.name = s.value();
3681 return Ok(());
3682 }
3683 if inner.path.is_ident("to") {
3684 let s: LitStr = inner.value()?.parse()?;
3685 m2m.to = s.value();
3686 return Ok(());
3687 }
3688 if inner.path.is_ident("through") {
3689 let s: LitStr = inner.value()?.parse()?;
3690 m2m.through = s.value();
3691 return Ok(());
3692 }
3693 if inner.path.is_ident("src") {
3694 let s: LitStr = inner.value()?.parse()?;
3695 m2m.src = s.value();
3696 return Ok(());
3697 }
3698 if inner.path.is_ident("dst") {
3699 let s: LitStr = inner.value()?.parse()?;
3700 m2m.dst = s.value();
3701 return Ok(());
3702 }
3703 Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3704 })?;
3705 if m2m.name.is_empty() {
3706 return Err(meta.error("m2m requires `name = \"...\"`"));
3707 }
3708 if m2m.to.is_empty() {
3709 return Err(meta.error("m2m requires `to = \"...\"`"));
3710 }
3711 if m2m.through.is_empty() {
3712 return Err(meta.error("m2m requires `through = \"...\"`"));
3713 }
3714 if m2m.src.is_empty() {
3715 return Err(meta.error("m2m requires `src = \"...\"`"));
3716 }
3717 if m2m.dst.is_empty() {
3718 return Err(meta.error("m2m requires `dst = \"...\"`"));
3719 }
3720 out.m2m.push(m2m);
3721 return Ok(());
3722 }
3723 Err(meta.error("unknown rustango container attribute"))
3724 })?;
3725 }
3726 Ok(out)
3727}
3728
3729fn split_field_list(raw: &str) -> Vec<String> {
3733 raw.split(',')
3734 .map(str::trim)
3735 .filter(|s| !s.is_empty())
3736 .map(str::to_owned)
3737 .collect()
3738}
3739
3740fn parse_together_attr(
3748 meta: &syn::meta::ParseNestedMeta<'_>,
3749 attr: &str,
3750) -> syn::Result<(Vec<String>, Option<String>)> {
3751 if meta.input.peek(syn::Token![=]) {
3754 let cols_lit: LitStr = meta.value()?.parse()?;
3755 let columns = split_field_list(&cols_lit.value());
3756 check_together_columns(meta, attr, &columns)?;
3757 return Ok((columns, None));
3758 }
3759 let mut columns: Option<Vec<String>> = None;
3760 let mut name: Option<String> = None;
3761 meta.parse_nested_meta(|inner| {
3762 if inner.path.is_ident("columns") {
3763 let s: LitStr = inner.value()?.parse()?;
3764 columns = Some(split_field_list(&s.value()));
3765 return Ok(());
3766 }
3767 if inner.path.is_ident("name") {
3768 let s: LitStr = inner.value()?.parse()?;
3769 name = Some(s.value());
3770 return Ok(());
3771 }
3772 Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3773 })?;
3774 let columns = columns.ok_or_else(|| {
3775 meta.error(format!(
3776 "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3777 ))
3778 })?;
3779 check_together_columns(meta, attr, &columns)?;
3780 Ok((columns, name))
3781}
3782
3783fn check_together_columns(
3784 meta: &syn::meta::ParseNestedMeta<'_>,
3785 attr: &str,
3786 columns: &[String],
3787) -> syn::Result<()> {
3788 if columns.len() < 2 {
3789 let single = if attr == "unique_together" {
3790 "#[rustango(unique)] on the field"
3791 } else {
3792 "#[rustango(index)] on the field"
3793 };
3794 return Err(meta.error(format!(
3795 "{attr} expects two or more columns; for a single-column equivalent use {single}",
3796 )));
3797 }
3798 Ok(())
3799}
3800
3801fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3810 raw.split('|')
3811 .map(str::trim)
3812 .filter(|s| !s.is_empty())
3813 .map(|section| {
3814 let (title, rest) = match section.split_once(':') {
3816 Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
3817 _ => (String::new(), section),
3818 };
3819 let fields = split_field_list(rest);
3820 (title, fields)
3821 })
3822 .collect()
3823}
3824
3825fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3828 raw.split(',')
3829 .map(str::trim)
3830 .filter(|s| !s.is_empty())
3831 .map(|spec| {
3832 spec.strip_prefix('-')
3833 .map_or((spec.to_owned(), false), |rest| {
3834 (rest.trim().to_owned(), true)
3835 })
3836 })
3837 .collect()
3838}
3839
3840struct FieldAttrs {
3841 column: Option<String>,
3842 primary_key: bool,
3843 fk: Option<String>,
3844 o2o: Option<String>,
3845 on: Option<String>,
3846 max_length: Option<u32>,
3847 min: Option<i64>,
3848 max: Option<i64>,
3849 default: Option<String>,
3850 auto_uuid: bool,
3856 auto_now_add: bool,
3861 auto_now: bool,
3867 soft_delete: bool,
3872 unique: bool,
3875 index: bool,
3879 index_unique: bool,
3880 index_name: Option<String>,
3881 generated_as: Option<String>,
3887}
3888
3889fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3890 let mut out = FieldAttrs {
3891 column: None,
3892 primary_key: false,
3893 fk: None,
3894 o2o: None,
3895 on: None,
3896 max_length: None,
3897 min: None,
3898 max: None,
3899 default: None,
3900 auto_uuid: false,
3901 auto_now_add: false,
3902 auto_now: false,
3903 soft_delete: false,
3904 unique: false,
3905 index: false,
3906 index_unique: false,
3907 index_name: None,
3908 generated_as: None,
3909 };
3910 for attr in &field.attrs {
3911 if !attr.path().is_ident("rustango") {
3912 continue;
3913 }
3914 attr.parse_nested_meta(|meta| {
3915 if meta.path.is_ident("column") {
3916 let s: LitStr = meta.value()?.parse()?;
3917 let name = s.value();
3918 validate_sql_identifier(&name, "column", s.span())?;
3919 out.column = Some(name);
3920 return Ok(());
3921 }
3922 if meta.path.is_ident("primary_key") {
3923 out.primary_key = true;
3924 return Ok(());
3925 }
3926 if meta.path.is_ident("fk") {
3927 let s: LitStr = meta.value()?.parse()?;
3928 out.fk = Some(s.value());
3929 return Ok(());
3930 }
3931 if meta.path.is_ident("o2o") {
3932 let s: LitStr = meta.value()?.parse()?;
3933 out.o2o = Some(s.value());
3934 return Ok(());
3935 }
3936 if meta.path.is_ident("on") {
3937 let s: LitStr = meta.value()?.parse()?;
3938 out.on = Some(s.value());
3939 return Ok(());
3940 }
3941 if meta.path.is_ident("max_length") {
3942 let lit: syn::LitInt = meta.value()?.parse()?;
3943 out.max_length = Some(lit.base10_parse::<u32>()?);
3944 return Ok(());
3945 }
3946 if meta.path.is_ident("min") {
3947 out.min = Some(parse_signed_i64(&meta)?);
3948 return Ok(());
3949 }
3950 if meta.path.is_ident("max") {
3951 out.max = Some(parse_signed_i64(&meta)?);
3952 return Ok(());
3953 }
3954 if meta.path.is_ident("default") {
3955 let s: LitStr = meta.value()?.parse()?;
3956 out.default = Some(s.value());
3957 return Ok(());
3958 }
3959 if meta.path.is_ident("generated_as") {
3960 let s: LitStr = meta.value()?.parse()?;
3961 out.generated_as = Some(s.value());
3962 return Ok(());
3963 }
3964 if meta.path.is_ident("auto_uuid") {
3965 out.auto_uuid = true;
3966 out.primary_key = true;
3970 if out.default.is_none() {
3971 out.default = Some("gen_random_uuid()".into());
3972 }
3973 return Ok(());
3974 }
3975 if meta.path.is_ident("auto_now_add") {
3976 out.auto_now_add = true;
3977 if out.default.is_none() {
3978 out.default = Some("now()".into());
3979 }
3980 return Ok(());
3981 }
3982 if meta.path.is_ident("auto_now") {
3983 out.auto_now = true;
3984 if out.default.is_none() {
3985 out.default = Some("now()".into());
3986 }
3987 return Ok(());
3988 }
3989 if meta.path.is_ident("soft_delete") {
3990 out.soft_delete = true;
3991 return Ok(());
3992 }
3993 if meta.path.is_ident("unique") {
3994 out.unique = true;
3995 return Ok(());
3996 }
3997 if meta.path.is_ident("index") {
3998 out.index = true;
3999 if meta.input.peek(syn::token::Paren) {
4001 meta.parse_nested_meta(|inner| {
4002 if inner.path.is_ident("unique") {
4003 out.index_unique = true;
4004 return Ok(());
4005 }
4006 if inner.path.is_ident("name") {
4007 let s: LitStr = inner.value()?.parse()?;
4008 out.index_name = Some(s.value());
4009 return Ok(());
4010 }
4011 Err(inner
4012 .error("unknown index sub-attribute (supported: `unique`, `name`)"))
4013 })?;
4014 }
4015 return Ok(());
4016 }
4017 Err(meta.error("unknown rustango field attribute"))
4018 })?;
4019 }
4020 Ok(out)
4021}
4022
4023fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
4025 let expr: syn::Expr = meta.value()?.parse()?;
4026 match expr {
4027 syn::Expr::Lit(syn::ExprLit {
4028 lit: syn::Lit::Int(lit),
4029 ..
4030 }) => lit.base10_parse::<i64>(),
4031 syn::Expr::Unary(syn::ExprUnary {
4032 op: syn::UnOp::Neg(_),
4033 expr,
4034 ..
4035 }) => {
4036 if let syn::Expr::Lit(syn::ExprLit {
4037 lit: syn::Lit::Int(lit),
4038 ..
4039 }) = *expr
4040 {
4041 let v: i64 = lit.base10_parse()?;
4042 Ok(-v)
4043 } else {
4044 Err(syn::Error::new_spanned(expr, "expected integer literal"))
4045 }
4046 }
4047 other => Err(syn::Error::new_spanned(
4048 other,
4049 "expected integer literal (signed)",
4050 )),
4051 }
4052}
4053
4054struct FieldInfo<'a> {
4055 ident: &'a syn::Ident,
4056 column: String,
4057 primary_key: bool,
4058 auto: bool,
4062 value_ty: &'a Type,
4065 field_type_tokens: TokenStream2,
4067 schema: TokenStream2,
4068 from_row_init: TokenStream2,
4069 from_aliased_row_init: TokenStream2,
4075 fk_inner: Option<Type>,
4079 fk_pk_kind: DetectedKind,
4085 nullable: bool,
4093 auto_now: bool,
4099 auto_now_add: bool,
4105 soft_delete: bool,
4110 generated_as: Option<String>,
4115}
4116
4117fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
4131 validate_sql_identifier(name, "table", span)
4132}
4133
4134fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
4139 if name.is_empty() {
4140 return Err(syn::Error::new(
4141 span,
4142 format!("`{kind} = \"\"` is not a valid SQL identifier"),
4143 ));
4144 }
4145 let mut chars = name.chars();
4146 let first = chars.next().unwrap();
4147 if !(first.is_ascii_alphabetic() || first == '_') {
4148 return Err(syn::Error::new(
4149 span,
4150 format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
4151 ));
4152 }
4153 for c in chars {
4154 if !(c.is_ascii_alphanumeric() || c == '_') {
4155 return Err(syn::Error::new(
4156 span,
4157 format!(
4158 "{kind} name `{name}` contains invalid character {c:?} — \
4159 SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
4160 Hyphens in particular break FK / index name derivation \
4161 downstream; use underscores instead (e.g. `{}`)",
4162 name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
4163 ),
4164 ));
4165 }
4166 }
4167 Ok(())
4168}
4169
4170fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
4171 let attrs = parse_field_attrs(field)?;
4172 let ident = field
4173 .ident
4174 .as_ref()
4175 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4176 let name = ident.to_string();
4177 let column = attrs.column.clone().unwrap_or_else(|| name.clone());
4178 let primary_key = attrs.primary_key;
4179 let DetectedType {
4180 kind,
4181 nullable,
4182 auto: detected_auto,
4183 fk_inner,
4184 } = detect_type(&field.ty)?;
4185 check_bound_compatibility(field, &attrs, kind)?;
4186 let auto = detected_auto;
4187 if attrs.auto_uuid {
4193 if kind != DetectedKind::Uuid {
4194 return Err(syn::Error::new_spanned(
4195 field,
4196 "`#[rustango(auto_uuid)]` requires the field type to be \
4197 `Auto<uuid::Uuid>`",
4198 ));
4199 }
4200 if !detected_auto {
4201 return Err(syn::Error::new_spanned(
4202 field,
4203 "`#[rustango(auto_uuid)]` requires the field type to be \
4204 wrapped in `Auto<...>` so the macro skips the column on \
4205 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
4206 ));
4207 }
4208 }
4209 if attrs.auto_now_add || attrs.auto_now {
4210 if kind != DetectedKind::DateTime {
4211 return Err(syn::Error::new_spanned(
4212 field,
4213 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4214 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
4215 ));
4216 }
4217 if !detected_auto {
4218 return Err(syn::Error::new_spanned(
4219 field,
4220 "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
4221 the field type to be wrapped in `Auto<...>` so the macro skips \
4222 the column on INSERT and the DB DEFAULT (`now()`) fires",
4223 ));
4224 }
4225 }
4226 if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
4227 return Err(syn::Error::new_spanned(
4228 field,
4229 "`#[rustango(soft_delete)]` requires the field type to be \
4230 `Option<chrono::DateTime<chrono::Utc>>`",
4231 ));
4232 }
4233 let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
4234 if detected_auto && !primary_key && !is_mixin_auto {
4235 return Err(syn::Error::new_spanned(
4236 field,
4237 "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
4238 or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
4239 `auto_now`",
4240 ));
4241 }
4242 if detected_auto && attrs.default.is_some() && !is_mixin_auto {
4243 return Err(syn::Error::new_spanned(
4244 field,
4245 "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
4246 SERIAL / BIGSERIAL already supplies a default sequence.",
4247 ));
4248 }
4249 if fk_inner.is_some() && primary_key {
4250 return Err(syn::Error::new_spanned(
4251 field,
4252 "`ForeignKey<T>` is not allowed on a primary-key field — \
4253 a row's PK is its own identity, not a reference to a parent.",
4254 ));
4255 }
4256 if attrs.generated_as.is_some() {
4257 if primary_key {
4258 return Err(syn::Error::new_spanned(
4259 field,
4260 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4261 primary-key field — a PK must be writable so the row \
4262 has an identity at INSERT time.",
4263 ));
4264 }
4265 if attrs.default.is_some() {
4266 return Err(syn::Error::new_spanned(
4267 field,
4268 "`#[rustango(generated_as = \"…\")]` cannot combine with \
4269 `default = \"…\"` — Postgres rejects DEFAULT on \
4270 generated columns. The expression IS the default.",
4271 ));
4272 }
4273 if detected_auto {
4274 return Err(syn::Error::new_spanned(
4275 field,
4276 "`#[rustango(generated_as = \"…\")]` is not allowed on \
4277 an `Auto<T>` field — generated columns are computed \
4278 by the DB, not server-assigned via a sequence. Use a \
4279 plain Rust type (e.g. `f64`).",
4280 ));
4281 }
4282 if fk_inner.is_some() {
4283 return Err(syn::Error::new_spanned(
4284 field,
4285 "`#[rustango(generated_as = \"…\")]` is not allowed on a \
4286 ForeignKey field.",
4287 ));
4288 }
4289 }
4290 let relation = relation_tokens(field, &attrs, fk_inner, table)?;
4291 let column_lit = column.as_str();
4292 let field_type_tokens = kind.variant_tokens();
4293 let max_length = optional_u32(attrs.max_length);
4294 let min = optional_i64(attrs.min);
4295 let max = optional_i64(attrs.max);
4296 let default = optional_str(attrs.default.as_deref());
4297
4298 let unique = attrs.unique;
4299 let generated_as = optional_str(attrs.generated_as.as_deref());
4300 let schema = quote! {
4301 ::rustango::core::FieldSchema {
4302 name: #name,
4303 column: #column_lit,
4304 ty: #field_type_tokens,
4305 nullable: #nullable,
4306 primary_key: #primary_key,
4307 relation: #relation,
4308 max_length: #max_length,
4309 min: #min,
4310 max: #max,
4311 default: #default,
4312 auto: #auto,
4313 unique: #unique,
4314 generated_as: #generated_as,
4315 }
4316 };
4317
4318 let from_row_init = quote! {
4319 #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
4320 };
4321 let from_aliased_row_init = quote! {
4322 #ident: ::rustango::sql::sqlx::Row::try_get(
4323 row,
4324 ::std::format!("{}__{}", prefix, #column_lit).as_str(),
4325 )?
4326 };
4327
4328 Ok(FieldInfo {
4329 ident,
4330 column,
4331 primary_key,
4332 auto,
4333 value_ty: &field.ty,
4334 field_type_tokens,
4335 schema,
4336 from_row_init,
4337 from_aliased_row_init,
4338 fk_inner: fk_inner.cloned(),
4339 fk_pk_kind: kind,
4340 nullable,
4341 auto_now: attrs.auto_now,
4342 auto_now_add: attrs.auto_now_add,
4343 soft_delete: attrs.soft_delete,
4344 generated_as: attrs.generated_as.clone(),
4345 })
4346}
4347
4348fn check_bound_compatibility(
4349 field: &syn::Field,
4350 attrs: &FieldAttrs,
4351 kind: DetectedKind,
4352) -> syn::Result<()> {
4353 if attrs.max_length.is_some() && kind != DetectedKind::String {
4354 return Err(syn::Error::new_spanned(
4355 field,
4356 "`max_length` is only valid on `String` fields (or `Option<String>`)",
4357 ));
4358 }
4359 if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
4360 return Err(syn::Error::new_spanned(
4361 field,
4362 "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
4363 ));
4364 }
4365 if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
4366 if min > max {
4367 return Err(syn::Error::new_spanned(
4368 field,
4369 format!("`min` ({min}) is greater than `max` ({max})"),
4370 ));
4371 }
4372 }
4373 Ok(())
4374}
4375
4376fn optional_u32(value: Option<u32>) -> TokenStream2 {
4377 if let Some(v) = value {
4378 quote!(::core::option::Option::Some(#v))
4379 } else {
4380 quote!(::core::option::Option::None)
4381 }
4382}
4383
4384fn optional_i64(value: Option<i64>) -> TokenStream2 {
4385 if let Some(v) = value {
4386 quote!(::core::option::Option::Some(#v))
4387 } else {
4388 quote!(::core::option::Option::None)
4389 }
4390}
4391
4392fn optional_str(value: Option<&str>) -> TokenStream2 {
4393 if let Some(v) = value {
4394 quote!(::core::option::Option::Some(#v))
4395 } else {
4396 quote!(::core::option::Option::None)
4397 }
4398}
4399
4400fn relation_tokens(
4401 field: &syn::Field,
4402 attrs: &FieldAttrs,
4403 fk_inner: Option<&syn::Type>,
4404 table: &str,
4405) -> syn::Result<TokenStream2> {
4406 if let Some(inner) = fk_inner {
4407 if attrs.fk.is_some() || attrs.o2o.is_some() {
4408 return Err(syn::Error::new_spanned(
4409 field,
4410 "`ForeignKey<T>` already declares the FK target via the type parameter — \
4411 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
4412 ));
4413 }
4414 let on = attrs.on.as_deref().unwrap_or("id");
4415 return Ok(quote! {
4416 ::core::option::Option::Some(::rustango::core::Relation::Fk {
4417 to: <#inner as ::rustango::core::Model>::SCHEMA.table,
4418 on: #on,
4419 })
4420 });
4421 }
4422 match (&attrs.fk, &attrs.o2o) {
4423 (Some(_), Some(_)) => Err(syn::Error::new_spanned(
4424 field,
4425 "`fk` and `o2o` are mutually exclusive",
4426 )),
4427 (Some(to), None) => {
4428 let on = attrs.on.as_deref().unwrap_or("id");
4429 let resolved = if to == "self" { table } else { to };
4435 Ok(quote! {
4436 ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4437 })
4438 }
4439 (None, Some(to)) => {
4440 let on = attrs.on.as_deref().unwrap_or("id");
4441 let resolved = if to == "self" { table } else { to };
4442 Ok(quote! {
4443 ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4444 })
4445 }
4446 (None, None) => {
4447 if attrs.on.is_some() {
4448 return Err(syn::Error::new_spanned(
4449 field,
4450 "`on` requires `fk` or `o2o`",
4451 ));
4452 }
4453 Ok(quote!(::core::option::Option::None))
4454 }
4455 }
4456}
4457
4458#[derive(Clone, Copy, PartialEq, Eq)]
4462enum DetectedKind {
4463 I16,
4464 I32,
4465 I64,
4466 F32,
4467 F64,
4468 Bool,
4469 String,
4470 DateTime,
4471 Date,
4472 Uuid,
4473 Json,
4474}
4475
4476impl DetectedKind {
4477 fn variant_tokens(self) -> TokenStream2 {
4478 match self {
4479 Self::I16 => quote!(::rustango::core::FieldType::I16),
4480 Self::I32 => quote!(::rustango::core::FieldType::I32),
4481 Self::I64 => quote!(::rustango::core::FieldType::I64),
4482 Self::F32 => quote!(::rustango::core::FieldType::F32),
4483 Self::F64 => quote!(::rustango::core::FieldType::F64),
4484 Self::Bool => quote!(::rustango::core::FieldType::Bool),
4485 Self::String => quote!(::rustango::core::FieldType::String),
4486 Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4487 Self::Date => quote!(::rustango::core::FieldType::Date),
4488 Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4489 Self::Json => quote!(::rustango::core::FieldType::Json),
4490 }
4491 }
4492
4493 fn is_integer(self) -> bool {
4494 matches!(self, Self::I16 | Self::I32 | Self::I64)
4495 }
4496
4497 fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
4505 match self {
4506 Self::I16 => (quote!(I16), quote!(0i16)),
4507 Self::I32 => (quote!(I32), quote!(0i32)),
4508 Self::I64 => (quote!(I64), quote!(0i64)),
4509 Self::F32 => (quote!(F32), quote!(0f32)),
4510 Self::F64 => (quote!(F64), quote!(0f64)),
4511 Self::Bool => (quote!(Bool), quote!(false)),
4512 Self::String => (quote!(String), quote!(::std::string::String::new())),
4513 Self::DateTime => (
4514 quote!(DateTime),
4515 quote!(<::chrono::DateTime<::chrono::Utc> as ::std::default::Default>::default()),
4516 ),
4517 Self::Date => (
4518 quote!(Date),
4519 quote!(<::chrono::NaiveDate as ::std::default::Default>::default()),
4520 ),
4521 Self::Uuid => (quote!(Uuid), quote!(::uuid::Uuid::nil())),
4522 Self::Json => (quote!(Json), quote!(::serde_json::Value::Null)),
4523 }
4524 }
4525}
4526
4527#[derive(Clone, Copy)]
4533struct DetectedType<'a> {
4534 kind: DetectedKind,
4535 nullable: bool,
4536 auto: bool,
4537 fk_inner: Option<&'a syn::Type>,
4538}
4539
4540fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4545 let Type::Path(TypePath { path, qself: None }) = ty else {
4546 return None;
4547 };
4548 let last = path.segments.last()?;
4549 if last.ident != "Auto" {
4550 return None;
4551 }
4552 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4553 return None;
4554 };
4555 args.args.iter().find_map(|a| match a {
4556 syn::GenericArgument::Type(t) => Some(t),
4557 _ => None,
4558 })
4559}
4560
4561fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4562 let Type::Path(TypePath { path, qself: None }) = ty else {
4563 return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4564 };
4565 let last = path
4566 .segments
4567 .last()
4568 .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4569
4570 if last.ident == "Option" {
4571 let inner = generic_inner(ty, &last.arguments, "Option")?;
4572 let inner_det = detect_type(inner)?;
4573 if inner_det.nullable {
4574 return Err(syn::Error::new_spanned(
4575 ty,
4576 "nested Option is not supported",
4577 ));
4578 }
4579 if inner_det.auto {
4580 return Err(syn::Error::new_spanned(
4581 ty,
4582 "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4583 ));
4584 }
4585 return Ok(DetectedType {
4586 nullable: true,
4587 ..inner_det
4588 });
4589 }
4590
4591 if last.ident == "Auto" {
4592 let inner = generic_inner(ty, &last.arguments, "Auto")?;
4593 let inner_det = detect_type(inner)?;
4594 if inner_det.auto {
4595 return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
4596 }
4597 if inner_det.nullable {
4598 return Err(syn::Error::new_spanned(
4599 ty,
4600 "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4601 ));
4602 }
4603 if inner_det.fk_inner.is_some() {
4604 return Err(syn::Error::new_spanned(
4605 ty,
4606 "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4607 ));
4608 }
4609 if !matches!(
4610 inner_det.kind,
4611 DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4612 ) {
4613 return Err(syn::Error::new_spanned(
4614 ty,
4615 "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4616 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4617 (DEFAULT now())",
4618 ));
4619 }
4620 return Ok(DetectedType {
4621 auto: true,
4622 ..inner_det
4623 });
4624 }
4625
4626 if last.ident == "ForeignKey" {
4627 let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
4628 let kind = match key_ty {
4636 Some(k) => detect_type(k)?.kind,
4637 None => DetectedKind::I64,
4638 };
4639 return Ok(DetectedType {
4640 kind,
4641 nullable: false,
4642 auto: false,
4643 fk_inner: Some(inner),
4644 });
4645 }
4646
4647 let kind = match last.ident.to_string().as_str() {
4648 "i16" => DetectedKind::I16,
4649 "i32" => DetectedKind::I32,
4650 "i64" => DetectedKind::I64,
4651 "f32" => DetectedKind::F32,
4652 "f64" => DetectedKind::F64,
4653 "bool" => DetectedKind::Bool,
4654 "String" => DetectedKind::String,
4655 "DateTime" => DetectedKind::DateTime,
4656 "NaiveDate" => DetectedKind::Date,
4657 "Uuid" => DetectedKind::Uuid,
4658 "Value" => DetectedKind::Json,
4659 other => {
4660 return Err(syn::Error::new_spanned(
4661 ty,
4662 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)"),
4663 ));
4664 }
4665 };
4666 Ok(DetectedType {
4667 kind,
4668 nullable: false,
4669 auto: false,
4670 fk_inner: None,
4671 })
4672}
4673
4674fn generic_inner<'a>(
4675 ty: &'a Type,
4676 arguments: &'a PathArguments,
4677 wrapper: &str,
4678) -> syn::Result<&'a Type> {
4679 let PathArguments::AngleBracketed(args) = arguments else {
4680 return Err(syn::Error::new_spanned(
4681 ty,
4682 format!("{wrapper} requires a generic argument"),
4683 ));
4684 };
4685 args.args
4686 .iter()
4687 .find_map(|a| match a {
4688 GenericArgument::Type(t) => Some(t),
4689 _ => None,
4690 })
4691 .ok_or_else(|| {
4692 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4693 })
4694}
4695
4696fn generic_pair<'a>(
4700 ty: &'a Type,
4701 arguments: &'a PathArguments,
4702 wrapper: &str,
4703) -> syn::Result<(&'a Type, Option<&'a Type>)> {
4704 let PathArguments::AngleBracketed(args) = arguments else {
4705 return Err(syn::Error::new_spanned(
4706 ty,
4707 format!("{wrapper} requires a generic argument"),
4708 ));
4709 };
4710 let mut types = args.args.iter().filter_map(|a| match a {
4711 GenericArgument::Type(t) => Some(t),
4712 _ => None,
4713 });
4714 let first = types.next().ok_or_else(|| {
4715 syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4716 })?;
4717 let second = types.next();
4718 Ok((first, second))
4719}
4720
4721fn to_snake_case(s: &str) -> String {
4722 let mut out = String::with_capacity(s.len() + 4);
4723 for (i, ch) in s.chars().enumerate() {
4724 if ch.is_ascii_uppercase() {
4725 if i > 0 {
4726 out.push('_');
4727 }
4728 out.push(ch.to_ascii_lowercase());
4729 } else {
4730 out.push(ch);
4731 }
4732 }
4733 out
4734}
4735
4736#[derive(Default)]
4742struct FormFieldAttrs {
4743 min: Option<i64>,
4744 max: Option<i64>,
4745 min_length: Option<u32>,
4746 max_length: Option<u32>,
4747}
4748
4749#[derive(Clone, Copy)]
4751enum FormFieldKind {
4752 String,
4753 I16,
4754 I32,
4755 I64,
4756 F32,
4757 F64,
4758 Bool,
4759}
4760
4761impl FormFieldKind {
4762 fn parse_method(self) -> &'static str {
4763 match self {
4764 Self::I16 => "i16",
4765 Self::I32 => "i32",
4766 Self::I64 => "i64",
4767 Self::F32 => "f32",
4768 Self::F64 => "f64",
4769 Self::String | Self::Bool => "",
4772 }
4773 }
4774}
4775
4776fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4777 let struct_name = &input.ident;
4778
4779 let Data::Struct(data) = &input.data else {
4780 return Err(syn::Error::new_spanned(
4781 struct_name,
4782 "Form can only be derived on structs",
4783 ));
4784 };
4785 let Fields::Named(named) = &data.fields else {
4786 return Err(syn::Error::new_spanned(
4787 struct_name,
4788 "Form requires a struct with named fields",
4789 ));
4790 };
4791
4792 let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4793 let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4794
4795 for field in &named.named {
4796 let ident = field
4797 .ident
4798 .as_ref()
4799 .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4800 let attrs = parse_form_field_attrs(field)?;
4801 let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4802
4803 let name_lit = ident.to_string();
4804 let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4805 field_blocks.push(parse_block);
4806 field_idents.push(ident);
4807 }
4808
4809 Ok(quote! {
4810 impl ::rustango::forms::Form for #struct_name {
4811 fn parse(
4812 data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4813 ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4814 let mut __errors = ::rustango::forms::FormErrors::default();
4815 #( #field_blocks )*
4816 if !__errors.is_empty() {
4817 return ::core::result::Result::Err(__errors);
4818 }
4819 ::core::result::Result::Ok(Self {
4820 #( #field_idents ),*
4821 })
4822 }
4823 }
4824 })
4825}
4826
4827fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4828 let mut out = FormFieldAttrs::default();
4829 for attr in &field.attrs {
4830 if !attr.path().is_ident("form") {
4831 continue;
4832 }
4833 attr.parse_nested_meta(|meta| {
4834 if meta.path.is_ident("min") {
4835 let lit: syn::LitInt = meta.value()?.parse()?;
4836 out.min = Some(lit.base10_parse::<i64>()?);
4837 return Ok(());
4838 }
4839 if meta.path.is_ident("max") {
4840 let lit: syn::LitInt = meta.value()?.parse()?;
4841 out.max = Some(lit.base10_parse::<i64>()?);
4842 return Ok(());
4843 }
4844 if meta.path.is_ident("min_length") {
4845 let lit: syn::LitInt = meta.value()?.parse()?;
4846 out.min_length = Some(lit.base10_parse::<u32>()?);
4847 return Ok(());
4848 }
4849 if meta.path.is_ident("max_length") {
4850 let lit: syn::LitInt = meta.value()?.parse()?;
4851 out.max_length = Some(lit.base10_parse::<u32>()?);
4852 return Ok(());
4853 }
4854 Err(meta.error(
4855 "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4856 ))
4857 })?;
4858 }
4859 Ok(out)
4860}
4861
4862fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4863 let Type::Path(TypePath { path, qself: None }) = ty else {
4864 return Err(syn::Error::new(
4865 span,
4866 "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4867 ));
4868 };
4869 let last = path
4870 .segments
4871 .last()
4872 .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4873
4874 if last.ident == "Option" {
4875 let inner = generic_inner(ty, &last.arguments, "Option")?;
4876 let (kind, nested) = detect_form_field(inner, span)?;
4877 if nested {
4878 return Err(syn::Error::new(
4879 span,
4880 "nested Option in Form fields is not supported",
4881 ));
4882 }
4883 return Ok((kind, true));
4884 }
4885
4886 let kind = match last.ident.to_string().as_str() {
4887 "String" => FormFieldKind::String,
4888 "i16" => FormFieldKind::I16,
4889 "i32" => FormFieldKind::I32,
4890 "i64" => FormFieldKind::I64,
4891 "f32" => FormFieldKind::F32,
4892 "f64" => FormFieldKind::F64,
4893 "bool" => FormFieldKind::Bool,
4894 other => {
4895 return Err(syn::Error::new(
4896 span,
4897 format!(
4898 "Form field type `{other}` is not supported in v0.8 — use String / \
4899 i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4900 ),
4901 ));
4902 }
4903 };
4904 Ok((kind, false))
4905}
4906
4907#[allow(clippy::too_many_lines)]
4908fn render_form_field_parse(
4909 ident: &syn::Ident,
4910 name_lit: &str,
4911 kind: FormFieldKind,
4912 nullable: bool,
4913 attrs: &FormFieldAttrs,
4914) -> TokenStream2 {
4915 let lookup = quote! {
4918 let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4919 };
4920
4921 let parsed_value = match kind {
4922 FormFieldKind::Bool => quote! {
4923 let __v: bool = match __raw {
4924 ::core::option::Option::None => false,
4925 ::core::option::Option::Some(__s) => !matches!(
4926 __s.to_ascii_lowercase().as_str(),
4927 "" | "false" | "0" | "off" | "no"
4928 ),
4929 };
4930 },
4931 FormFieldKind::String => {
4932 if nullable {
4933 quote! {
4934 let __v: ::core::option::Option<::std::string::String> = match __raw {
4935 ::core::option::Option::None => ::core::option::Option::None,
4936 ::core::option::Option::Some(__s) if __s.is_empty() => {
4937 ::core::option::Option::None
4938 }
4939 ::core::option::Option::Some(__s) => {
4940 ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4941 }
4942 };
4943 }
4944 } else {
4945 quote! {
4946 let __v: ::std::string::String = match __raw {
4947 ::core::option::Option::Some(__s) if !__s.is_empty() => {
4948 ::core::clone::Clone::clone(__s)
4949 }
4950 _ => {
4951 __errors.add(#name_lit, "This field is required.");
4952 ::std::string::String::new()
4953 }
4954 };
4955 }
4956 }
4957 }
4958 FormFieldKind::I16
4959 | FormFieldKind::I32
4960 | FormFieldKind::I64
4961 | FormFieldKind::F32
4962 | FormFieldKind::F64 => {
4963 let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
4964 let ty_lit = kind.parse_method();
4965 let default_val = match kind {
4966 FormFieldKind::I16 => quote! { 0i16 },
4967 FormFieldKind::I32 => quote! { 0i32 },
4968 FormFieldKind::I64 => quote! { 0i64 },
4969 FormFieldKind::F32 => quote! { 0f32 },
4970 FormFieldKind::F64 => quote! { 0f64 },
4971 _ => quote! { Default::default() },
4972 };
4973 if nullable {
4974 quote! {
4975 let __v: ::core::option::Option<#parse_ty> = match __raw {
4976 ::core::option::Option::None => ::core::option::Option::None,
4977 ::core::option::Option::Some(__s) if __s.is_empty() => {
4978 ::core::option::Option::None
4979 }
4980 ::core::option::Option::Some(__s) => {
4981 match __s.parse::<#parse_ty>() {
4982 ::core::result::Result::Ok(__n) => {
4983 ::core::option::Option::Some(__n)
4984 }
4985 ::core::result::Result::Err(__e) => {
4986 __errors.add(
4987 #name_lit,
4988 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4989 );
4990 ::core::option::Option::None
4991 }
4992 }
4993 }
4994 };
4995 }
4996 } else {
4997 quote! {
4998 let __v: #parse_ty = match __raw {
4999 ::core::option::Option::Some(__s) if !__s.is_empty() => {
5000 match __s.parse::<#parse_ty>() {
5001 ::core::result::Result::Ok(__n) => __n,
5002 ::core::result::Result::Err(__e) => {
5003 __errors.add(
5004 #name_lit,
5005 ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
5006 );
5007 #default_val
5008 }
5009 }
5010 }
5011 _ => {
5012 __errors.add(#name_lit, "This field is required.");
5013 #default_val
5014 }
5015 };
5016 }
5017 }
5018 }
5019 };
5020
5021 let validators = render_form_validators(name_lit, kind, nullable, attrs);
5022
5023 quote! {
5024 let #ident = {
5025 #lookup
5026 #parsed_value
5027 #validators
5028 __v
5029 };
5030 }
5031}
5032
5033fn render_form_validators(
5034 name_lit: &str,
5035 kind: FormFieldKind,
5036 nullable: bool,
5037 attrs: &FormFieldAttrs,
5038) -> TokenStream2 {
5039 let mut checks: Vec<TokenStream2> = Vec::new();
5040
5041 let val_ref = if nullable {
5042 quote! { __v.as_ref() }
5043 } else {
5044 quote! { ::core::option::Option::Some(&__v) }
5045 };
5046
5047 let is_string = matches!(kind, FormFieldKind::String);
5048 let is_numeric = matches!(
5049 kind,
5050 FormFieldKind::I16
5051 | FormFieldKind::I32
5052 | FormFieldKind::I64
5053 | FormFieldKind::F32
5054 | FormFieldKind::F64
5055 );
5056
5057 if is_string {
5058 if let Some(min_len) = attrs.min_length {
5059 let min_len_usize = min_len as usize;
5060 checks.push(quote! {
5061 if let ::core::option::Option::Some(__s) = #val_ref {
5062 if __s.len() < #min_len_usize {
5063 __errors.add(
5064 #name_lit,
5065 ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
5066 );
5067 }
5068 }
5069 });
5070 }
5071 if let Some(max_len) = attrs.max_length {
5072 let max_len_usize = max_len as usize;
5073 checks.push(quote! {
5074 if let ::core::option::Option::Some(__s) = #val_ref {
5075 if __s.len() > #max_len_usize {
5076 __errors.add(
5077 #name_lit,
5078 ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
5079 );
5080 }
5081 }
5082 });
5083 }
5084 }
5085
5086 if is_numeric {
5087 if let Some(min) = attrs.min {
5088 checks.push(quote! {
5089 if let ::core::option::Option::Some(__n) = #val_ref {
5090 if (*__n as f64) < (#min as f64) {
5091 __errors.add(
5092 #name_lit,
5093 ::std::format!("Ensure this value is greater than or equal to {}.", #min),
5094 );
5095 }
5096 }
5097 });
5098 }
5099 if let Some(max) = attrs.max {
5100 checks.push(quote! {
5101 if let ::core::option::Option::Some(__n) = #val_ref {
5102 if (*__n as f64) > (#max as f64) {
5103 __errors.add(
5104 #name_lit,
5105 ::std::format!("Ensure this value is less than or equal to {}.", #max),
5106 );
5107 }
5108 }
5109 });
5110 }
5111 }
5112
5113 quote! { #( #checks )* }
5114}
5115
5116struct ViewSetAttrs {
5121 model: syn::Path,
5122 fields: Option<Vec<String>>,
5123 filter_fields: Vec<String>,
5124 search_fields: Vec<String>,
5125 ordering: Vec<(String, bool)>,
5127 page_size: Option<usize>,
5128 read_only: bool,
5129 perms: ViewSetPermsAttrs,
5130}
5131
5132#[derive(Default)]
5133struct ViewSetPermsAttrs {
5134 list: Vec<String>,
5135 retrieve: Vec<String>,
5136 create: Vec<String>,
5137 update: Vec<String>,
5138 destroy: Vec<String>,
5139}
5140
5141fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
5142 let struct_name = &input.ident;
5143
5144 match &input.data {
5146 Data::Struct(s) => match &s.fields {
5147 Fields::Unit | Fields::Named(_) => {}
5148 Fields::Unnamed(_) => {
5149 return Err(syn::Error::new_spanned(
5150 struct_name,
5151 "ViewSet can only be derived on a unit struct or an empty named struct",
5152 ));
5153 }
5154 },
5155 _ => {
5156 return Err(syn::Error::new_spanned(
5157 struct_name,
5158 "ViewSet can only be derived on a struct",
5159 ));
5160 }
5161 }
5162
5163 let attrs = parse_viewset_attrs(input)?;
5164 let model_path = &attrs.model;
5165
5166 let fields_call = if let Some(ref fields) = attrs.fields {
5168 let lits = fields.iter().map(|f| f.as_str());
5169 quote!(.fields(&[ #(#lits),* ]))
5170 } else {
5171 quote!()
5172 };
5173
5174 let filter_fields_call = if attrs.filter_fields.is_empty() {
5175 quote!()
5176 } else {
5177 let lits = attrs.filter_fields.iter().map(|f| f.as_str());
5178 quote!(.filter_fields(&[ #(#lits),* ]))
5179 };
5180
5181 let search_fields_call = if attrs.search_fields.is_empty() {
5182 quote!()
5183 } else {
5184 let lits = attrs.search_fields.iter().map(|f| f.as_str());
5185 quote!(.search_fields(&[ #(#lits),* ]))
5186 };
5187
5188 let ordering_call = if attrs.ordering.is_empty() {
5189 quote!()
5190 } else {
5191 let pairs = attrs.ordering.iter().map(|(f, desc)| {
5192 let f = f.as_str();
5193 quote!((#f, #desc))
5194 });
5195 quote!(.ordering(&[ #(#pairs),* ]))
5196 };
5197
5198 let page_size_call = if let Some(n) = attrs.page_size {
5199 quote!(.page_size(#n))
5200 } else {
5201 quote!()
5202 };
5203
5204 let read_only_call = if attrs.read_only {
5205 quote!(.read_only())
5206 } else {
5207 quote!()
5208 };
5209
5210 let perms = &attrs.perms;
5211 let perms_call = if perms.list.is_empty()
5212 && perms.retrieve.is_empty()
5213 && perms.create.is_empty()
5214 && perms.update.is_empty()
5215 && perms.destroy.is_empty()
5216 {
5217 quote!()
5218 } else {
5219 let list_lits = perms.list.iter().map(|s| s.as_str());
5220 let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
5221 let create_lits = perms.create.iter().map(|s| s.as_str());
5222 let update_lits = perms.update.iter().map(|s| s.as_str());
5223 let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
5224 quote! {
5225 .permissions(::rustango::viewset::ViewSetPerms {
5226 list: ::std::vec![ #(#list_lits.to_owned()),* ],
5227 retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
5228 create: ::std::vec![ #(#create_lits.to_owned()),* ],
5229 update: ::std::vec![ #(#update_lits.to_owned()),* ],
5230 destroy: ::std::vec![ #(#destroy_lits.to_owned()),* ],
5231 })
5232 }
5233 };
5234
5235 Ok(quote! {
5236 impl #struct_name {
5237 pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
5240 ::rustango::viewset::ViewSet::for_model(
5241 <#model_path as ::rustango::core::Model>::SCHEMA
5242 )
5243 #fields_call
5244 #filter_fields_call
5245 #search_fields_call
5246 #ordering_call
5247 #page_size_call
5248 #perms_call
5249 #read_only_call
5250 .router(prefix, pool)
5251 }
5252 }
5253 })
5254}
5255
5256fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
5257 let mut model: Option<syn::Path> = None;
5258 let mut fields: Option<Vec<String>> = None;
5259 let mut filter_fields: Vec<String> = Vec::new();
5260 let mut search_fields: Vec<String> = Vec::new();
5261 let mut ordering: Vec<(String, bool)> = Vec::new();
5262 let mut page_size: Option<usize> = None;
5263 let mut read_only = false;
5264 let mut perms = ViewSetPermsAttrs::default();
5265
5266 for attr in &input.attrs {
5267 if !attr.path().is_ident("viewset") {
5268 continue;
5269 }
5270 attr.parse_nested_meta(|meta| {
5271 if meta.path.is_ident("model") {
5272 let path: syn::Path = meta.value()?.parse()?;
5273 model = Some(path);
5274 return Ok(());
5275 }
5276 if meta.path.is_ident("fields") {
5277 let s: LitStr = meta.value()?.parse()?;
5278 fields = Some(split_field_list(&s.value()));
5279 return Ok(());
5280 }
5281 if meta.path.is_ident("filter_fields") {
5282 let s: LitStr = meta.value()?.parse()?;
5283 filter_fields = split_field_list(&s.value());
5284 return Ok(());
5285 }
5286 if meta.path.is_ident("search_fields") {
5287 let s: LitStr = meta.value()?.parse()?;
5288 search_fields = split_field_list(&s.value());
5289 return Ok(());
5290 }
5291 if meta.path.is_ident("ordering") {
5292 let s: LitStr = meta.value()?.parse()?;
5293 ordering = parse_ordering_list(&s.value());
5294 return Ok(());
5295 }
5296 if meta.path.is_ident("page_size") {
5297 let lit: syn::LitInt = meta.value()?.parse()?;
5298 page_size = Some(lit.base10_parse::<usize>()?);
5299 return Ok(());
5300 }
5301 if meta.path.is_ident("read_only") {
5302 read_only = true;
5303 return Ok(());
5304 }
5305 if meta.path.is_ident("permissions") {
5306 meta.parse_nested_meta(|inner| {
5307 let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
5308 let s: LitStr = inner.value()?.parse()?;
5309 Ok(split_field_list(&s.value()))
5310 };
5311 if inner.path.is_ident("list") {
5312 perms.list = parse_codenames(&inner)?;
5313 } else if inner.path.is_ident("retrieve") {
5314 perms.retrieve = parse_codenames(&inner)?;
5315 } else if inner.path.is_ident("create") {
5316 perms.create = parse_codenames(&inner)?;
5317 } else if inner.path.is_ident("update") {
5318 perms.update = parse_codenames(&inner)?;
5319 } else if inner.path.is_ident("destroy") {
5320 perms.destroy = parse_codenames(&inner)?;
5321 } else {
5322 return Err(inner.error(
5323 "unknown permissions key (supported: list, retrieve, create, update, destroy)",
5324 ));
5325 }
5326 Ok(())
5327 })?;
5328 return Ok(());
5329 }
5330 Err(meta.error(
5331 "unknown viewset attribute (supported: model, fields, filter_fields, \
5332 search_fields, ordering, page_size, read_only, permissions(...))",
5333 ))
5334 })?;
5335 }
5336
5337 let model = model.ok_or_else(|| {
5338 syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
5339 })?;
5340
5341 Ok(ViewSetAttrs {
5342 model,
5343 fields,
5344 filter_fields,
5345 search_fields,
5346 ordering,
5347 page_size,
5348 read_only,
5349 perms,
5350 })
5351}
5352
5353struct SerializerContainerAttrs {
5356 model: syn::Path,
5357}
5358
5359#[derive(Default)]
5360struct SerializerFieldAttrs {
5361 read_only: bool,
5362 write_only: bool,
5363 source: Option<String>,
5364 skip: bool,
5365 method: Option<String>,
5369 validate: Option<String>,
5374 nested: bool,
5384 nested_strict: bool,
5389 many: Option<syn::Type>,
5398}
5399
5400fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
5401 let mut model: Option<syn::Path> = None;
5402 for attr in &input.attrs {
5403 if !attr.path().is_ident("serializer") {
5404 continue;
5405 }
5406 attr.parse_nested_meta(|meta| {
5407 if meta.path.is_ident("model") {
5408 let _eq: syn::Token![=] = meta.input.parse()?;
5409 model = Some(meta.input.parse()?);
5410 return Ok(());
5411 }
5412 Err(meta.error("unknown serializer container attribute (supported: `model`)"))
5413 })?;
5414 }
5415 let model = model.ok_or_else(|| {
5416 syn::Error::new_spanned(
5417 &input.ident,
5418 "`#[serializer(model = SomeModel)]` is required",
5419 )
5420 })?;
5421 Ok(SerializerContainerAttrs { model })
5422}
5423
5424fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
5425 let mut out = SerializerFieldAttrs::default();
5426 for attr in &field.attrs {
5427 if !attr.path().is_ident("serializer") {
5428 continue;
5429 }
5430 attr.parse_nested_meta(|meta| {
5431 if meta.path.is_ident("read_only") {
5432 out.read_only = true;
5433 return Ok(());
5434 }
5435 if meta.path.is_ident("write_only") {
5436 out.write_only = true;
5437 return Ok(());
5438 }
5439 if meta.path.is_ident("skip") {
5440 out.skip = true;
5441 return Ok(());
5442 }
5443 if meta.path.is_ident("source") {
5444 let s: LitStr = meta.value()?.parse()?;
5445 out.source = Some(s.value());
5446 return Ok(());
5447 }
5448 if meta.path.is_ident("method") {
5449 let s: LitStr = meta.value()?.parse()?;
5450 out.method = Some(s.value());
5451 return Ok(());
5452 }
5453 if meta.path.is_ident("validate") {
5454 let s: LitStr = meta.value()?.parse()?;
5455 out.validate = Some(s.value());
5456 return Ok(());
5457 }
5458 if meta.path.is_ident("many") {
5459 let _eq: syn::Token![=] = meta.input.parse()?;
5460 out.many = Some(meta.input.parse()?);
5461 return Ok(());
5462 }
5463 if meta.path.is_ident("nested") {
5464 out.nested = true;
5465 if meta.input.peek(syn::token::Paren) {
5468 meta.parse_nested_meta(|inner| {
5469 if inner.path.is_ident("strict") {
5470 out.nested_strict = true;
5471 return Ok(());
5472 }
5473 Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
5474 })?;
5475 }
5476 return Ok(());
5477 }
5478 Err(meta.error(
5479 "unknown serializer field attribute (supported: \
5480 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
5481 ))
5482 })?;
5483 }
5484 if out.read_only && out.write_only {
5486 return Err(syn::Error::new_spanned(
5487 field,
5488 "a field cannot be both `read_only` and `write_only`",
5489 ));
5490 }
5491 if out.method.is_some() && out.source.is_some() {
5492 return Err(syn::Error::new_spanned(
5493 field,
5494 "`method` and `source` are mutually exclusive — `method` computes \
5495 the value from a method, `source` reads it from a different model field",
5496 ));
5497 }
5498 Ok(out)
5499}
5500
5501fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
5502 let struct_name = &input.ident;
5503 let struct_name_lit = struct_name.to_string();
5504
5505 let Data::Struct(data) = &input.data else {
5506 return Err(syn::Error::new_spanned(
5507 struct_name,
5508 "Serializer can only be derived on structs",
5509 ));
5510 };
5511 let Fields::Named(named) = &data.fields else {
5512 return Err(syn::Error::new_spanned(
5513 struct_name,
5514 "Serializer requires a struct with named fields",
5515 ));
5516 };
5517
5518 let container = parse_serializer_container_attrs(input)?;
5519 let model_path = &container.model;
5520
5521 #[allow(dead_code)]
5525 struct FieldInfo {
5526 ident: syn::Ident,
5527 ty: syn::Type,
5528 attrs: SerializerFieldAttrs,
5529 }
5530 let mut fields_info: Vec<FieldInfo> = Vec::new();
5531 for field in &named.named {
5532 let ident = field.ident.clone().expect("named field has ident");
5533 let attrs = parse_serializer_field_attrs(field)?;
5534 fields_info.push(FieldInfo {
5535 ident,
5536 ty: field.ty.clone(),
5537 attrs,
5538 });
5539 }
5540
5541 let from_model_fields = fields_info.iter().map(|fi| {
5543 let ident = &fi.ident;
5544 let ty = &fi.ty;
5545 if let Some(_inner) = &fi.attrs.many {
5546 quote! { #ident: ::std::vec::Vec::new() }
5550 } else if let Some(method) = &fi.attrs.method {
5551 let method_ident = syn::Ident::new(method, ident.span());
5555 quote! { #ident: Self::#method_ident(model) }
5556 } else if fi.attrs.nested {
5557 let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5573 let src_ident = syn::Ident::new(&src_name, ident.span());
5574 if fi.attrs.nested_strict {
5575 let panic_msg = format!(
5576 "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5577 call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5578 );
5579 quote! {
5580 #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5581 model.#src_ident.value().expect(#panic_msg),
5582 )
5583 }
5584 } else {
5585 quote! {
5586 #ident: match model.#src_ident.value() {
5587 ::core::option::Option::Some(__loaded) =>
5588 <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5589 ::core::option::Option::None =>
5590 ::core::default::Default::default(),
5591 }
5592 }
5593 }
5594 } else if fi.attrs.write_only || fi.attrs.skip {
5595 quote! { #ident: ::core::default::Default::default() }
5597 } else if let Some(src) = &fi.attrs.source {
5598 let src_ident = syn::Ident::new(src, ident.span());
5599 quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5600 } else {
5601 quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5602 }
5603 });
5604
5605 let validator_calls: Vec<_> = fields_info
5609 .iter()
5610 .filter_map(|fi| {
5611 let ident = &fi.ident;
5612 let name_lit = ident.to_string();
5613 let method = fi.attrs.validate.as_ref()?;
5614 let method_ident = syn::Ident::new(method, ident.span());
5615 Some(quote! {
5616 if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5617 __errors.add(#name_lit.to_owned(), __e);
5618 }
5619 })
5620 })
5621 .collect();
5622 let validate_method = if validator_calls.is_empty() {
5623 quote! {}
5624 } else {
5625 quote! {
5626 impl #struct_name {
5627 pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5631 let mut __errors = ::rustango::forms::FormErrors::default();
5632 #( #validator_calls )*
5633 if __errors.is_empty() {
5634 ::core::result::Result::Ok(())
5635 } else {
5636 ::core::result::Result::Err(__errors)
5637 }
5638 }
5639 }
5640 }
5641 };
5642
5643 let many_setters: Vec<_> = fields_info
5647 .iter()
5648 .filter_map(|fi| {
5649 let many_ty = fi.attrs.many.as_ref()?;
5650 let ident = &fi.ident;
5651 let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5652 Some(quote! {
5653 pub fn #setter(
5658 &mut self,
5659 models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5660 ) -> &mut Self {
5661 self.#ident = models.iter()
5662 .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5663 .collect();
5664 self
5665 }
5666 })
5667 })
5668 .collect();
5669 let many_setters_impl = if many_setters.is_empty() {
5670 quote! {}
5671 } else {
5672 quote! {
5673 impl #struct_name {
5674 #( #many_setters )*
5675 }
5676 }
5677 };
5678
5679 let output_fields: Vec<_> = fields_info
5681 .iter()
5682 .filter(|fi| !fi.attrs.write_only)
5683 .collect();
5684 let output_field_count = output_fields.len();
5685 let serialize_fields = output_fields.iter().map(|fi| {
5686 let ident = &fi.ident;
5687 let name_lit = ident.to_string();
5688 quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5689 });
5690
5691 let writable_lits: Vec<_> = fields_info
5693 .iter()
5694 .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5695 .map(|fi| fi.ident.to_string())
5696 .collect();
5697
5698 let openapi_impl = {
5702 #[cfg(feature = "openapi")]
5703 {
5704 let property_calls = output_fields.iter().map(|fi| {
5705 let ident = &fi.ident;
5706 let name_lit = ident.to_string();
5707 let ty = &fi.ty;
5708 let nullable_call = if is_option(ty) {
5709 quote! { .nullable() }
5710 } else {
5711 quote! {}
5712 };
5713 quote! {
5714 .property(
5715 #name_lit,
5716 <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5717 #nullable_call,
5718 )
5719 }
5720 });
5721 let required_lits: Vec<_> = output_fields
5722 .iter()
5723 .filter(|fi| !is_option(&fi.ty))
5724 .map(|fi| fi.ident.to_string())
5725 .collect();
5726 quote! {
5727 impl ::rustango::openapi::OpenApiSchema for #struct_name {
5728 fn openapi_schema() -> ::rustango::openapi::Schema {
5729 ::rustango::openapi::Schema::object()
5730 #( #property_calls )*
5731 .required([ #( #required_lits ),* ])
5732 }
5733 }
5734 }
5735 }
5736 #[cfg(not(feature = "openapi"))]
5737 {
5738 quote! {}
5739 }
5740 };
5741
5742 Ok(quote! {
5743 impl ::rustango::serializer::ModelSerializer for #struct_name {
5744 type Model = #model_path;
5745
5746 fn from_model(model: &Self::Model) -> Self {
5747 Self {
5748 #( #from_model_fields ),*
5749 }
5750 }
5751
5752 fn writable_fields() -> &'static [&'static str] {
5753 &[ #( #writable_lits ),* ]
5754 }
5755 }
5756
5757 impl ::serde::Serialize for #struct_name {
5758 fn serialize<S>(&self, serializer: S)
5759 -> ::core::result::Result<S::Ok, S::Error>
5760 where
5761 S: ::serde::Serializer,
5762 {
5763 use ::serde::ser::SerializeStruct;
5764 let mut __state = serializer.serialize_struct(
5765 #struct_name_lit,
5766 #output_field_count,
5767 )?;
5768 #( #serialize_fields )*
5769 __state.end()
5770 }
5771 }
5772
5773 #openapi_impl
5774
5775 #validate_method
5776
5777 #many_setters_impl
5778 })
5779}
5780
5781#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5785fn is_option(ty: &syn::Type) -> bool {
5786 if let syn::Type::Path(p) = ty {
5787 if let Some(last) = p.path.segments.last() {
5788 return last.ident == "Option";
5789 }
5790 }
5791 false
5792}